Skip to content

Instantly share code, notes, and snippets.

@lelandrichardson
Created May 2, 2020 20:49
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lelandrichardson/35b2743e1acd5d672f963f92aca57d4a to your computer and use it in GitHub Desktop.
Save lelandrichardson/35b2743e1acd5d672f963f92aca57d4a to your computer and use it in GitHub Desktop.
/*
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