Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Jetpack Compose animations playground | DevFest...

Jetpack Compose animations playground | DevFest Ireland 2023 🇮🇪

Animations make our apps nicer! Let's see how easy it is to use them. In this talk, you will have an overview of how to orchestrate multiple animation states, different ways of triggering them, and measure your app performance.

Avatar for Daniele Favaro

Daniele Favaro

December 04, 2023
Tweet

More Decks by Daniele Favaro

Other Decks in Programming

Transcript

  1. Jetpack Compose animations playground Daniele Favaro Android Dev @ GDG

    Android Stockholm December 2023 DevFest Ireland 2023
  2. () -> Unit Modifier .graphicsLayer { ... } .constrainAs {

    ... } .drawBehind { ... } .cool stuff ...
  3. () -> Unit @Composable fun SampleWheel( animatedDegree: Float ) {

    Box( Modifier.graphicsLayer { rotationZ = animatedDegree } ) } Modifier.graphicsLayer { rotationZ = animatedDegree }
  4. () -> Unit @Composable fun SampleWheel( animatedDegree: () -> Float

    ) { Box( Modifier.graphicsLayer { rotationZ = animatedDegree } ) } Modifier.graphicsLayer { rotationZ = animatedDegree() }
  5. @Composable internal fun RatingHeaderCompose( animationModel: AnimationModel, sectorList: List<SectorModel>, scoreMin: Int,

    scoreMax: Int, transition: Transition<RatingTransitionState> ) { val context = LocalContext.current val scoreLabel: String by remember { derivedStateOf { var label: String = context.getString(R.string.rating_not_applicable) // check if our target is in any of the input sectors ... label } } val animatedScore: Int by transition.animateInt( label = "", transitionSpec = { tween( durationMillis = animationModel.animDuration, delayMillis = animationModel.animDelay, easing = FastOutSlowInEasing )
  6. transition: Transition<RatingTransitionState> ) { val context = LocalContext.current val scoreLabel:

    String by remember { derivedStateOf { var label: String = context.getString(R.string.rating_not_applicable) // check if our target is in any of the input sectors ... label } } val animatedScore: Int by transition.animateInt( label = "", transitionSpec = { tween( durationMillis = animationModel.animDuration, delayMillis = animationModel.animDelay, easing = FastOutSlowInEasing ) } ) { state -> when (state) { RatingTransitionState.Initial -> 0 RatingTransitionState.Rated -> animationModel.targetValue.toInt() } }
  7. transitionSpec = { tween( durationMillis = animationModel.animDuration, delayMillis = animationModel.animDelay,

    easing = FastOutSlowInEasing ) } ) { state -> when (state) { RatingTransitionState.Initial -> 0 RatingTransitionState.Rated -> animationModel.targetValue.toInt() } } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Bottom ) { Text(text = "$animatedScore") AnimatedVisibility( visible = transition.currentState == transition.targetState, // signal for animation end enter = slideInVertically { height -> -height } + fadeIn() ) { Text(text = scoreLabel) } } }
  8. transitionSpec = { tween( durationMillis = animationModel.animDuration, delayMillis = animationModel.animDelay,

    easing = FastOutSlowInEasing ) } ) { state -> when (state) { RatingTransitionState.Initial -> 0 RatingTransitionState.Rated -> animationModel.targetValue.toInt() } } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Bottom ) { Text(text = "$animatedScore") AnimatedVisibility( visible = transition.currentState == transition.targetState, // signal for animation end enter = slideInVertically { height -> -height } + fadeIn() ) { Text(text = scoreLabel) } } }
  9. @Composable internal fun RatingBarCompose ... ConstraintLayout(modifier = Modifier.fillMaxWidth()) { val

    (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHeight) .constrainAs(ratingBar) { top.linkTo(index.top) bottom.linkTo(index.bottom) } ) { scoreRanges.forEach { range -> Spacer(…) } } } Row(modifier = Modifier.fillMaxWidth().padding(top = defaultMarginS)) { scoreRanges.forEachIndexed { index, range -> Row( modifier = Modifier.weight(...), horizontalArrangement = if (index == 0) Arrangement.SpaceBetween else Arrangement.End ) {
  10. } Row(modifier = Modifier.fillMaxWidth().padding(top = defaultMarginS)) { scoreRanges.forEachIndexed { index,

    range -> Row( modifier = Modifier.weight(...), horizontalArrangement = if (index == 0) Arrangement.SpaceBetween else Arrangement.End ) { if (index == 0) { Text( text = scoreMinLabel, color = MaterialTheme.colorScheme.onBackground ) } if (index == scoreRanges.lastIndex) { Text( text = scoreMaxLabel, color = MaterialTheme.colorScheme.onBackground ) } else { Text( text = range.end.toString(), color = MaterialTheme.colorScheme.onBackground ) } } } }
  11. } Row(modifier = Modifier.fillMaxWidth().padding(top = defaultMarginS)) { scoreRanges.forEachIndexed { index,

    range -> Row( modifier = Modifier.weight(...), horizontalArrangement = if (index == 0) Arrangement.SpaceBetween else Arrangement.End ) { if (index == 0) { Text( text = scoreMinLabel, color = MaterialTheme.colorScheme.onBackground ) } if (index == scoreRanges.lastIndex) { Text( text = scoreMaxLabel, color = MaterialTheme.colorScheme.onBackground ) } else { Text( text = range.end.toString(), color = MaterialTheme.colorScheme.onBackground ) } } } } } Row(modifier = Modifier.fillMaxWidth().padding(top = defaultMarginS)) { scoreRanges.forEachIndexed { index, range -> Row(modifier = Modifier.fillMaxWidth().padding(top = defaultMarginS)) { scoreRanges.forEachIndexed { index, range ->
  12. Row(modifier = Modifier .fillMaxWidth() .padding(top = defaultMarginS) ) { scoreRanges.forEachIndexed

    { index, range -> // labels } @Composable internal fun RatingBarCompose ... ConstraintLayout(modifier = Modifier.fillMaxWidth()) { val (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHeight) .constrainAs(ratingBar) { top.linkTo(index.top) bottom.linkTo(index.bottom) } ) { scoreRanges.forEach { range -> Spacer(…) } } }
  13. BulletCompose( modifier = Modifier .size(indicatorRadius * 2) .border(indicatorBorderWidth, RatingBarBorderColor, CircleShape)

    .constrainAs(index) { top.linkTo(parent.top) translationX = animatedScoreDp }, backgroundColor = animatedBulletColor ) val (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHeight) .constrainAs(ratingBar) { top.linkTo(index.top) bottom.linkTo(index.bottom) } ) { scoreRanges.forEach { range -> Spacer(…) } } } Row(modifier = Modifier .fillMaxWidth() .padding(top = defaultMarginS)
  14. BulletCompose( modifier = Modifier .size(indicatorRadius * 2) .border(indicatorBorderWidth, RatingBarBorderColor, CircleShape)

    .constrainAs(index) { top.linkTo(parent.top) translationX = animatedScoreDp }, backgroundColor = animatedBulletColor ) val (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHeight) .constrainAs(ratingBar) { top.linkTo(index.top) bottom.linkTo(index.bottom) } ) { scoreRanges.forEach { range -> Spacer(…) } } } Row(modifier = Modifier .fillMaxWidth() .padding(top = defaultMarginS)
  15. val animatedScoreDp: Dp by transition.animateDp( label = "", transitionSpec =

    { tween( durationMillis = animDuration, delayMillis = animDelay, easing = FastOutSlowInEasing ) } ) { state -> when (state) { RatingTransitionState.Initial -> 0 RatingTransitionState.Rated -> scoreTransitionDp } } @Composable internal fun RatingBarCompose ... ConstraintLayout(modifier = Modifier.fil ... BulletCompose( modifier = Modifier .size(indicatorRadius * 2) .border(indicatorBorderWidth, Rati CircleShape) .constrainAs(index) { top.linkTo(parent.top) translationX = animatedScoreDp }, backgroundColor = animatedBulletCol ) }
  16. val scoreTransitionDp: Dp by remember { derivedStateOf { ratingBarWidthDpState *

    getSectorWeight( rangeStart = scoreMin, rangeEnd = scoreState, // rating scoreMax = scoreMax, scoreMin = scoreMin ) } } @Composable internal fun RatingBarCompose ... ConstraintLayout(modifier = Modifier.fil ... BulletCompose( modifier = Modifier .size(indicatorRadius * 2) .border(indicatorBorderWidth, Rati CircleShape) .constrainAs(index) { top.linkTo(parent.top) translationX = animatedScoreDp }, backgroundColor = animatedBulletCol ) } val animatedScoreDp: Dp by transition.animateDp( ... ) { state -> when (state) { RatingTransitionState.Initial -> 0 RatingTransitionState.Rated -> scoreTransitionDp } }
  17. @Composable internal fun RatingBarCompose ... ConstraintLayout(modifier = Modifier.fil ... BulletCompose(

    modifier = Modifier .size(indicatorRadius * 2) .border(indicatorBorderWidth, Rati CircleShape) .constrainAs(index) { top.linkTo(parent.top) translationX = animatedScoreDp }, backgroundColor = animatedBulletCol ) } val scoreTransitionDp: Dp by remember { derivedStateOf { ratingBarWidthDpState * getSectorWeight( rangeStart = scoreMin, rangeEnd = scoreState, // rating scoreMax = scoreMax, scoreMin = scoreMin ) } } val animatedScoreDp: Dp by transition.animateDp( ... ) { state -> when (state) { RatingTransitionState.Initial -> 0 RatingTransitionState.Rated -> scoreTransitionDp } }
  18. @Composable internal fun RatingBarCompose ... val scoreTransitionDp: Dp by remember

    { derivedStateOf { ratingBarWidthDpState * getSectorWeight( rangeStart = scoreMin, rangeEnd = scoreState, // rating scoreMax = scoreMax, scoreMin = scoreMin ) } } val animatedScoreDp: Dp by transition.animateDp( ... ) { state -> when (state) { RatingTransitionState.Initial -> 0 RatingTransitionState.Rated -> scoreTransitionDp } } ) { val (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHe .constrainAs(ratingBar) { top.linkTo(index.top) bottom.linkTo(index.bottom) } ) { scoreRanges.forEach { range -> Spacer(…) } } BulletCompose( modifier = Modifier .size(indicatorRadius * 2) .border(indicatorBorderWidth, ConstraintLayout(modifier = Modifier .fillMaxWidth()
  19. var ratingBarWidthDpState: Dp by remember mutableStateOf(0.dp) } ) { val

    (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHe .constrainAs(ratingBar) { top.linkTo(index.top) bottom.linkTo(index.bottom) } ) { scoreRanges.forEach { range -> Spacer(…) } } BulletCompose( @Composable internal fun RatingBarCompose ... ConstraintLayout(modifier = Modifier .fillMaxWidth() val scoreTransitionDp: Dp by remember { derivedStateOf { ratingBarWidthDpState * getSectorWeight( rangeStart = scoreMin, rangeEnd = scoreState, // rating scoreMax = scoreMax, scoreMin = scoreMin ) } } val animatedScoreDp: Dp by transition.animateDp( ... ) { state -> when (state) { RatingTransitionState.Initial -> 0 RatingTransitionState.Rated -> scoreTransitionDp } }
  20. .onGloballyPositioned { with(localDensity) { ratingBarWidthDpState = it.size.width. onRatingBarDraw.invoke() } }

    val scoreTransitionDp: Dp by remember { derivedStateOf { ratingBarWidthDpState * getSectorWeight( rangeStart = scoreMin, rangeEnd = scoreState, // rating scoreMax = scoreMax, scoreMin = scoreMin ) } } val animatedScoreDp: Dp by transition.animateDp( ... ) { state -> when (state) { RatingTransitionState.Initial -> 0 RatingTransitionState.Rated -> scoreTransitionDp } } var ratingBarWidthDpState: Dp by remember mutableStateOf(0.dp) } @Composable internal fun RatingBarCompose ... ConstraintLayout(modifier = Modifier .fillMaxWidth() ) { val (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHe .constrainAs(ratingBar) { top.linkTo(index.top) bottom.linkTo(index.bottom) } ) {
  21. val scoreTransitionDp: Dp by remember { derivedStateOf { ratingBarWidthDpState *

    getSectorWeight( rangeStart = scoreMin, rangeEnd = scoreState, // rating scoreMax = scoreMax, scoreMin = scoreMin ) } } val animatedScoreDp: Dp by transition.animateDp( ... ) { state -> when (state) { RatingTransitionState.Initial -> 0 RatingTransitionState.Rated -> scoreTransitionDp } } var ratingBarWidthDpState: Dp by remember mutableStateOf(0.dp) } @Composable internal fun RatingBarCompose ... ConstraintLayout(modifier = Modifier .fillMaxWidth() ) { val (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHe .constrainAs(ratingBar) { top.linkTo(index.top) bottom.linkTo(index.bottom) } ) { .onGloballyPositioned { with(localDensity) { ratingBarWidthDpState = it.size.width. onRatingBarDraw.invoke() } }
  22. @Composable internal fun RatingBarCompose ..., onRatingBarDraw: () -> Unit )

    { var ratingBarWidthDpState: Dp by remember mutableStateOf(0.dp) } ConstraintLayout(modifier = Modifier .fillMaxWidth() ) { val (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHe .constrainAs(ratingBar) { top.linkTo(index.top) val scoreTransitionDp: Dp by remember { derivedStateOf { ratingBarWidthDpState * getSectorWeight( rangeStart = scoreMin, rangeEnd = scoreState, // rating scoreMax = scoreMax, scoreMin = scoreMin ) } } val animatedScoreDp: Dp by transition.animateDp( ... ) { state -> when (state) { RatingTransitionState.Initial -> 0 RatingTransitionState.Rated -> scoreTransitionDp } } .onGloballyPositioned { with(localDensity) { ratingBarWidthDpState = it.size.width. onRatingBarDraw.invoke() } }
  23. RatingBarCompose( ..., transition = transition, onRatingBarDraw = { currentState =

    RatingTransitionState.Rated } ) @Composable internal fun RatingBarCompose ..., onRatingBarDraw: () -> Unit ) { var ratingBarWidthDpState: Dp by remember { mutableStateOf(0.dp) } ConstraintLayout(modifier = Modifier .fillMaxWidth() ) { val (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHeight) .constrainAs(ratingBar) { top.linkTo(index.top) bottom.linkTo(index.bottom) } ) { .onGloballyPositioned { with(localDensity) { ratingBarWidthDpState = it.size.width.toDp() onRatingBarDraw.invoke() } }
  24. var currentState by remember { mutableStateOf( RatingTransitionState.Initial ) } val

    transition = updateTransition( currentState, label = “” ) RatingBarCompose( ..., transition = transition, onRatingBarDraw = { currentState = RatingTransitionState.Rated } ) @Composable internal fun RatingBarCompose ..., onRatingBarDraw: () -> Unit ) { var ratingBarWidthDpState: Dp by remember { mutableStateOf(0.dp) } ConstraintLayout(modifier = Modifier .fillMaxWidth() ) { val (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHeight) .constrainAs(ratingBar) { top.linkTo(index.top) bottom.linkTo(index.bottom) } ) { .onGloballyPositioned { with(localDensity) { ratingBarWidthDpState = it.size.width.toDp() onRatingBarDraw.invoke() } }
  25. @Composable fun RatingCompose @Composable internal fun RatingBarCompose ..., onRatingBarDraw: ()

    -> Unit ) { var ratingBarWidthDpState: Dp by remember { mutableStateOf(0.dp) } ConstraintLayout(modifier = Modifier .fillMaxWidth() ) { val (ratingBar, index) = createRefs() Row( modifier = Modifier.fillMaxWidth().height(ratingBarHeight) .constrainAs(ratingBar) { top.linkTo(index.top) bottom.linkTo(index.bottom) } ) { var currentState by remember { mutableStateOf( RatingTransitionState.Initial ) } val transition = updateTransition( currentState, label = “” ) RatingBarCompose( ..., transition = transition, onRatingBarDraw = { currentState = RatingTransitionState.Rated } ) .onGloballyPositioned { with(localDensity) { ratingBarWidthDpState = it.size.width.toDp() onRatingBarDraw.invoke() } }
  26. @Composable fun RatingCompose var currentState by remember { mutableStateOf( RatingTransitionState.Initial

    ) } val transition = updateTransition( currentState, label = “” ) RatingBarCompose( ..., transition = transition, onRatingBarDraw = { currentState = RatingTransitionState.Rated } )
  27. @Composable internal fun BigNumberCanvas Box { Canvas( Modifier .size(size) .aspectRatio(1f)

    .graphicsLayer { rotationZ = animatedDegree }, onDraw = { ... } ) Text(animatedScore) }
  28. @Composable fun RatingBigNumberCompose var currentState by remember { mutableStateOf( RatingTransitionState.Initial

    ) } val transition = updateTransition( currentState, label = “” ) BigNumberCanvas( ..., transition = transition ) @Composable internal fun BigNumberCanvas Box { Canvas( Modifier .size(size) .aspectRatio(1f) .graphicsLayer { rotationZ = animatedDegree }, onDraw = { ... } ) Text(animatedScore) }
  29. Layout Inspector Composition Tracing trace(“possible cause”) { // heavy operation

    into a composition scope } Time Compose:recompose android.compose.material.MaterialTheme android.compose.runtime.CompositionLocalProvider com.example.FooPage .MyImage .MyButton Untraced code
  30. Layout Inspector Composition Tracing trace(“possible cause”) { // heavy operation

    into a composition scope } Time Compose:recompose android.compose.material.MaterialTheme android.compose.runtime.CompositionLocalProvider com.example.FooPage .MyImage .MyButton possible cause
  31. ‘test and run’ takeaways • test with real devices •

    and tooling (might need to delay anim) • and eventually release buildType
  32. Practicalities takeaways • avoid plenty of animations all over •

    micro-interactions are cool • defer reads by using lambda functions