Skip to content

Instantly share code, notes, and snippets.

@GianpaMX
Created April 7, 2022 12:39
Show Gist options
  • Save GianpaMX/289bd0fde84aa18f8c118d80d524a790 to your computer and use it in GitHub Desktop.
Save GianpaMX/289bd0fde84aa18f8c118d80d524a790 to your computer and use it in GitHub Desktop.
compose animation

Everything became easier when I realized that compose animations are just and effect when the state changes. How do the animate* funcrtions work? they just the value in a small increment everytime they are called. That changes the state of the composable so the compose engine would render another frame of the animation.

container transform animation

Animations like container transform can be break down into small steps, for the following animation:

The steps would be:

  1. Elevate the row
  2. Expand the row
  3. Expand the bottom sheet
  4. Make the corners of the bottom sheet squared
  5. Hide the floating action button
  6. Replace the content

Steps 3-6 run together and they have to start and finish at the same time of 1 and 2.

animations timeline

For steps 1 and 2 I created a transition based on animation state:

private enum class AnimationState {
    NOT_STARTED,  // not elevated nor expanded state
    ELEVATING,    // start elevating when the user has clicked
    EXPANDING,    // start expaning when the elevate animation has finished
    ENDED         // elevated and expanded state
}

The transition is created based on AnimationState

val transitionState = remember { MutableTransitionState(AnimationState.NOT_STARTED) }
val transition = updateTransition(transitionState, label = "transition")

The elevate animation from 0 to 14

val elevation by transition.animateDp(
    label = "elevation",
    transitionSpec = { tween(containerAnimationInMillis / 2) },
    targetValueByState = { if (it.hasStarted()) 14.dp else 0.dp }
)

The expand animation from 56 dp to screen height

val height by transition.animateDp(
    label = "height",
    transitionSpec = { tween(containerAnimationInMillis / 2) },
    targetValueByState = { if (it.hasElevated()) LocalConfiguration.current.screenHeightDp.dp else 56.dp }
)

Both animations have to last the same time as the other four. That's why the transitionSpec is condifured to last half of the total time.

The next four animations animations are a little bit simpler, when the user clicks a row, all of them are fired and configured to last containerAnimationInMillis.

We created a transition as well but this time we only care if the user has click a row or not

val transition = updateTransition(selectedRow != null, label = "transition")

To expad the bottom sheet we change the height from 80% to 100% of the screen height

val height by transition.animateDp(
    label = "height",
    transitionSpec = { tween(containerAnimationInMillis) },
    targetValueByState = { isItSelected ->
        configuration.screenHeightDp.dp * if (isItSelected) 1.0f else 0.8f
    }
)

To make the corners square, we change it from 12 dp to 0 dp

val roundCornerRadio by transition.animateDp(
    label = "roundCorners",
    transitionSpec = { tween(containerAnimationInMillis) },
    targetValueByState = { isItSelected ->
        if (isItSelected) 0.dp else 12.dp
    }
)

To hide the Floating Action Button we use AnimatedVisibility with visible = selectedRow == null as its state

AnimatedVisibility(
    visible = selectedRow == null,
    enter = fadeIn(animationSpec = tween(containerAnimationInMillis)) + scaleIn(animationSpec = tween(containerAnimationInMillis)),
    exit = scaleOut(animationSpec = tween(containerAnimationInMillis)) + fadeOut(animationSpec = tween(containerAnimationInMillis)),

    ) {
    FloatingActionButton(contentColor = Color.White, onClick = { }) {
        Icon(painter = painterResource(R.drawable.ic_add), contentDescription = null)
    }
}

And to replace the content we use Crossfade

Crossfade(targetState = selectedExpenseId, animationSpec = tween(containerAnimationInMillis)) { expenseId ->
    if (expenseId != null) {
        EditExpenseScreen(expenseId)
    } else {
        ViewExpensesScreen()
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment