$30 off During Our Annual Pro Sale. View Details »

Jetpack Compose animations playground | DevFest Pescara 2023 🇮🇹

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.

Daniele Favaro

November 19, 2023
Tweet

Other Decks in Programming

Transcript

  1. November 2023
    Jetpack Compose

    animations playground
    Daniele Favaro

    Android Dev @

    GDG Android Stockholm

    View Slide

  2. Jetpack Compose
    quick overview

    View Slide

  3. View Slide

  4. Composition lifecycle
    enter the

    composition
    exit the

    composition
    @Composable
    input data output UI

    View Slide

  5. Composition lifecycle
    enter the

    composition
    exit the

    composition
    @Composable
    recomposition

    0:N times
    input data output UI

    View Slide

  6. composition layout draw

    View Slide

  7. composition
    layout draw
    .myPage
    .myImage .myText

    View Slide

  8. composition layout
    draw
    .myPage
    .myImage .myText
    .myPage
    .myText
    .myImage

    View Slide

  9. composition layout draw
    .myPage
    .myImage .myText
    .myPage
    .myText
    .myImage
    Sample text

    View Slide

  10. composition layout draw

    View Slide

  11. () -> Unit

    View Slide

  12. () -> Unit
    Modifier
    .graphicsLayer { ... }
    .constrainAs { ... }
    .drawBehind { ... }
    .cool stuff ...

    View Slide

  13. Modifier.graphicsLayer(
    rotationZ = animatedDegree
    )
    () -> Unit
    Modifier.graphicsLayer {
    rotationZ = animatedDegree
    }

    View Slide

  14. () -> Unit
    Modifier.graphicsLayer {
    rotationZ = animatedDegree
    }
    Modifier.graphicsLayer(
    rotationZ = animatedDegree
    )

    View Slide

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

    View Slide

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

    View Slide

  17. () -> Unit

    View Slide

  18. Today’s

    desired

    outcome

    View Slide

  19. View Slide

  20. animateInt
    AnimatedVisibility
    animateDp
    animateColor

    View Slide

  21. Choose your Animation
    developer.android.com/jetpack/compose/

    animation/resources

    View Slide

  22. View Slide

  23. View Slide

  24. View Slide

  25. View Slide

  26. View Slide

  27. View Slide

  28. View Slide

  29. View Slide

  30. View Slide

  31. developer.android.com/jetpack/compose/

    animation/resources

    View Slide

  32. View Slide

  33. View Slide

  34. @Composable
    fun RatingCompose
    @Composable
    internal fun RatingHeaderCompose
    @Composable
    internal fun RatingBarCompose

    View Slide

  35. @Composable
    internal fun RatingHeaderCompose

    View Slide

  36. @Composable
    internal fun RatingHeaderCompose(
    animationModel: AnimationModel,
    sectorList: List,
    scoreMin: Int,
    scoreMax: Int,
    transition: Transition
    ) {
    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
    )

    View Slide

  37. transition: Transition
    ) {
    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()
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  40. @Composable
    internal fun RatingHeaderCompose

    View Slide

  41. @Composable
    fun RatingCompose
    @Composable
    internal fun RatingHeaderCompose
    @Composable
    internal fun RatingBarCompose

    View Slide

  42. @Composable
    internal fun RatingBarCompose

    View Slide

  43. @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
    ) {

    View Slide

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

    View Slide

  45. Row(modifier = Modifier.fillMaxWidth().padding(top = defaultMarginS)) {
    scoreRanges.forEachIndexed { index, range ->
    // labels
    }
    }

    View Slide

  46. 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(…)
    }
    }
    }

    View Slide

  47. 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)

    View Slide

  48. 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)

    View Slide

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

    View Slide

  50. 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
    }
    }

    View Slide

  51. @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
    }
    }

    View Slide

  52. 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
    }
    }

    View Slide

  53. 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)

    View Slide

  54. 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)

    View Slide

  55. 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)

    View Slide

  56. 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)

    View Slide

  57. 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)

    View Slide

  58. @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()

    View Slide

  59. 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
    }
    }

    View Slide

  60. .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)
    }
    ) {

    View Slide

  61. 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()
    }
    }

    View Slide

  62. @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()
    }
    }

    View Slide

  63. 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()
    }
    }

    View Slide

  64. 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()
    }
    }

    View Slide

  65. @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()
    }
    }

    View Slide

  66. @Composable
    fun RatingCompose
    var currentState by remember {
    mutableStateOf(
    RatingTransitionState.Initial
    )
    }
    val transition = updateTransition(
    currentState,
    label = “”
    )
    RatingBarCompose(
    ...,
    transition = transition,
    onRatingBarDraw = {
    currentState = RatingTransitionState.Rated
    }
    )

    View Slide

  67. @Composable
    fun RatingCompose

    View Slide

  68. @Composable
    fun RatingCompose
    @Composable
    internal fun RatingHeaderCompose
    @Composable
    internal fun RatingBarCompose

    View Slide

  69. View Slide

  70. Today’s

    desired

    outcome

    View Slide

  71. View Slide

  72. animateInt
    animateFloat

    View Slide

  73. Choose your Animation
    developer.android.com/
    jetpack/compose/
    animation/resources

    View Slide

  74. View Slide

  75. View Slide

  76. @Composable
    fun RatingBigNumberCompose
    @Composable
    internal fun BigNumberCanvas

    View Slide

  77. @Composable
    internal fun BigNumberCanvas
    Box {
    Canvas(
    Modifier
    .size(size)
    .aspectRatio(1f)
    .graphicsLayer {
    rotationZ = animatedDegree
    },
    onDraw = { ... }
    )
    Text(animatedScore)
    }

    View Slide

  78. @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)
    }

    View Slide

  79. View Slide

  80. Measure
    through tooling
    developer.android.com/jetpack/compose/

    tooling/#debug

    View Slide

  81. Composition Tracing
    Layout Inspector

    View Slide

  82. Layout

    Inspector
    Composition

    Tracing

    View Slide

  83. View Slide

  84. View Slide

  85. View Slide

  86. View Slide

  87. View Slide

  88. Layout

    Inspector
    Composition

    Tracing

    View Slide

  89. Layout

    Inspector
    Composition

    Tracing Time
    Compose:recompose
    android.compose.material.MaterialTheme
    android.compose.runtime.CompositionLocalProvider
    com.example.FooPage
    .MyImage .MyButton
    Untraced code
    debugImplementation(
    “androidx.compose.runtime:runtime-tracing:”
    )

    View Slide

  90. 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

    View Slide

  91. 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

    View Slide

  92. Composition Tracing
    Layout Inspector

    View Slide

  93. Mindset takeaways
    • think simple
    • and combine for the complex

    View Slide

  94. ‘test and run’ takeaways
    • test with real devices
    • and tooling (might need to delay anim)
    • and eventually
    release buildType

    View Slide

  95. Practicalities takeaways
    • avoid plenty of animations all over
    • micro-interactions are cool
    • defer reads by using
    lambda functions

    View Slide

  96. pull request
    have fun
    fork

    View Slide

  97. Jetpack Compose

    animations playground
    November 2023
    Thank you for listening
    !

    View Slide