-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3d478b8
commit 79886ff
Showing
12 changed files
with
595 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
245 changes: 245 additions & 0 deletions
245
core/src/main/kotlin/org/michaelbel/core/placeholder/Placeholder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
package org.michaelbel.core.placeholder | ||
|
||
import androidx.compose.animation.core.FiniteAnimationSpec | ||
import androidx.compose.animation.core.InfiniteRepeatableSpec | ||
import androidx.compose.animation.core.MutableTransitionState | ||
import androidx.compose.animation.core.RepeatMode | ||
import androidx.compose.animation.core.Transition | ||
import androidx.compose.animation.core.animateFloat | ||
import androidx.compose.animation.core.infiniteRepeatable | ||
import androidx.compose.animation.core.rememberInfiniteTransition | ||
import androidx.compose.animation.core.spring | ||
import androidx.compose.animation.core.tween | ||
import androidx.compose.animation.core.updateTransition | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableFloatStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.setValue | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.composed | ||
import androidx.compose.ui.draw.drawWithContent | ||
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.DrawScope | ||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas | ||
import androidx.compose.ui.node.Ref | ||
import androidx.compose.ui.platform.debugInspectorInfo | ||
import androidx.compose.ui.unit.LayoutDirection | ||
|
||
/** | ||
* Contains default values used by [Modifier.placeholder] and [PlaceholderHighlight]. | ||
*/ | ||
internal object PlaceholderDefaults { | ||
/** | ||
* The default [InfiniteRepeatableSpec] to use for [fade]. | ||
*/ | ||
val fadeAnimationSpec: InfiniteRepeatableSpec<Float> by lazy { | ||
infiniteRepeatable( | ||
animation = tween(delayMillis = 200, durationMillis = 600), | ||
repeatMode = RepeatMode.Reverse, | ||
) | ||
} | ||
|
||
/** | ||
* The default [InfiniteRepeatableSpec] to use for [shimmer]. | ||
*/ | ||
val shimmerAnimationSpec: InfiniteRepeatableSpec<Float> by lazy { | ||
infiniteRepeatable( | ||
animation = tween(durationMillis = 1700, delayMillis = 200), | ||
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 [contentFadeTransitionSpec] and | ||
* [placeholderFadeTransitionSpec] 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. | ||
* | ||
* @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 placeholderFadeTransitionSpec The transition spec to use when fading the placeholder | ||
* on/off screen. The boolean parameter defined for the transition is [visible]. | ||
* @param contentFadeTransitionSpec The transition spec to use when fading the content | ||
* on/off screen. The boolean parameter defined for the transition is [visible]. | ||
*/ | ||
fun Modifier.placeholder( | ||
visible: Boolean, | ||
color: Color, | ||
shape: Shape = RectangleShape, | ||
highlight: PlaceholderHighlight? = null, | ||
placeholderFadeTransitionSpec: @Composable Transition.Segment<Boolean>.() -> FiniteAnimationSpec<Float> = { spring() }, | ||
contentFadeTransitionSpec: @Composable Transition.Segment<Boolean>.() -> FiniteAnimationSpec<Float> = { spring() } | ||
): Modifier = composed( | ||
inspectorInfo = debugInspectorInfo { | ||
name = "placeholder" | ||
value = visible | ||
properties["visible"] = visible | ||
properties["color"] = color | ||
properties["highlight"] = highlight | ||
properties["shape"] = shape | ||
} | ||
) { | ||
// Values used for caching purposes | ||
val lastSize = remember { Ref<Size>() } | ||
val lastLayoutDirection = remember { Ref<LayoutDirection>() } | ||
val lastOutline = remember { Ref<Outline>() } | ||
|
||
// The current highlight animation progress | ||
var highlightProgress: Float by remember { mutableFloatStateOf(0F) } | ||
|
||
// This is our crossfade transition | ||
val transitionState = remember { MutableTransitionState(visible) }.apply { | ||
targetState = visible | ||
} | ||
val transition = updateTransition(transitionState, "placeholder_crossfade") | ||
|
||
val placeholderAlpha by transition.animateFloat( | ||
transitionSpec = placeholderFadeTransitionSpec, | ||
label = "placeholder_fade", | ||
targetValueByState = { placeholderVisible -> if (placeholderVisible) 1F else 0F } | ||
) | ||
val contentAlpha by transition.animateFloat( | ||
transitionSpec = contentFadeTransitionSpec, | ||
label = "content_fade", | ||
targetValueByState = { placeholderVisible -> if (placeholderVisible) 0F else 1F } | ||
) | ||
|
||
// Run the optional animation spec and update the progress if the placeholder is visible | ||
val animationSpec = highlight?.animationSpec | ||
if (animationSpec != null && (visible || placeholderAlpha >= 0.01F)) { | ||
val infiniteTransition = rememberInfiniteTransition(label = "") | ||
highlightProgress = infiniteTransition.animateFloat( | ||
initialValue = 0F, | ||
targetValue = 1F, | ||
animationSpec = animationSpec, | ||
label = "" | ||
).value | ||
} | ||
|
||
val paint = remember { Paint() } | ||
remember(color, shape, highlight) { | ||
drawWithContent { | ||
// 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) { | ||
with(this@drawWithContent) { | ||
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.value = drawPlaceholder( | ||
shape = shape, | ||
color = color, | ||
highlight = highlight, | ||
progress = highlightProgress, | ||
lastOutline = lastOutline.value, | ||
lastLayoutDirection = lastLayoutDirection.value, | ||
lastSize = lastSize.value, | ||
) | ||
} | ||
} else if (placeholderAlpha >= 0.99F) { | ||
// If the placeholder alpha is > 99%, draw it with no alpha | ||
lastOutline.value = drawPlaceholder( | ||
shape = shape, | ||
color = color, | ||
highlight = highlight, | ||
progress = highlightProgress, | ||
lastOutline = lastOutline.value, | ||
lastLayoutDirection = lastLayoutDirection.value, | ||
lastSize = lastSize.value, | ||
) | ||
} | ||
|
||
// Keep track of the last size & layout direction | ||
lastSize.value = size | ||
lastLayoutDirection.value = 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() | ||
} |
Oops, something went wrong.