Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Dissecting Compose Animation

Dissecting Compose Animation

Animation plays a crucial role in App UI/UX. If not done correctly it can bite back leading to a bad app experience. A key to good animation is knowing API well and when to use which one.

While it is crucial to know animation APIs on the consumer side it is essential to know the internals of Compose animation to be able to debug or optimize something which requires internal knowledge.

This talk aims to throw some light on the internals of the following APIs


- Low-level animation API - Animation & AnimationSpecs

- Animatable API

- animation*AsState API

- Transition API


At the end, we will learn the internals of Compose Animation which will prepare you to optimize your App UI/UX experience!

Sagar Viradiya

November 12, 2023
Tweet

More Decks by Sagar Viradiya

Other Decks in Programming

Transcript

  1. Dissecting Compose Animation A deep dive into Compose Animation API

    androiddev.social/@sagar Android at linkedin.com/in/sagarviradiya/ @viradiya_sagar
  2. 3 Agenda Animation - Low level API Compose Animation in

    Nutshell Animatable animate*AsState Transition 1 2 3 4 5
  3. 5 Importance of knowing API internals 1. Essentials to implement

    efficient animation 2. Choosing right API for a problem 3. Debugging performance or behaviour 4. Can write custom high level API as a thin wrapper over low level API 5. Depth of knowledge
  4. 7 Compose animation in a nutshell 0 0.06 1 0.12

    0.18 0.25 0.29 0.35 0.45 0.5 0.57 0.6 0.65 0.7 0.95 0.85 0.75 500 ms State<T>
  5. 23 Animation interface Animation<T, V : AnimationVector> { . .

    . //Returns the value of the animation at the given play time fun getValueFromNanos(playTimeNanos: Long): T . . . }
  6. 24 Animation interface Animation<T, V : AnimationVector> { . .

    . //Returns the value of the animation at the given play time fun getValueFromNanos(playTimeNanos: Long): T . . . }
  7. 28 AnimationSpec interface VectorizedAnimationSpec<V : AnimationVector> { . . /**

    * Calculates the value of the animation at given playtime, with * the provided start/end values, and start velocity. */ fun getValueFromNanos( playTimeNanos: Long, initialValue: V, targetValue: V, initialVelocity: V ): V . . . }
  8. 31 Animatable • Stateful • Coroutine based • Synchronised with

    Android’s choreographer to listen for frames • Wrapper over animation
  9. 35 Animatable Example // Compose scope val isVisible by remember

    { mutableStateOf(false) } val alphaAnimatable = remember { Animatable(0f) }
  10. 36 Animatable Example // Compose scope val isVisible by remember

    { mutableStateOf(false) } val alphaAnimatable = remember { Animatable(0f) } LaunchedEffect(isVisible) { // Coroutine Scope alphaAnimatable.animateTo(if (isVisible) 1f else 0f) }
  11. 37 Animatable Example // Compose scope val isVisible by remember

    { mutableStateOf(false) } val alphaAnimatable = remember { Animatable(0f) } LaunchedEffect(isVisible) { // Coroutine Scope alphaAnimatable.animateTo(if (isVisible) 1f else 0f) } Image( modifier = Modifier.graphicsLayer { alpha = alphaAnimatable.value }, painter = painterResource(id = R.drawable.image), contentDescription = "Image" )
  12. 38 Animatable Example // Compose scope val isVisible by remember

    { mutableStateOf(false) } val alphaAnimatable = remember { Animatable(0f) } LaunchedEffect(isVisible) { // Coroutine Scope alphaAnimatable.animateTo(if (isVisible) 1f else 0f) } Image( modifier = Modifier.graphicsLayer { alpha = alphaAnimatable.value }, painter = painterResource(id = R.drawable.image), contentDescription = "Image" )
  13. 39 animateTo 🧐 suspend fun animateTo( targetValue: T, animationSpec: AnimationSpec<T>

    = defaultSpringSpec, initialVelocity: T = velocity, block: (Animatable<T, V>.() -> Unit)? = null ): AnimationResult<T, V> { val anim = TargetBasedAnimation( animationSpec = animationSpec, initialValue = value, targetValue = targetValue, typeConverter = typeConverter, initialVelocity = initialVelocity ) return runAnimation(anim, initialVelocity, block) }
  14. 40 animateTo 🧐 suspend fun animateTo( targetValue: T, animationSpec: AnimationSpec<T>

    = defaultSpringSpec, initialVelocity: T = velocity, block: (Animatable<T, V>.() -> Unit)? = null ): AnimationResult<T, V> { val anim = TargetBasedAnimation( animationSpec = animationSpec, initialValue = value, targetValue = targetValue, typeConverter = typeConverter, initialVelocity = initialVelocity ) return runAnimation(anim, initialVelocity, block) }
  15. 41 animateTo 🧐 suspend fun animateTo( targetValue: T, animationSpec: AnimationSpec<T>

    = defaultSpringSpec, initialVelocity: T = velocity, block: (Animatable<T, V>.() -> Unit)? = null ): AnimationResult<T, V> { val anim = TargetBasedAnimation( animationSpec = animationSpec, initialValue = value, targetValue = targetValue, typeConverter = typeConverter, initialVelocity = initialVelocity ) return runAnimation(anim, initialVelocity, block) }
  16. 42 runAnimation 🧐 . . . while (lateInitScope!!.isRunning) { val

    durationScale = coroutineContext.durationScale animation.callWithFrameNanos { frameTimeInNano -> lateInitScope!!.doAnimationFrameWithScale( frameTimeInNano, durationScale, animation, this, block ) } } . . .
  17. 43 runAnimation 🧐 . . . while (lateInitScope!!.isRunning) { val

    durationScale = coroutineContext.durationScale animation.callWithFrameNanos { frameTimeInNano -> lateInitScope!!.doAnimationFrameWithScale( frameTimeInNano, durationScale, animation, this, block ) } } . . .
  18. 44 runAnimation 🧐 . . . while (lateInitScope!!.isRunning) { val

    durationScale = coroutineContext.durationScale animation.callWithFrameNanos { frameTimeInNano -> lateInitScope!!.doAnimationFrameWithScale( frameTimeInNano, durationScale, animation, this, block ) } } . . .
  19. 45 runAnimation 🧐 . . . while (lateInitScope!!.isRunning) { val

    durationScale = coroutineContext.durationScale animation.callWithFrameNanos { frameTimeInNano -> lateInitScope!!.doAnimationFrameWithScale( frameTimeInNano, durationScale, animation, this, block ) } } . . .
  20. 46 callWithFrameNanos 🧐 private suspend fun callWithFrameNanos( onFrame: (frameTimeNanos: Long)

    -> R ): R { return if (isInfinite) { withInfiniteAnimationFrameNanos(onFrame) } else { withFrameNanos { frameTimeNano -> onFrame.invoke(frameTimeNano / AnimationDebugDurationScale) } } }
  21. 47 callWithFrameNanos 🧐 private suspend fun callWithFrameNanos( onFrame: (frameTimeNanos: Long)

    -> R ): R { return if (isInfinite) { withInfiniteAnimationFrameNanos(onFrame) } else { withFrameNanos { frameTimeNano -> onFrame.invoke(frameTimeNano / AnimationDebugDurationScale) } } }
  22. 48 callWithFrameNanos 🧐 private suspend fun callWithFrameNanos( onFrame: (frameTimeNanos: Long)

    -> R ): R { return if (isInfinite) { withInfiniteAnimationFrameNanos(onFrame) } else { withFrameNanos { frameTimeNano -> onFrame.invoke(frameTimeNano / AnimationDebugDurationScale) } } }
  23. 49 withFrameNanos 🧐 suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) ->

    R): R { return coroutineContext.monotonicFrameClock.withFrameNanos(onFrame) }
  24. 50 withFrameNanos 🧐 suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) ->

    R): R { return coroutineContext.monotonicFrameClock.withFrameNanos(onFrame) }
  25. 51 MonotonicFrameClock 🧐 interface MonotonicFrameClock : CoroutineContext.Element { /** *

    Suspends until a new frame is requested, immediately invokes [onFrame] * with the frame time in nanoseconds in the calling context of frame * dispatch, then resumes with the result from [onFrame]. */ suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R . . . }
  26. 52 MonotonicFrameClock 🧐 interface MonotonicFrameClock : CoroutineContext.Element { /** *

    Suspends until a new frame is requested, immediately invokes [onFrame] * with the frame time in nanoseconds in the calling context of frame * dispatch, then resumes with the result from [onFrame]. */ suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R . . . }
  27. 53 MonotonicFrameClock 🧐 class AndroidUiFrameClock internal constructor( val choreographer: Choreographer,

    private val dispatcher: AndroidUiDispatcher? ) : androidx.compose.runtime.MonotonicFrameClock { override suspend fun <R> withFrameNanos( onFrame: (Long) -> R ): R { . . return suspendCancellableCoroutine { co -> val callback = Choreographer.FrameCallback { frameTimeNanos -> co.resumeWith(runCatching { onFrame(frameTimeNanos) }) } . . } }
  28. 54 MonotonicFrameClock 🧐 class AndroidUiFrameClock internal constructor( val choreographer: Choreographer,

    private val dispatcher: AndroidUiDispatcher? ) : androidx.compose.runtime.MonotonicFrameClock { override suspend fun <R> withFrameNanos( onFrame: (Long) -> R ): R { . . return suspendCancellableCoroutine { co -> val callback = Choreographer.FrameCallback { frameTimeNanos -> co.resumeWith(runCatching { onFrame(frameTimeNanos) }) } . . } }
  29. 55 MonotonicFrameClock 🧐 class AndroidUiFrameClock internal constructor( val choreographer: Choreographer,

    private val dispatcher: AndroidUiDispatcher? ) : androidx.compose.runtime.MonotonicFrameClock { override suspend fun <R> withFrameNanos( onFrame: (Long) -> R ): R { . . return suspendCancellableCoroutine { co -> val callback = Choreographer.FrameCallback { frameTimeNanos -> co.resumeWith(runCatching { onFrame(frameTimeNanos) }) } . . } }
  30. 56 MonotonicFrameClock 🧐 class AndroidUiFrameClock internal constructor( val choreographer: Choreographer,

    private val dispatcher: AndroidUiDispatcher? ) : androidx.compose.runtime.MonotonicFrameClock { override suspend fun <R> withFrameNanos( onFrame: (Long) -> R ): R { . . return suspendCancellableCoroutine { co -> val callback = Choreographer.FrameCallback { frameTimeNanos -> co.resumeWith(runCatching { onFrame(frameTimeNanos) }) } . . } }
  31. 57 runAnimation 🧐 . . . while (lateInitScope!!.isRunning) { val

    durationScale = coroutineContext.durationScale animation.callWithFrameNanos { frameTimeInNano -> lateInitScope!!.doAnimationFrameWithScale( frameTimeInNano, durationScale, animation, this, block ) } } . . .
  32. 58 doAnimationFrameWithScale 🧐 private fun doAnimationFrameWithScale( frameTimeNanos: Long, durationScale: Float,

    anim: Animation<T, V>, state: AnimationState<T, V>, block: AnimationScope<T, V>.() -> Unit ) { val playTimeNanos = if (durationScale == 0f) { anim.durationNanos } else { ((frameTimeNanos - startTimeNanos) / durationScale).toLong() } doAnimationFrame(frameTimeNanos, playTimeNanos, anim, state, block) }
  33. 59 doAnimationFrameWithScale 🧐 private fun doAnimationFrameWithScale( frameTimeNanos: Long, durationScale: Float,

    anim: Animation<T, V>, state: AnimationState<T, V>, block: AnimationScope<T, V>.() -> Unit ) { val playTimeNanos = if (durationScale == 0f) { anim.durationNanos } else { ((frameTimeNanos - startTimeNanos) / durationScale).toLong() } doAnimationFrame(frameTimeNanos, playTimeNanos, anim, state, block) }
  34. 60 doAnimationFrameWithScale 🧐 private fun doAnimationFrameWithScale( frameTimeNanos: Long, durationScale: Float,

    anim: Animation<T, V>, state: AnimationState<T, V>, block: AnimationScope<T, V>.() -> Unit ) { val playTimeNanos = if (durationScale == 0f) { anim.durationNanos } else { ((frameTimeNanos - startTimeNanos) / durationScale).toLong() } doAnimationFrame(frameTimeNanos, playTimeNanos, anim, state, block) }
  35. 61 doAnimationFrame 🧐 private fun doAnimationFrame( frameTimeNanos: Long, playTimeNanos: Long,

    anim: Animation<T, V>, state: AnimationState<T, V>, block: AnimationScope<T, V>.() -> Unit ) { lastFrameTimeNanos = frameTimeNanos value = anim.getValueFromNanos(playTimeNanos) velocityVector = anim.getVelocityVectorFromNanos(playTimeNanos) val isLastFrame = anim.isFinishedFromNanos(playTimeNanos) if (isLastFrame) { finishedTimeNanos = lastFrameTimeNanos isRunning = false } updateState(state) }
  36. 62 doAnimationFrame 🧐 private fun doAnimationFrame( frameTimeNanos: Long, playTimeNanos: Long,

    anim: Animation<T, V>, state: AnimationState<T, V>, block: AnimationScope<T, V>.() -> Unit ) { lastFrameTimeNanos = frameTimeNanos value = anim.getValueFromNanos(playTimeNanos) velocityVector = anim.getVelocityVectorFromNanos(playTimeNanos) val isLastFrame = anim.isFinishedFromNanos(playTimeNanos) if (isLastFrame) { finishedTimeNanos = lastFrameTimeNanos isRunning = false } updateState(state) }
  37. 63 doAnimationFrame 🧐 private fun doAnimationFrame( frameTimeNanos: Long, playTimeNanos: Long,

    anim: Animation<T, V>, state: AnimationState<T, V>, block: AnimationScope<T, V>.() -> Unit ) { lastFrameTimeNanos = frameTimeNanos value = anim.getValueFromNanos(playTimeNanos) velocityVector = anim.getVelocityVectorFromNanos(playTimeNanos) val isLastFrame = anim.isFinishedFromNanos(playTimeNanos) if (isLastFrame) { finishedTimeNanos = lastFrameTimeNanos isRunning = false } updateState(state) }
  38. 64 doAnimationFrame 🧐 private fun doAnimationFrame( frameTimeNanos: Long, playTimeNanos: Long,

    anim: Animation<T, V>, state: AnimationState<T, V>, block: AnimationScope<T, V>.() -> Unit ) { lastFrameTimeNanos = frameTimeNanos value = anim.getValueFromNanos(playTimeNanos) velocityVector = anim.getVelocityVectorFromNanos(playTimeNanos) val isLastFrame = anim.isFinishedFromNanos(playTimeNanos) if (isLastFrame) { finishedTimeNanos = lastFrameTimeNanos isRunning = false } updateState(state) }
  39. 66 animate*AsState • Triggers an animation on a state change

    • Composable wrapping animatable • Expose animatable state
  40. 69 animate*AsState Example // Compose Context val isVisible by remember

    { mutableStateOf(false) } val alphaState: State<Float> = animateFloatAsState( targetValue = if (isVisible) 1f else 0f, label = "alphaAnimation" )
  41. 70 animate*AsState Example // Compose Context val isVisible by remember

    { mutableStateOf(false) } val alphaState: State<Float> = animateFloatAsState( targetValue = if (isVisible) 1f else 0f, label = "alphaAnimation" ) Image( modifier = Modifier.graphicsLayer { alpha = alphaState.value }, painter = painterResource(id = R.drawable.image), contentDescription = "Image" )
  42. 72 Transition • Trigger multiple animations on a state change

    • Composable wrapping a low level animation for each child animation • Stateful with a state for each animation • Synchronised with Android’s choreographer to listen for frames
  43. 74 // Compose Context var isVisible by remember { mutableStateOf(false)

    } Transition val transition = updateTransition( target = isVisible, label = "box state" )
  44. 75 // Compose Context var isVisible by remember { mutableStateOf(false)

    } Transition val transition = updateTransition( target = isVisible, label = "box state" ) val alpha by transition.animateFloat( label = "alpha" ) { isVisible -> if (isVisible) 1f else 0f } State Target : 0
  45. 76 // Compose Context var isVisible by remember { mutableStateOf(false)

    } Transition val transition = updateTransition( target = isVisible, label = "box state" ) val alpha by transition.animateFloat( label = "alpha" ) { isVisible -> if (isVisible) 1f else 0f } val scale by transition.animateFloat( label = "Scale" ) { isVisible -> if (isVisible) 1f else 0f } State Target : 0 State Target : 0
  46. 77 // Compose Context var isVisible by remember { mutableStateOf(false)

    } Transition val transition = updateTransition( target = isVisible, label = "box state" ) val alpha by transition.animateFloat( label = "alpha" ) { isVisible -> if (isVisible) 1f else 0f } val scale by transition.animateFloat( label = "Scale" ) { isVisible -> if (isVisible) 1f else 0f } State Target : 0 State Target : 0
  47. 78 // Compose Context var isVisible by remember { mutableStateOf(false)

    } Transition val transition = updateTransition( target = isVisible, label = "box state" ) val alpha by transition.animateFloat( label = "alpha" ) { isVisible -> if (isVisible) 1f else 0f } val scale by transition.animateFloat( label = "Scale" ) { isVisible -> if (isVisible) 1f else 0f } State Target : 0 State Target : 0
  48. 79 // Compose Context var isVisible by remember { mutableStateOf(false)

    } Transition val transition = updateTransition( target = isVisible, label = "box state" ) State val scale by transition.animateFloat( label = "Scale" ) { isVisible -> if (isVisible) 1f else 0f } State val alpha by transition.animateFloat( label = "alpha" ) { isVisible -> if (isVisible) 1f else 0f } State Target : 1 State Target : 0
  49. 80 // Compose Context var isVisible by remember { mutableStateOf(false)

    } Transition val transition = updateTransition( target = isVisible, label = "box state" ) State State State Target : 1 State Target : 1 val alpha by transition.animateFloat( label = "alpha" ) { isVisible -> if (isVisible) 1f else 0f } val scale by transition.animateFloat( label = "Scale" ) { isVisible -> if (isVisible) 1f else 0f }
  50. 81 // Compose Context var isVisible by remember { mutableStateOf(false)

    } Transition val transition = updateTransition( target = isVisible, label = "box state" ) State State State Target : 1 State Target : 1 val alpha by transition.animateFloat( label = "alpha" ) { isVisible -> if (isVisible) 1f else 0f } val scale by transition.animateFloat( label = "Scale" ) { isVisible -> if (isVisible) 1f else 0f } Listen for frames Time in nano
  51. 82 // Compose Context var isVisible by remember { mutableStateOf(false)

    } Transition val transition = updateTransition( target = isVisible, label = "box state" ) State State State Target : 1 State Target : 1 val alpha by transition.animateFloat( label = "alpha" ) { isVisible -> if (isVisible) 1f else 0f } val scale by transition.animateFloat( label = "Scale" ) { isVisible -> if (isVisible) 1f else 0f } Listen for frames Time in nano
  52. 83 // Compose Context var isVisible by remember { mutableStateOf(false)

    } Transition val transition = updateTransition( target = isVisible, label = "box state" ) State State State Target : 1 Value : anim(time) State Target : 1 Value : anim(time) val alpha by transition.animateFloat( label = "alpha" ) { isVisible -> if (isVisible) 1f else 0f } val scale by transition.animateFloat( label = "Scale" ) { isVisible -> if (isVisible) 1f else 0f } Listen for frames Time in nano
  53. 84 Summary Animatable Animation == fun(fun(time)) animate*AsState Transition 1 2

    3 4 Stateful wrapper over Animation Composable wrapping Animatable Composable wrapping Animations