-
-
Save lelandrichardson/35b2743e1acd5d672f963f92aca57d4a to your computer and use it in GitHub Desktop.
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
/* | |
Disclaimer: I haven't tried running any of this, and wrote it outside of an IDE, so | |
there might be some errors. | |
I have put two implementations here. One is closer to your original one, which uses | |
Canvas. This is arguably the simpler and more straightforward implementation. | |
I additionally added a modifier-based implementation, which would allow you to apply | |
this to arbirtry modifier chains and might be useful in some ways. I don't think the | |
performance of these should be any different | |
Notes from original implementation: | |
https://gist.github.com/karandeep26/7cfd576c9fc2f537abd017dd9035172c | |
1. All of your composable functions are implemented as methods on the activity. | |
At the moment, this means that none of them are "skippable". This is probably not | |
hurting you in this particular example, but it could in normal use. This is going | |
to go away as a perf consideration soon as we will track whether or not you are using | |
<this> inside of the function body and treat the function as a pure function of | |
its inputs if you are not. | |
2. You're allocating a Paint object in each shimmer, and even worse, you're creating a | |
new one every time the shimmer gets recomposed. Paint objects are jni types and very | |
costly to allocate. We are trying to get our public APIs to have almost no paints at all. | |
*/ | |
// Usage | |
// ================ | |
@Composable | |
fun Skeleton() { | |
Column() { | |
Row { | |
Shimmer(Modifier.width(20.dp).preferredHeight(20.dp)) | |
Spacer(Modifier.width(20.dp)) | |
Column() { | |
Shimmer(Modifier.fillMaxWidth().preferredHeight(20.dp)) | |
Spacer(Modifier.height(20.dp)) | |
Shimmer(Modifier.fillMaxWidth().preferredHeight(20.dp)) | |
Spacer(Modifier.height(20.dp)) | |
Shimmer(Modifier.fillMaxWidth().preferredHeight(20.dp)) | |
} | |
} | |
Spacer(modifier = Modifier.height(50.dp)) | |
} | |
} | |
/* | |
Notes for my implementation(s): | |
1. I am using AnimatedFloat to animate the progress, which should yield more predictable | |
performance. | |
2. I am avoiding allocating a Paint on each recomposition, but I'm not avoiding the allocation | |
in general since I don't think we can yet with LinearGradient. | |
3. You could try to share the same animatedFloat and Paint across all instances of the shimmer. | |
I didn't do that in this implementation though. | |
*/ | |
// Composable implementation | |
// ====================== | |
@Composable | |
fun Shimmer(modifier: Modifier) { | |
val paint = remember { Paint().apply { | |
isAntiAlias = true | |
style = PaintingStyle.fill | |
color = "#efefef".color | |
} } | |
val t = animatedFloat(0f) | |
onActive { | |
t.loop(from = 0f, to = 1f, duration = 1000) | |
onDispose { | |
t.stop() | |
} | |
} | |
Canvas(modifier) { | |
paint.shader = LinearGradientShader( | |
size.topLeft, | |
size.bottomRight, | |
shaderColors, | |
listOf(0f, t.value, 1f) | |
) | |
drawRect( | |
rect = size, | |
paint = paint | |
) | |
} | |
} | |
// modifier-based implementation: | |
// ================== | |
fun Modifier.shimmer() = composed { | |
val progress = animatedFloat(0f) | |
onActive { | |
progress.loop(0f, 1f, 1000) | |
onDispose { | |
progress.stop() | |
} | |
} | |
remember { ShimmerModifier(progress) } | |
} | |
private class ShimmerModifier(val t: AnimatedFloat) : DrawModifier { | |
private val shaderColors = listOf( | |
"#AAAAAA".color, | |
"#a2AAAAAA".color, | |
"#AAAAAA".color | |
) | |
private val paint = Paint().apply { | |
isAntiAlias = true | |
style = PaintingStyle.fill | |
color = "#efefef".color | |
} | |
override fun ContentDrawScope.draw() { | |
paint.shader = LinearGradientShader( | |
size.topLeft, | |
size.bottomRight, | |
shaderColors, | |
listOf(0f, t.value, 1f) | |
) | |
drawRect( | |
rect = size, | |
paint = paint | |
) | |
} | |
} | |
@Composable | |
fun Shimmer(modifier: Modifier) { | |
Box(modifier.shimmer()) | |
} | |
// Utilities: | |
// ===================== | |
// no reason to do this as an extension function other than I think it looks prettier :) | |
private val String.color: Color | |
get() = Color(android.graphics.Color.parseColor(value)) | |
// this just loops a duration animation forever. | |
fun AnimatedFloat.loop(from: Flaot, to: Float, durationMs: Int) { | |
fun singleLoop() { | |
snapTo(from) | |
animateTo( | |
to, | |
TweenBuilder<Float>.apply { duration = durationMs }, | |
onEnd = { reason, _ -> | |
if (reason == AnimationEndReason.TargetReached) singleLoop() | |
} | |
) | |
} | |
singleLoop() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment