diff --git a/app/design-system/design-system-hedvig/build.gradle.kts b/app/design-system/design-system-hedvig/build.gradle.kts index 72307f7a1f..14b5b844e8 100644 --- a/app/design-system/design-system-hedvig/build.gradle.kts +++ b/app/design-system/design-system-hedvig/build.gradle.kts @@ -24,6 +24,5 @@ dependencies { implementation(projects.coreResources) implementation(projects.designSystemInternals) implementation(projects.navigationCore) - implementation(projects.placeholder) implementation(projects.coreUiData) } diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/FileContainer.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/FileContainer.kt index 30de343f33..d4fa343fee 100644 --- a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/FileContainer.kt +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/FileContainer.kt @@ -15,9 +15,9 @@ import coil.ImageLoader import coil.compose.AsyncImage import coil.compose.AsyncImagePainter import coil.request.ImageRequest -import com.hedvig.android.placeholder.PlaceholderHighlight -import com.hedvig.android.placeholder.fade -import com.hedvig.android.placeholder.placeholder +import com.hedvig.android.design.system.hedvig.placeholder.PlaceholderHighlight +import com.hedvig.android.design.system.hedvig.placeholder.fade +import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder @Composable internal fun FileContainer( @@ -62,7 +62,7 @@ internal fun FileContainer( }, modifier = modifier .height(109.dp) - .placeholder(visible = loadedImageIntrinsicSize.value == null, highlight = PlaceholderHighlight.fade()) + .hedvigPlaceholder(visible = loadedImageIntrinsicSize.value == null, highlight = PlaceholderHighlight.fade()) .clip(HedvigTheme.shapes.cornerMedium), ) } diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt index 1820b67dfd..1e37671063 100644 --- a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt @@ -36,9 +36,9 @@ import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightS import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightShade.MEDIUM import com.hedvig.android.design.system.hedvig.icon.HedvigIcons import com.hedvig.android.design.system.hedvig.icon.HelipadOutline -import com.hedvig.android.placeholder.PlaceholderHighlight -import com.hedvig.android.placeholder.placeholder -import com.hedvig.android.placeholder.shimmer +import com.hedvig.android.design.system.hedvig.placeholder.PlaceholderHighlight +import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder +import com.hedvig.android.design.system.hedvig.placeholder.shimmer @Composable fun HedvigCard( @@ -86,7 +86,7 @@ fun InsuranceCard( painter = ColorPainter(Color.Black.copy(alpha = 0.3f)), modifier = Modifier .matchParentSize() - .placeholder(visible = true, highlight = PlaceholderHighlight.shimmer()), + .hedvigPlaceholder(visible = true, highlight = PlaceholderHighlight.shimmer()), contentDescription = null, ) } else { @@ -127,14 +127,14 @@ fun InsuranceCard( HedvigText( topText, color = HedvigTheme.colorScheme.textWhite, - modifier = Modifier.placeholder(visible = isLoading, highlight = PlaceholderHighlight.shimmer()), + modifier = Modifier.hedvigPlaceholder(visible = isLoading, highlight = PlaceholderHighlight.shimmer()), ) Spacer(Modifier.height(4.dp)) HedvigTheme(darkTheme = true) { HedvigText( text = bottomText, color = HedvigTheme.colorScheme.textSecondaryTranslucent, - modifier = Modifier.placeholder(visible = isLoading, highlight = PlaceholderHighlight.shimmer()), + modifier = Modifier.hedvigPlaceholder(visible = isLoading, highlight = PlaceholderHighlight.shimmer()), ) } } diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/Placeholder.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/Placeholder.kt new file mode 100644 index 0000000000..02f721fe90 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/Placeholder.kt @@ -0,0 +1,351 @@ +package com.hedvig.android.design.system.hedvig.placeholder + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.InfiniteRepeatableSpec +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.toRect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.LayoutDirection +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Contains default values used by [Modifier.hedvigPlaceholder] and [PlaceholderHighlight]. + */ +internal object PlaceholderDefaults { + /** + * The default [InfiniteRepeatableSpec] to use for [fade]. + */ + internal val fadeAnimationSpec: InfiniteRepeatableSpec by lazy { + infiniteRepeatable( + animation = tween(delayMillis = 200, durationMillis = 600), + repeatMode = RepeatMode.Reverse, + ) + } + + /** + * The default [InfiniteRepeatableSpec] to use for [shimmer]. + */ + internal val shimmerAnimationSpec: InfiniteRepeatableSpec by lazy { + infiniteRepeatable( + animation = tween(durationMillis = 1700, delayMillis = 0), + repeatMode = RepeatMode.Restart, + ) + } +} + +/** + * Draws some skeleton UI which is typically used whilst content is 'loading'. + * + * A version of this modifier which uses appropriate values for Material themed apps is available + * in the 'Placeholder Material' library. + * + * You can provide a [PlaceholderHighlight] which runs an highlight animation on the placeholder. + * The [shimmer] and [fade] implementations are provided for easy usage. + * + * A cross-fade transition will be applied to the content and placeholder UI when the [visible] + * value changes. The transition can be customized via the [contentFadeAnimationSpec] and + * [placeholderFadeAnimationSpec] parameters. + * + * You can find more information on the pattern at the Material Theming + * [Placeholder UI](https://material.io/design/communication/launch-screen.html#placeholder-ui) + * guidelines. + * + * @sample com.google.accompanist.sample.placeholder.DocSample_Foundation_Placeholder + * + * @param visible whether the placeholder should be visible or not. + * @param color the color used to draw the placeholder UI. + * @param shape desired shape of the placeholder. Defaults to [RectangleShape]. + * @param highlight optional highlight animation. + * @param placeholderFadeAnimationSpec The transition spec to use when fading the placeholder + * on/off screen. The boolean parameter defined for the transition is [visible]. + * @param contentFadeAnimationSpec The transition spec to use when fading the content + * on/off screen. The boolean parameter defined for the transition is [visible]. + */ + +internal fun Modifier.hedvigPlaceholder( + visible: Boolean, + color: Color, + shape: Shape = RectangleShape, + highlight: PlaceholderHighlight? = null, + placeholderFadeAnimationSpec: AnimationSpec = spring(), + contentFadeAnimationSpec: AnimationSpec = spring(), +): Modifier = this then PlaceholderElement( + visible = visible, + color = color, + shape = shape, + highlight = highlight, + placeholderFadeAnimationSpec = placeholderFadeAnimationSpec, + contentFadeAnimationSpec = contentFadeAnimationSpec, +) + +private data class PlaceholderElement( + private val visible: Boolean, + private val color: Color, + private val shape: Shape, + private val highlight: PlaceholderHighlight?, + private val placeholderFadeAnimationSpec: AnimationSpec, + private val contentFadeAnimationSpec: AnimationSpec, +) : ModifierNodeElement() { + override fun create(): PlaceholderNode = PlaceholderNode( + visible = visible, + color = color, + shape = shape, + highlight = highlight, + placeholderFadeAnimationSpec = placeholderFadeAnimationSpec, + contentFadeAnimationSpec = contentFadeAnimationSpec, + ) + + override fun update(node: PlaceholderNode) { + node.apply { + updateVisible(visible) + updateColor(color) + updateShape(shape) + updateHighlight(highlight) + updatePlaceholderFadeAnimationSpec(placeholderFadeAnimationSpec) + updateContentFadeAnimationSpec(contentFadeAnimationSpec) + } + } + + override fun InspectorInfo.inspectableProperties() { + name = "placeholder" + value = visible + properties["visible"] = visible + properties["color"] = color + properties["highlight"] = highlight + properties["shape"] = shape + } +} + +private class PlaceholderNode( + private var visible: Boolean, + private var color: Color, + private var shape: Shape = RectangleShape, + private var highlight: PlaceholderHighlight? = null, + private var placeholderFadeAnimationSpec: AnimationSpec, + private var contentFadeAnimationSpec: AnimationSpec, +) : DrawModifierNode, Modifier.Node() { + private val crossfadeTransitionState = MutableTransitionState(visible).apply { + targetState = visible + } + + private val paint: Paint = Paint() + + private var contentAlpha: Float = if (visible) 0F else 1F + private var placeholderAlpha: Float = if (visible) 1F else 0F + + // The current highlight animation progress + private var highlightProgress: Float = 0F + + // Values used for caching purposes + private var lastSize: Size = Size.Unspecified + private var lastLayoutDirection: LayoutDirection? = null + private var lastOutline: Outline? = null + + fun updateVisible(visible: Boolean) { + if (this.visible != visible) { + this.visible = visible + crossfadeTransitionState.targetState = visible + coroutineScope.runAlphaAnimations() + coroutineScope.runHighlightAnimation() + } + } + + fun updateColor(color: Color) { + if (this.color != color) { + this.color = color + } + } + + fun updateShape(shape: Shape) { + if (this.shape != shape) { + this.shape = shape + } + } + + fun updateHighlight(highlight: PlaceholderHighlight?) { + if (this.highlight != highlight) { + this.highlight = highlight + coroutineScope.runHighlightAnimation() + } + } + + fun updatePlaceholderFadeAnimationSpec(placeholderFadeAnimationSpec: AnimationSpec) { + if (this.placeholderFadeAnimationSpec != placeholderFadeAnimationSpec) { + this.placeholderFadeAnimationSpec = placeholderFadeAnimationSpec + } + } + + fun updateContentFadeAnimationSpec(contentFadeAnimationSpec: AnimationSpec) { + if (this.contentFadeAnimationSpec != contentFadeAnimationSpec) { + this.contentFadeAnimationSpec = contentFadeAnimationSpec + } + } + + override fun onAttach() { + coroutineScope.runAlphaAnimations() + coroutineScope.runHighlightAnimation() + } + + private val placeholderAnimation = Animatable(placeholderAlpha) + private val contentAnimation = Animatable(contentAlpha) + + private fun CoroutineScope.runAlphaAnimations() { + launch { + placeholderAnimation.animateTo( + targetValue = if (visible) 1F else 0F, + placeholderFadeAnimationSpec, + ) { + val placeholderAlphaWas0 = placeholderAlpha < 0.01F + placeholderAlpha = value + if (placeholderAlphaWas0 && placeholderAlpha >= 0.01F && !visible) { + coroutineScope.runHighlightAnimation() + } + invalidateDraw() + } + } + + launch { + contentAnimation.animateTo( + targetValue = if (visible) 0F else 1F, + contentFadeAnimationSpec, + ) { + contentAlpha = value + invalidateDraw() + } + } + } + + private val infiniteAnimation = Animatable(0F) + + private fun CoroutineScope.runHighlightAnimation() { + val isEffectivelyVisible = visible || placeholderAlpha >= 0.01F + val animationSpec = highlight?.animationSpec + if (isEffectivelyVisible && animationSpec != null) { + launch { + infiniteAnimation.animateTo(1F, animationSpec) { + highlightProgress = value + invalidateDraw() + } + } + } + } + + override fun ContentDrawScope.draw() { + val drawContent = ::drawContent + + // Draw the composable content first + if (contentAlpha in 0.01F..0.99F) { + // If the content alpha is between 1% and 99%, draw it in a layer with + // the alpha applied + paint.alpha = contentAlpha + withLayer(paint) { + drawContent() + } + } else if (contentAlpha >= 0.99F) { + // If the content alpha is > 99%, draw it with no alpha + drawContent() + } + + if (placeholderAlpha in 0.01F..0.99F) { + // If the placeholder alpha is between 1% and 99%, draw it in a layer with + // the alpha applied + paint.alpha = placeholderAlpha + withLayer(paint) { + lastOutline = drawPlaceholder( + shape = shape, + color = color, + highlight = highlight, + progress = highlightProgress, + lastOutline = lastOutline, + lastLayoutDirection = lastLayoutDirection, + lastSize = lastSize, + ) + } + } else if (placeholderAlpha >= 0.99F) { + // If the placeholder alpha is > 99%, draw it with no alpha + lastOutline = drawPlaceholder( + shape = shape, + color = color, + highlight = highlight, + progress = highlightProgress, + lastOutline = lastOutline, + lastLayoutDirection = lastLayoutDirection, + lastSize = lastSize, + ) + } + + // Keep track of the last size & layout direction + lastSize = size + lastLayoutDirection = layoutDirection + } +} + +private fun DrawScope.drawPlaceholder( + shape: Shape, + color: Color, + highlight: PlaceholderHighlight?, + progress: Float, + lastOutline: Outline?, + lastLayoutDirection: LayoutDirection?, + lastSize: Size?, +): Outline? { + // shortcut to avoid Outline calculation and allocation + if (shape === RectangleShape) { + // Draw the initial background color + drawRect(color = color) + + if (highlight != null) { + drawRect( + brush = highlight.brush(progress, size), + alpha = highlight.alpha(progress), + ) + } + // We didn't create an outline so return null + return null + } + + // Otherwise we need to create an outline from the shape + val outline = lastOutline.takeIf { + size == lastSize && layoutDirection == lastLayoutDirection + } ?: shape.createOutline(size, layoutDirection, this) + + // Draw the placeholder color + drawOutline(outline = outline, color = color) + + if (highlight != null) { + drawOutline( + outline = outline, + brush = highlight.brush(progress, size), + alpha = highlight.alpha(progress), + ) + } + + // Return the outline we used + return outline +} + +private inline fun DrawScope.withLayer(paint: Paint, drawBlock: DrawScope.() -> Unit) = drawIntoCanvas { canvas -> + canvas.saveLayer(size.toRect(), paint) + drawBlock() + canvas.restore() +} diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/PlaceholderHighlight.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/PlaceholderHighlight.kt new file mode 100644 index 0000000000..601a6fcfc6 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/PlaceholderHighlight.kt @@ -0,0 +1,131 @@ +package com.hedvig.android.design.system.hedvig.placeholder + +import androidx.annotation.FloatRange +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.InfiniteRepeatableSpec +import androidx.compose.runtime.Stable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.util.lerp +import kotlin.math.max + +/** + * A class which provides a brush to paint placeholder based on progress. + */ +@Stable +interface PlaceholderHighlight { + /** + * The optional [AnimationSpec] to use when running the animation for this highlight. + */ + val animationSpec: InfiniteRepeatableSpec? + + /** + * Return a [Brush] to draw for the given [progress] and [size]. + * + * @param progress the current animated progress in the range of 0f..1f. + * @param size The size of the current layout to draw in. + */ + fun brush( + @FloatRange(from = 0.0, to = 1.0) progress: Float, + size: Size, + ): Brush + + /** + * Return the desired alpha value used for drawing the [Brush] returned from [brush]. + * + * @param progress the current animated progress in the range of 0f..1f. + */ + @FloatRange(from = 0.0, to = 1.0) + fun alpha(progress: Float): Float + + companion object +} + +/** + * Creates a [Fade] brush with the given initial and target colors. + * + * @sample com.google.accompanist.sample.placeholder.DocSample_Foundation_PlaceholderFade + * + * @param highlightColor the color of the highlight which is faded in/out. + * @param animationSpec the [AnimationSpec] to configure the animation. + */ +internal fun PlaceholderHighlight.Companion.fade( + highlightColor: Color, + animationSpec: InfiniteRepeatableSpec = PlaceholderDefaults.fadeAnimationSpec, +): PlaceholderHighlight = Fade( + highlightColor = highlightColor, + animationSpec = animationSpec, +) + +/** + * Creates a [PlaceholderHighlight] which 'shimmers', using the given [highlightColor]. + * + * The highlight starts at the top-start, and then grows to the bottom-end during the animation. + * During that time it is also faded in, from 0f..progressForMaxAlpha, and then faded out from + * progressForMaxAlpha..1f. + * + * @sample com.google.accompanist.sample.placeholder.DocSample_Foundation_PlaceholderShimmer + * + * @param highlightColor the color of the highlight 'shimmer'. + * @param animationSpec the [AnimationSpec] to configure the animation. + * @param progressForMaxAlpha The progress where the shimmer should be at it's peak opacity. + * Defaults to 0.6f. + */ +internal fun PlaceholderHighlight.Companion.shimmer( + highlightColor: Color, + animationSpec: InfiniteRepeatableSpec = PlaceholderDefaults.shimmerAnimationSpec, + @FloatRange(from = 0.0, to = 1.0) progressForMaxAlpha: Float = 0.6f, +): PlaceholderHighlight = Shimmer( + highlightColor = highlightColor, + animationSpec = animationSpec, + progressForMaxAlpha = progressForMaxAlpha, +) + +private data class Fade( + private val highlightColor: Color, + override val animationSpec: InfiniteRepeatableSpec, +) : PlaceholderHighlight { + private val brush = SolidColor(highlightColor) + + override fun brush(progress: Float, size: Size): Brush = brush + + override fun alpha(progress: Float): Float = progress +} + +private data class Shimmer( + private val highlightColor: Color, + override val animationSpec: InfiniteRepeatableSpec, + private val progressForMaxAlpha: Float = 0.6f, +) : PlaceholderHighlight { + override fun brush(progress: Float, size: Size): Brush = Brush.radialGradient( + colors = listOf( + highlightColor.copy(alpha = 0f), + highlightColor, + highlightColor.copy(alpha = 0f), + ), + center = Offset(x = 0f, y = 0f), + radius = (max(size.width, size.height) * progress * 2).coerceAtLeast(0.01f), + ) + + override fun alpha(progress: Float): Float = when { + // From 0f...ProgressForOpaqueAlpha we animate from 0..1 + progress <= progressForMaxAlpha -> { + lerp( + start = 0f, + stop = 1f, + fraction = progress / progressForMaxAlpha, + ) + } + // From ProgressForOpaqueAlpha..1f we animate from 1..0 + else -> { + lerp( + start = 1f, + stop = 0f, + fraction = (progress - progressForMaxAlpha) / (1f - progressForMaxAlpha), + ) + } + } +} diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/PlaceholderHightlightMaterial3.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/PlaceholderHightlightMaterial3.kt new file mode 100644 index 0000000000..8f883469b4 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/PlaceholderHightlightMaterial3.kt @@ -0,0 +1,48 @@ +package com.hedvig.android.design.system.hedvig.placeholder + +import androidx.annotation.FloatRange +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.InfiniteRepeatableSpec +import androidx.compose.runtime.Composable +import com.hedvig.android.design.system.hedvig.HedvigTheme + +/** + * Creates a [PlaceholderHighlight] which fades in an appropriate color, using the + * given [animationSpec]. + * + * @sample com.google.accompanist.sample.placeholder.DocSample_Material_PlaceholderFade + * + * @param animationSpec the [AnimationSpec] to configure the animation. + */ +@Composable +fun PlaceholderHighlight.Companion.fade( + animationSpec: InfiniteRepeatableSpec = PlaceholderDefaults.fadeAnimationSpec, +): PlaceholderHighlight = PlaceholderHighlight.fade( + highlightColor = PlaceholderDefaults.fadeHighlightColor(), + animationSpec = animationSpec, +) + +/** + * Creates a [PlaceholderHighlight] which 'shimmers', using a default color. + * + * The highlight starts at the top-start, and then grows to the bottom-end during the animation. + * During that time it is also faded in, from 0f..progressForMaxAlpha, and then faded out from + * progressForMaxAlpha..1f. + * + * @sample com.google.accompanist.sample.placeholder.DocSample_Material_PlaceholderShimmer + * + * @param animationSpec the [AnimationSpec] to configure the animation. + * @param progressForMaxAlpha The progress where the shimmer should be at it's peak opacity. + * Defaults to 0.6f. + */ +@Composable +fun PlaceholderHighlight.Companion.shimmer( + animationSpec: InfiniteRepeatableSpec = PlaceholderDefaults.shimmerAnimationSpec, + @FloatRange(from = 0.0, to = 1.0) progressForMaxAlpha: Float = 0.6f, +): PlaceholderHighlight = PlaceholderHighlight.shimmer( + highlightColor = PlaceholderDefaults.shimmerHighlightColor( + backgroundColor = HedvigTheme.colorScheme.surfacePrimary, + ), + animationSpec = animationSpec, + progressForMaxAlpha = progressForMaxAlpha, +) diff --git a/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/PlaceholderMaterial3.kt b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/PlaceholderMaterial3.kt new file mode 100644 index 0000000000..675ddcf1c2 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/main/kotlin/com/hedvig/android/design/system/hedvig/placeholder/PlaceholderMaterial3.kt @@ -0,0 +1,160 @@ +package com.hedvig.android.design.system.hedvig.placeholder + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.spring +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.unit.dp +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigPreview +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.contentColorFor + +/** + * Returns the value used as the the `color` parameter value on [Modifier.hedvigPlaceholder]. + * + * @param backgroundColor The current background color of the layout. Defaults to + * `HedvigTheme.colorScheme.surfacePrimary`. + * @param contentColor The content color to be used on top of [backgroundColor]. + * @param contentAlpha The alpha component to set on [contentColor] when compositing the color + * on top of [backgroundColor]. Defaults to `0.1f`. + */ +@Suppress("UnusedReceiverParameter") +@Composable +internal fun PlaceholderDefaults.color( + backgroundColor: Color = HedvigTheme.colorScheme.surfacePrimary, + contentColor: Color = contentColorFor(backgroundColor), + contentAlpha: Float = 0.1f, +): Color = contentColor.copy(contentAlpha).compositeOver(backgroundColor) + +/** + * Returns the value used as the the `highlightColor` parameter value of + * [PlaceholderHighlight.Companion.fade]. + * + * @param backgroundColor The current background color of the layout. Defaults to + * `MaterialTheme.colorScheme.surface`. + * @param alpha The alpha component to set on [backgroundColor]. Defaults to `0.3f`. + */ +@Suppress("UnusedReceiverParameter") +@Composable +internal fun PlaceholderDefaults.fadeHighlightColor( + backgroundColor: Color = HedvigTheme.colorScheme.surfacePrimary, + alpha: Float = 0.55f, +): Color = backgroundColor.copy(alpha = alpha) + +/** + * Returns the value used as the the `highlightColor` parameter value of + * [PlaceholderHighlight.Companion.shimmer]. + * + * @param backgroundColor The current background color of the layout. Defaults to + * `MaterialTheme.colorScheme.inverseSurface`. + * @param alpha The alpha component to set on [backgroundColor]. Defaults to `0.75f`. + */ +@Suppress("UnusedReceiverParameter") +@Composable +internal fun PlaceholderDefaults.shimmerHighlightColor( + backgroundColor: Color = HedvigTheme.colorScheme.surfacePrimary, + alpha: Float = 0.75f, +): Color { + return backgroundColor.copy(alpha = alpha) +} + +/** + * Draws some skeleton UI which is typically used whilst content is 'loading'. + * + * To customize the color and shape of the placeholder, you can use the foundation version of + * [Modifier.hedvigPlaceholder], along with the values provided by [PlaceholderDefaults]. + * + * A cross-fade transition will be applied to the content and placeholder UI when the [visible] + * value changes. The transition can be customized via the [contentFadeAnimationSpec] and + * [placeholderFadeAnimationSpec] parameters. + * + * You can provide a [PlaceholderHighlight] which runs an highlight animation on the placeholder. + * The [shimmer] and [fade] implementations are provided for easy usage. + * + * You can find more information on the pattern at the Material Theming + * [Placeholder UI](https://material.io/design/communication/launch-screen.html#placeholder-ui) + * guidelines. + * + * @sample com.google.accompanist.sample.placeholder.DocSample_Material_Placeholder + * + * @param visible whether the placeholder should be visible or not. + * @param color the color used to draw the placeholder UI. If [Color.Unspecified] is provided, + * the placeholder will use [PlaceholderDefaults.color]. + * @param shape desired shape of the placeholder. If null is provided the placeholder + * will use the small shape set in [MaterialTheme.shapes]. + * @param highlight optional highlight animation. + * @param placeholderFadeAnimationSpec The transition spec to use when fading the placeholder + * on/off screen. The boolean parameter defined for the transition is [visible]. + * @param contentFadeAnimationSpec The transition spec to use when fading the content + * on/off screen. The boolean parameter defined for the transition is [visible]. + */ +fun Modifier.hedvigPlaceholder( + visible: Boolean, + color: Color = Color.Unspecified, + shape: Shape? = null, + highlight: PlaceholderHighlight? = null, + placeholderFadeAnimationSpec: AnimationSpec = spring(), + contentFadeAnimationSpec: AnimationSpec = spring(), +): Modifier = this.composed { + Modifier.hedvigPlaceholder( + visible = visible, + color = if (color.isSpecified) color else PlaceholderDefaults.color(), + shape = shape ?: HedvigTheme.shapes.cornerSmall, + highlight = highlight, + placeholderFadeAnimationSpec = placeholderFadeAnimationSpec, + contentFadeAnimationSpec = contentFadeAnimationSpec, + ) +} + +@HedvigPreview +@Composable +private fun PlaceholderPreview() { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column { + HedvigButton( + enabled = true, + text = "Button", + onClick = {}, + modifier = Modifier + .padding(bottom = 16.dp, start = 16.dp, end = 16.dp) + .fillMaxWidth() + .hedvigPlaceholder( + visible = true, + highlight = PlaceholderHighlight.fade(), + ), + ) + HedvigButton( + enabled = true, + text = "Button", + onClick = {}, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp) + .hedvigPlaceholder( + visible = true, + highlight = PlaceholderHighlight.shimmer(), + ), + ) + } + } + } + } +} diff --git a/app/feature/feature-help-center/build.gradle.kts b/app/feature/feature-help-center/build.gradle.kts index 953ddf8a76..8906cccd16 100644 --- a/app/feature/feature-help-center/build.gradle.kts +++ b/app/feature/feature-help-center/build.gradle.kts @@ -42,7 +42,6 @@ dependencies { implementation(projects.navigationCompose) implementation(projects.navigationComposeTyped) implementation(projects.navigationCore) - implementation(projects.placeholder) implementation(projects.uiEmergency) testImplementation(libs.apollo.testingSupport) diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt index 0601349f95..dede65af69 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt @@ -92,6 +92,9 @@ import com.hedvig.android.design.system.hedvig.clearFocusOnTap import com.hedvig.android.design.system.hedvig.icon.Close import com.hedvig.android.design.system.hedvig.icon.HedvigIcons import com.hedvig.android.design.system.hedvig.icon.Search +import com.hedvig.android.design.system.hedvig.placeholder.PlaceholderHighlight +import com.hedvig.android.design.system.hedvig.placeholder.fade +import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder import com.hedvig.android.design.system.hedvig.plus import com.hedvig.android.feature.help.center.HelpCenterEvent import com.hedvig.android.feature.help.center.HelpCenterUiState @@ -106,9 +109,6 @@ import com.hedvig.android.feature.help.center.model.Topic import com.hedvig.android.feature.help.center.ui.HelpCenterSection import com.hedvig.android.feature.help.center.ui.HelpCenterSectionWithClickableRows import com.hedvig.android.feature.help.center.ui.StillNeedHelpSection -import com.hedvig.android.placeholder.PlaceholderHighlight -import com.hedvig.android.placeholder.fade -import com.hedvig.android.placeholder.placeholder import hedvig.resources.R @Composable @@ -712,7 +712,7 @@ private fun PlaceholderQuickLinks() { HedvigText( text = "HHHHHH", modifier = Modifier - .placeholder(visible = true, highlight = PlaceholderHighlight.fade()), + .hedvigPlaceholder(visible = true, highlight = PlaceholderHighlight.fade()), ) }, bottomText = { @@ -720,7 +720,7 @@ private fun PlaceholderQuickLinks() { text = "HHHHHHHHHHHHHHHHHH", style = HedvigTheme.typography.label, modifier = Modifier - .placeholder(true, highlight = PlaceholderHighlight.fade()), + .hedvigPlaceholder(true, highlight = PlaceholderHighlight.fade()), ) }, ) diff --git a/app/feature/feature-insurances/build.gradle.kts b/app/feature/feature-insurances/build.gradle.kts index b49a51ac52..cdd2b17905 100644 --- a/app/feature/feature-insurances/build.gradle.kts +++ b/app/feature/feature-insurances/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { implementation(projects.navigationComposeTyped) implementation(projects.navigationCore) implementation(projects.notificationBadgeDataPublic) - implementation(projects.placeholder) implementation(projects.pullrefresh) implementation(projects.uiEmergency) diff --git a/app/feature/feature-profile/build.gradle.kts b/app/feature/feature-profile/build.gradle.kts index ff9feb0e3d..825db2679d 100644 --- a/app/feature/feature-profile/build.gradle.kts +++ b/app/feature/feature-profile/build.gradle.kts @@ -52,7 +52,6 @@ dependencies { implementation(projects.navigationComposeTyped) implementation(projects.navigationCore) implementation(projects.notificationPermission) - implementation(projects.placeholder) implementation(projects.pullrefresh) implementation(projects.theme) diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt index 729fd9f661..acbae72316 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt @@ -59,13 +59,13 @@ import com.hedvig.android.design.system.hedvig.icon.InfoFilled import com.hedvig.android.design.system.hedvig.icon.InfoOutline import com.hedvig.android.design.system.hedvig.icon.MultipleDocuments import com.hedvig.android.design.system.hedvig.icon.Settings +import com.hedvig.android.design.system.hedvig.placeholder.PlaceholderHighlight +import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder +import com.hedvig.android.design.system.hedvig.placeholder.shimmer import com.hedvig.android.design.system.hedvig.plus import com.hedvig.android.memberreminders.ui.MemberReminderCards import com.hedvig.android.notification.permission.NotificationPermissionDialog import com.hedvig.android.notification.permission.rememberNotificationPermissionState -import com.hedvig.android.placeholder.PlaceholderHighlight -import com.hedvig.android.placeholder.placeholder -import com.hedvig.android.placeholder.shimmer import com.hedvig.android.pullrefresh.PullRefreshDefaults import com.hedvig.android.pullrefresh.PullRefreshIndicator import com.hedvig.android.pullrefresh.pullRefresh @@ -350,7 +350,7 @@ private fun ProfileRow( contentDescription = null, modifier = Modifier .size(24.dp) - .placeholder( + .hedvigPlaceholder( isLoading, highlight = PlaceholderHighlight.shimmer(), ), @@ -359,7 +359,7 @@ private fun ProfileRow( HedvigText( text = title, modifier = Modifier - .placeholder( + .hedvigPlaceholder( isLoading, highlight = PlaceholderHighlight.shimmer(), ), diff --git a/app/shared/forever-ui/build.gradle.kts b/app/shared/forever-ui/build.gradle.kts index 2822d082b6..3aa6580df7 100644 --- a/app/shared/forever-ui/build.gradle.kts +++ b/app/shared/forever-ui/build.gradle.kts @@ -31,5 +31,4 @@ dependencies { implementation(projects.moleculePublic) implementation(projects.pullrefresh) implementation(projects.composeUi) - implementation(projects.placeholder) } diff --git a/app/shared/forever-ui/src/main/kotlin/com/hedvig/android/shared/foreverui/ui/ui/ForeverDestination.kt b/app/shared/forever-ui/src/main/kotlin/com/hedvig/android/shared/foreverui/ui/ui/ForeverDestination.kt index 9a9154c491..e73f862ed3 100644 --- a/app/shared/forever-ui/src/main/kotlin/com/hedvig/android/shared/foreverui/ui/ui/ForeverDestination.kt +++ b/app/shared/forever-ui/src/main/kotlin/com/hedvig/android/shared/foreverui/ui/ui/ForeverDestination.kt @@ -72,10 +72,10 @@ import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.icon.Copy import com.hedvig.android.design.system.hedvig.icon.HedvigIcons import com.hedvig.android.design.system.hedvig.icon.InfoOutline +import com.hedvig.android.design.system.hedvig.placeholder.PlaceholderHighlight +import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder +import com.hedvig.android.design.system.hedvig.placeholder.shimmer import com.hedvig.android.language.LanguageService -import com.hedvig.android.placeholder.PlaceholderHighlight -import com.hedvig.android.placeholder.placeholder -import com.hedvig.android.placeholder.shimmer import com.hedvig.android.pullrefresh.PullRefreshDefaults import com.hedvig.android.pullrefresh.PullRefreshIndicator import com.hedvig.android.pullrefresh.PullRefreshState @@ -240,7 +240,7 @@ internal fun LoadingForeverContent() { .wrapContentWidth(Alignment.CenterHorizontally) .clip(CircleShape) .size(215.dp) - .placeholder( + .hedvigPlaceholder( visible = true, highlight = PlaceholderHighlight.shimmer(), ), diff --git a/app/ui/cross-sells/build.gradle.kts b/app/ui/cross-sells/build.gradle.kts index 78481e564f..76f9be8a59 100644 --- a/app/ui/cross-sells/build.gradle.kts +++ b/app/ui/cross-sells/build.gradle.kts @@ -16,5 +16,4 @@ dependencies { implementation(projects.coreUiData) implementation(projects.dataContractAndroid) implementation(projects.designSystemHedvig) - implementation(projects.placeholder) } diff --git a/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt b/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt index 14f835dbed..ed09dfb368 100644 --- a/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt +++ b/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt @@ -37,10 +37,10 @@ import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigText import com.hedvig.android.design.system.hedvig.HedvigTheme import com.hedvig.android.design.system.hedvig.Surface -import com.hedvig.android.placeholder.PlaceholderHighlight -import com.hedvig.android.placeholder.fade -import com.hedvig.android.placeholder.placeholder -import com.hedvig.android.placeholder.shimmer +import com.hedvig.android.design.system.hedvig.placeholder.PlaceholderHighlight +import com.hedvig.android.design.system.hedvig.placeholder.fade +import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder +import com.hedvig.android.design.system.hedvig.placeholder.shimmer import hedvig.resources.R @Composable @@ -121,7 +121,7 @@ private fun CrossSellItem( contentDescription = null, modifier = Modifier .size(48.dp) - .placeholder( + .hedvigPlaceholder( visible = isLoading, highlight = PlaceholderHighlight.fade(), shape = HedvigTheme.shapes.cornerLarge, @@ -135,14 +135,14 @@ private fun CrossSellItem( HedvigText( text = crossSellTitle, style = HedvigTheme.typography.bodySmall, - modifier = Modifier.placeholder(visible = isLoading, highlight = PlaceholderHighlight.shimmer()), + modifier = Modifier.hedvigPlaceholder(visible = isLoading, highlight = PlaceholderHighlight.shimmer()), ) Spacer(Modifier.height(4.dp)) HedvigText( text = crossSellSubtitle, style = HedvigTheme.typography.label, color = HedvigTheme.colorScheme.textSecondary, - modifier = Modifier.placeholder(visible = isLoading, highlight = PlaceholderHighlight.shimmer()), + modifier = Modifier.hedvigPlaceholder(visible = isLoading, highlight = PlaceholderHighlight.shimmer()), ) } Spacer(Modifier.width(16.dp)) @@ -153,7 +153,7 @@ private fun CrossSellItem( }, buttonSize = Small, buttonStyle = PrimaryAlt, - modifier = Modifier.placeholder( + modifier = Modifier.hedvigPlaceholder( visible = isLoading, highlight = PlaceholderHighlight.shimmer(), shape = HedvigTheme.shapes.cornerXLarge,