Slide 1

Slide 1 text

Dissecting Compose Animation A deep dive into Compose Animation API androiddev.social/@sagar Android at linkedin.com/in/sagarviradiya/ @viradiya_sagar

Slide 2

Slide 2 text

2 Our house of brands

Slide 3

Slide 3 text

3 Agenda Animation - Low level API Compose Animation in Nutshell Animatable animate*AsState Transition 1 2 3 4 5

Slide 4

Slide 4 text

4 Why?

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

6 Compose Animation in a nutshell

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

8 Compose animation in a nutshell 0 1 500 ms Frequency of state change?

Slide 9

Slide 9 text

9 60 hz

Slide 10

Slide 10 text

10 ~16.67 ms

Slide 11

Slide 11 text

11 30 for 500 ms animation

Slide 12

Slide 12 text

12 90 hz

Slide 13

Slide 13 text

13 ~11.11 ms

Slide 14

Slide 14 text

14 46 for 500 ms animation

Slide 15

Slide 15 text

15 30 for 500 ms animation 60 hz 46 120 hz 90 hz 61

Slide 16

Slide 16 text

16 Compose Animation API Hierarchy

Slide 17

Slide 17 text

17 Compose Animation API Hierarchy Animation Animatable Transition animate*AsState

Slide 18

Slide 18 text

18 Animation

Slide 19

Slide 19 text

19 Animation ● Core engine of Compose animation ● Stateless ● Independent of compose

Slide 20

Slide 20 text

20 Animation f(time) Play time Value

Slide 21

Slide 21 text

21 Animation 0 1 500 ms f(250 ms) 0.25

Slide 22

Slide 22 text

22 Animation 0 1 500 ms f(375 ms) 0.75

Slide 23

Slide 23 text

23 Animation interface Animation { . . . //Returns the value of the animation at the given play time fun getValueFromNanos(playTimeNanos: Long): T . . . }

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

25 Animation Implementation TargetBasedAnimation DecayAnimation

Slide 26

Slide 26 text

26 Animation TargetBasedAnimation Play time Value AnimationSpec f(time)

Slide 27

Slide 27 text

27 Animation f( f(time) ) Play time Value

Slide 28

Slide 28 text

28 AnimationSpec interface VectorizedAnimationSpec { . . /** * 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 . . . }

Slide 29

Slide 29 text

29 Built-in Animation Spec ● SpringSpec ● TweenSpec ● RepeatableSpec ● InfiniteRepeatableSpec Many more…

Slide 30

Slide 30 text

30 Animatable

Slide 31

Slide 31 text

31 Animatable ● Stateful ● Coroutine based ● Synchronised with Android’s choreographer to listen for frames ● Wrapper over animation

Slide 32

Slide 32 text

32 Animatable State TargetBasedAnimation FrameListener

Slide 33

Slide 33 text

33 Animatable State Animation FrameListener Listen for frames Time in nano Playtime in nano Value

Slide 34

Slide 34 text

34 Animatable Example // Compose scope val isVisible by remember { mutableStateOf(false) }

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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) }

Slide 37

Slide 37 text

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" )

Slide 38

Slide 38 text

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" )

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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 withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R . . . }

Slide 52

Slide 52 text

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 withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R . . . }

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

65 animate*As State

Slide 66

Slide 66 text

66 animate*AsState ● Triggers an animation on a state change ● Composable wrapping animatable ● Expose animatable state

Slide 67

Slide 67 text

67 animate*AsState Animatable animateTo(target) State

Slide 68

Slide 68 text

68 animate*AsState Example // Compose Context val isVisible by remember { mutableStateOf(false) }

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

70 animate*AsState Example // Compose Context val isVisible by remember { mutableStateOf(false) } val alphaState: State = animateFloatAsState( targetValue = if (isVisible) 1f else 0f, label = "alphaAnimation" ) Image( modifier = Modifier.graphicsLayer { alpha = alphaState.value }, painter = painterResource(id = R.drawable.image), contentDescription = "Image" )

Slide 71

Slide 71 text

71 Transition

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

73 // Compose Context var isVisible by remember { mutableStateOf(false) }

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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 }

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

84 Summary Animatable Animation == fun(fun(time)) animate*AsState Transition 1 2 3 4 Stateful wrapper over Animation Composable wrapping Animatable Composable wrapping Animations

Slide 85

Slide 85 text

85 Questions?