What would be the proper way to animate a value wi...
# compose
t
What would be the proper way to animate a value without triggering recompositon? Like a ProgressIndicator with the lambda progress to defer readings. Everything works but I'd like to animate large value change to avoid jumps. Reading the progress outside of the canvas to animate the value will trigger recomposition since this happens very frequently I wonder if there's a clever way to handle this.
z
If you read a state in a draw block, it will only invalidate draw when it changes. If you’re drawing the thing yourself this could be an option. If you’re animating another component that takes a value as a parameter to a composable function, then you don’t have another option unless you want to fork the component. But thats not necessarily a problem
t
Hum I have a component that takes a ()-> Float for the progress. And read it in the canvas draw so it works no recomposition at all. But now I want to animate the progress change but inside that component only so not at the source emitter.
1 - use a lambda, avoid passing in to the progress semantics directly as well. I think these APIs are much better now
2 - avoiding even that per frame drawing, if you know how much time is 1 pixel.
t
Yes I use the canvas width, but on Horologist you have a special progress handling at the source for the animation of the value change. So the component does not handle it and it's just delayed reading.
👍 1
e
Same requirement in our DS I implemented the other day
Copy code
@Composable
fun ProgressIndicator(
  progress: () -> Float,
  modifier: Modifier = Modifier,
  progressColor: BackgroundColor = BackgroundColor.Unspecified,
  progressBarColor: BackgroundColor = BackgroundColor.Unspecified,
  animateProgress: Boolean = true,
) {
  val bgs = LocalBackgroundColors.current
  val color = progressColor.takeOrElse { bgs.state.successHighEmphasize }
  val barColor = progressBarColor.takeOrElse { bgs.tertiary }

  val getProgress = remember(progress) { { progress().coerceIn(ProgressRange) } }
  val getValue = if (!animateProgress) getProgress else {
    val animation = remember { AnimationState(getProgress()) }
    LaunchedEffect(getProgress) {
      snapshotFlow(getProgress).collectLatest {
        animation.animateTo(it, ProgressAnimationSpec)
      }
    }
    ({ animation.value })
  }

  Canvas(
    modifier
      .progressSemantics(progress)
      .requiredHeight(4.dp)
      .fillMaxWidth()
      .clip(CircleShape)
  ) {
    drawTrack(barColor, 1f)
    drawTrack(color, getValue())
  }
}
Dont like the snapshot flow impl, but it was either that or take
State<Float>
in my parameters, and I’d rather not. The getProgress could also easily be a
rememberUpdatedState
. Also duplicated
Modifier.progressSemantics
to take a lambda.
1
t
Wow brilliant, I would have never thought of starting the LaunchedEffect there. Thanks.
jetpack compose 1