1+ package org.michaelbel.core.placeholder
2+
3+ import androidx.compose.animation.core.FiniteAnimationSpec
4+ import androidx.compose.animation.core.InfiniteRepeatableSpec
5+ import androidx.compose.animation.core.MutableTransitionState
6+ import androidx.compose.animation.core.RepeatMode
7+ import androidx.compose.animation.core.Transition
8+ import androidx.compose.animation.core.animateFloat
9+ import androidx.compose.animation.core.infiniteRepeatable
10+ import androidx.compose.animation.core.rememberInfiniteTransition
11+ import androidx.compose.animation.core.spring
12+ import androidx.compose.animation.core.tween
13+ import androidx.compose.animation.core.updateTransition
14+ import androidx.compose.runtime.Composable
15+ import androidx.compose.runtime.getValue
16+ import androidx.compose.runtime.mutableFloatStateOf
17+ import androidx.compose.runtime.remember
18+ import androidx.compose.runtime.setValue
19+ import androidx.compose.ui.Modifier
20+ import androidx.compose.ui.composed
21+ import androidx.compose.ui.draw.drawWithContent
22+ import androidx.compose.ui.geometry.Size
23+ import androidx.compose.ui.geometry.toRect
24+ import androidx.compose.ui.graphics.Color
25+ import androidx.compose.ui.graphics.Outline
26+ import androidx.compose.ui.graphics.Paint
27+ import androidx.compose.ui.graphics.RectangleShape
28+ import androidx.compose.ui.graphics.Shape
29+ import androidx.compose.ui.graphics.drawOutline
30+ import androidx.compose.ui.graphics.drawscope.DrawScope
31+ import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
32+ import androidx.compose.ui.node.Ref
33+ import androidx.compose.ui.platform.debugInspectorInfo
34+ import androidx.compose.ui.unit.LayoutDirection
35+
36+ /* *
37+ * Contains default values used by [Modifier.placeholder] and [PlaceholderHighlight].
38+ */
39+ internal object PlaceholderDefaults {
40+ /* *
41+ * The default [InfiniteRepeatableSpec] to use for [fade].
42+ */
43+ val fadeAnimationSpec: InfiniteRepeatableSpec <Float > by lazy {
44+ infiniteRepeatable(
45+ animation = tween(delayMillis = 200 , durationMillis = 600 ),
46+ repeatMode = RepeatMode .Reverse ,
47+ )
48+ }
49+
50+ /* *
51+ * The default [InfiniteRepeatableSpec] to use for [shimmer].
52+ */
53+ val shimmerAnimationSpec: InfiniteRepeatableSpec <Float > by lazy {
54+ infiniteRepeatable(
55+ animation = tween(durationMillis = 1700 , delayMillis = 200 ),
56+ repeatMode = RepeatMode .Restart
57+ )
58+ }
59+ }
60+
61+ /* *
62+ * Draws some skeleton UI which is typically used whilst content is 'loading'.
63+ *
64+ * A version of this modifier which uses appropriate values for Material themed apps is available
65+ * in the 'Placeholder Material' library.
66+ *
67+ * You can provide a [PlaceholderHighlight] which runs an highlight animation on the placeholder.
68+ * The [shimmer] and [fade] implementations are provided for easy usage.
69+ *
70+ * A cross-fade transition will be applied to the content and placeholder UI when the [visible]
71+ * value changes. The transition can be customized via the [contentFadeTransitionSpec] and
72+ * [placeholderFadeTransitionSpec] parameters.
73+ *
74+ * You can find more information on the pattern at the Material Theming
75+ * [Placeholder UI](https://material.io/design/communication/launch-screen.html#placeholder-ui)
76+ * guidelines.
77+ *
78+ * @param visible whether the placeholder should be visible or not.
79+ * @param color the color used to draw the placeholder UI.
80+ * @param shape desired shape of the placeholder. Defaults to [RectangleShape].
81+ * @param highlight optional highlight animation.
82+ * @param placeholderFadeTransitionSpec The transition spec to use when fading the placeholder
83+ * on/off screen. The boolean parameter defined for the transition is [visible].
84+ * @param contentFadeTransitionSpec The transition spec to use when fading the content
85+ * on/off screen. The boolean parameter defined for the transition is [visible].
86+ */
87+ fun Modifier.placeholder (
88+ visible : Boolean ,
89+ color : Color ,
90+ shape : Shape = RectangleShape ,
91+ highlight : PlaceholderHighlight ? = null,
92+ placeholderFadeTransitionSpec : @Composable Transition .Segment <Boolean >.() -> FiniteAnimationSpec <Float > = { spring() },
93+ contentFadeTransitionSpec : @Composable Transition .Segment <Boolean >.() -> FiniteAnimationSpec <Float > = { spring() }
94+ ): Modifier = composed(
95+ inspectorInfo = debugInspectorInfo {
96+ name = " placeholder"
97+ value = visible
98+ properties[" visible" ] = visible
99+ properties[" color" ] = color
100+ properties[" highlight" ] = highlight
101+ properties[" shape" ] = shape
102+ }
103+ ) {
104+ // Values used for caching purposes
105+ val lastSize = remember { Ref <Size >() }
106+ val lastLayoutDirection = remember { Ref <LayoutDirection >() }
107+ val lastOutline = remember { Ref <Outline >() }
108+
109+ // The current highlight animation progress
110+ var highlightProgress: Float by remember { mutableFloatStateOf(0F ) }
111+
112+ // This is our crossfade transition
113+ val transitionState = remember { MutableTransitionState (visible) }.apply {
114+ targetState = visible
115+ }
116+ val transition = updateTransition(transitionState, " placeholder_crossfade" )
117+
118+ val placeholderAlpha by transition.animateFloat(
119+ transitionSpec = placeholderFadeTransitionSpec,
120+ label = " placeholder_fade" ,
121+ targetValueByState = { placeholderVisible -> if (placeholderVisible) 1F else 0F }
122+ )
123+ val contentAlpha by transition.animateFloat(
124+ transitionSpec = contentFadeTransitionSpec,
125+ label = " content_fade" ,
126+ targetValueByState = { placeholderVisible -> if (placeholderVisible) 0F else 1F }
127+ )
128+
129+ // Run the optional animation spec and update the progress if the placeholder is visible
130+ val animationSpec = highlight?.animationSpec
131+ if (animationSpec != null && (visible || placeholderAlpha >= 0.01F )) {
132+ val infiniteTransition = rememberInfiniteTransition(label = " " )
133+ highlightProgress = infiniteTransition.animateFloat(
134+ initialValue = 0F ,
135+ targetValue = 1F ,
136+ animationSpec = animationSpec,
137+ label = " "
138+ ).value
139+ }
140+
141+ val paint = remember { Paint () }
142+ remember(color, shape, highlight) {
143+ drawWithContent {
144+ // Draw the composable content first
145+ if (contentAlpha in 0.01F .. 0.99F ) {
146+ // If the content alpha is between 1% and 99%, draw it in a layer with
147+ // the alpha applied
148+ paint.alpha = contentAlpha
149+ withLayer(paint) {
150+ with (this @drawWithContent) {
151+ drawContent()
152+ }
153+ }
154+ } else if (contentAlpha >= 0.99F ) {
155+ // If the content alpha is > 99%, draw it with no alpha
156+ drawContent()
157+ }
158+
159+ if (placeholderAlpha in 0.01F .. 0.99F ) {
160+ // If the placeholder alpha is between 1% and 99%, draw it in a layer with
161+ // the alpha applied
162+ paint.alpha = placeholderAlpha
163+ withLayer(paint) {
164+ lastOutline.value = drawPlaceholder(
165+ shape = shape,
166+ color = color,
167+ highlight = highlight,
168+ progress = highlightProgress,
169+ lastOutline = lastOutline.value,
170+ lastLayoutDirection = lastLayoutDirection.value,
171+ lastSize = lastSize.value,
172+ )
173+ }
174+ } else if (placeholderAlpha >= 0.99F ) {
175+ // If the placeholder alpha is > 99%, draw it with no alpha
176+ lastOutline.value = drawPlaceholder(
177+ shape = shape,
178+ color = color,
179+ highlight = highlight,
180+ progress = highlightProgress,
181+ lastOutline = lastOutline.value,
182+ lastLayoutDirection = lastLayoutDirection.value,
183+ lastSize = lastSize.value,
184+ )
185+ }
186+
187+ // Keep track of the last size & layout direction
188+ lastSize.value = size
189+ lastLayoutDirection.value = layoutDirection
190+ }
191+ }
192+ }
193+
194+ private fun DrawScope.drawPlaceholder (
195+ shape : Shape ,
196+ color : Color ,
197+ highlight : PlaceholderHighlight ? ,
198+ progress : Float ,
199+ lastOutline : Outline ? ,
200+ lastLayoutDirection : LayoutDirection ? ,
201+ lastSize : Size ? ,
202+ ): Outline ? {
203+ // shortcut to avoid Outline calculation and allocation
204+ if (shape == = RectangleShape ) {
205+ // Draw the initial background color
206+ drawRect(color = color)
207+
208+ if (highlight != null ) {
209+ drawRect(
210+ brush = highlight.brush(progress, size),
211+ alpha = highlight.alpha(progress),
212+ )
213+ }
214+ // We didn't create an outline so return null
215+ return null
216+ }
217+
218+ // Otherwise we need to create an outline from the shape
219+ val outline = lastOutline.takeIf {
220+ size == lastSize && layoutDirection == lastLayoutDirection
221+ } ? : shape.createOutline(size, layoutDirection, this )
222+
223+ // Draw the placeholder color
224+ drawOutline(outline = outline, color = color)
225+
226+ if (highlight != null ) {
227+ drawOutline(
228+ outline = outline,
229+ brush = highlight.brush(progress, size),
230+ alpha = highlight.alpha(progress),
231+ )
232+ }
233+
234+ // Return the outline we used
235+ return outline
236+ }
237+
238+ private inline fun DrawScope.withLayer (
239+ paint : Paint ,
240+ drawBlock : DrawScope .() -> Unit ,
241+ ) = drawIntoCanvas { canvas ->
242+ canvas.saveLayer(size.toRect(), paint)
243+ drawBlock()
244+ canvas.restore()
245+ }
0 commit comments