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

Jetpack Compose animations playground | DevFest...

Jetpack Compose animations playground | DevFest Pescara 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

November 19, 2023
Tweet

More Decks by Daniele Favaro

Other Decks in Programming

Transcript

  1. () -> Unit Modifier .graphicsLayer { ... } .constrainAs {

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

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

    ) { Box( Modifier.graphicsLayer { rotationZ = animatedDegree } ) } Modifier.graphicsLayer { rotationZ = animatedDegree() }
  4. @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 )
  5. 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() } }
  6. 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) } } }
  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. @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 ) {
  9. } 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 ) } } } }
  10. 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(…) } } }
  11. 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)
  12. 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)
  13. 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 ) }
  14. 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 } }
  15. @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 } }
  16. internal fun getSectorWeight( rangeStart: Int, rangeEnd: Int, scoreMax: Int, scoreMin:

    Int ): Float = (rangeEnd - rangeStart).toFloat() / (scoreMax - scoreMin) 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 } }
  17. 0 100 50 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 } } internal fun getSectorWeight( rangeStart: Int, rangeEnd: Int, scoreMax: Int, scoreMin: Int ): Float = (rangeEnd - rangeStart).toFloat() / (scoreMax - scoreMin)
  18. 0 100 50 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 } } internal fun getSectorWeight( rangeStart: Int, rangeEnd: Int, scoreMax: Int, scoreMin: Int ): Float = (rangeEnd - rangeStart).toFloat() / (scoreMax - scoreMin)
  19. 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 } } 0 100 50 internal fun getSectorWeight( rangeStart: Int, rangeEnd: Int, scoreMax: Int, scoreMin: Int ): Float = (rangeEnd - rangeStart).toFloat() / (scoreMax - scoreMin)
  20. 0 100 50 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 } } internal fun getSectorWeight( rangeStart: Int, rangeEnd: Int, scoreMax: Int, scoreMin: Int ): Float = (rangeEnd - rangeStart).toFloat() / (scoreMax - scoreMin)
  21. 0 100 50 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 } } internal fun getSectorWeight( rangeStart: Int, rangeEnd: Int, scoreMax: Int, scoreMin: Int ): Float = (rangeEnd - rangeStart).toFloat() / (scoreMax - scoreMin)
  22. @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()
  23. 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 } }
  24. .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) } ) {
  25. 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() } }
  26. @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() } }
  27. 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() } }
  28. 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() } }
  29. @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() } }
  30. @Composable fun RatingCompose var currentState by remember { mutableStateOf( RatingTransitionState.Initial

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

    .graphicsLayer { rotationZ = animatedDegree }, onDraw = { ... } ) Text(animatedScore) }
  32. @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) }
  33. 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
  34. 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
  35. ‘test and run’ takeaways • test with real devices •

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

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