This sets up a [Transition], and updates it with the target provided by [targetState].
DoubleTapToLikeSample
@Composable
fun DoubleTapToLikeSample() {
// enum class LikedStates { Initial, Liked, Disappeared }
@Composable
fun doubleTapToLike() {
// Creates a transition state that starts in [Disappeared] State
var transitionState by remember {
mutableStateOf(MutableTransitionState(LikedStates.Disappeared))
}
Box(
Modifier.fillMaxSize().pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
// This creates a new `MutableTransitionState` object. When a new
// `MutableTransitionState` object gets passed to `updateTransition`, a
// new transition will be created. All existing values, velocities will
// be lost as a result. Hence, in most cases, this is not recommended.
// The exception is when it's more important to respond immediately to
// user interaction than preserving continuity.
transitionState = MutableTransitionState(LikedStates.Initial)
}
)
}
) {
// This ensures sequential states: Initial -> Liked -> Disappeared
if (transitionState.currentState == LikedStates.Initial) {
transitionState.targetState = LikedStates.Liked
} else if (transitionState.currentState == LikedStates.Liked) {
// currentState will be updated to targetState when the transition is finished, so
// it can be used as a signal to start the next transition.
transitionState.targetState = LikedStates.Disappeared
}
// Creates a transition using the TransitionState object that gets recreated at each
// double tap.
val transition = rememberTransition(transitionState)
// Creates an alpha animation, as a part of the transition.
val alpha by
transition.animateFloat(
transitionSpec = {
when {
// Uses different animation specs for transitioning from/to different
// states
LikedStates.Initial isTransitioningTo LikedStates.Liked ->
keyframes {
durationMillis = 500
0f at 0 // optional
0.5f at 100
1f at 225 // optional
}
LikedStates.Liked isTransitioningTo LikedStates.Disappeared ->
tween(durationMillis = 200)
else -> snap()
}
}
) {
if (it == LikedStates.Liked) 1f else 0f
}
// Creates a scale animation, as a part of the transition
val scale by
transition.animateFloat(
transitionSpec = {
when {
// Uses different animation specs for transitioning from/to different
// states
LikedStates.Initial isTransitioningTo LikedStates.Liked ->
spring(dampingRatio = Spring.DampingRatioHighBouncy)
LikedStates.Liked isTransitioningTo LikedStates.Disappeared ->
tween(200)
else -> snap()
}
}
) {
when (it) {
LikedStates.Initial -> 0f
LikedStates.Liked -> 4f
LikedStates.Disappeared -> 2f
}
}
Icon(
Icons.Filled.Favorite,
"Like",
Modifier.align(Alignment.Center)
.graphicsLayer(alpha = alpha, scaleX = scale, scaleY = scale),
tint = Color.Red,
)
}
}
}
GestureAnimationSample
@Composable
fun GestureAnimationSample() {
// enum class ComponentState { Pressed, Released }
var useRed by remember { mutableStateOf(false) }
var toState by remember { mutableStateOf(ComponentState.Released) }
val modifier =
Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = {
toState = ComponentState.Pressed
tryAwaitRelease()
toState = ComponentState.Released
}
)
}
// Defines a transition of `ComponentState`, and updates the transition when the provided
// [targetState] changes. The transition will run all of the child animations towards the new
// [targetState] in response to the [targetState] change.
val transition: Transition<ComponentState> = updateTransition(targetState = toState)
// Defines a float animation as a child animation the transition. The current animation value
// can be read from the returned State<Float>.
val scale: Float by
transition.animateFloat(
// Defines a transition spec that uses the same low-stiffness spring for *all*
// transitions of this float, no matter what the target is.
transitionSpec = { spring(stiffness = 50f) }
) { state ->
// This code block declares a mapping from state to value.
if (state == ComponentState.Pressed) 3f else 1f
}
// Defines a color animation as a child animation of the transition.
val color: Color by
transition.animateColor(
transitionSpec = {
when {
ComponentState.Pressed isTransitioningTo ComponentState.Released ->
// Uses spring for the transition going from pressed to released
spring(stiffness = 50f)
else ->
// Uses tween for all the other transitions. (In this case there is
// only one other transition. i.e. released -> pressed.)
tween(durationMillis = 500)
}
}
) { state ->
when (state) {
// Similar to the float animation, we need to declare the target values
// for each state. In this code block we can access theme colors.
ComponentState.Pressed -> MaterialTheme.colors.primary
// We can also have the target value depend on other mutableStates,
// such as `useRed` here. Whenever the target value changes, transition
// will automatically animate to the new value even if it has already
// arrived at its target state.
ComponentState.Released -> if (useRed) Color.Red else MaterialTheme.colors.secondary
}
}
Column {
Button(
modifier = Modifier.padding(10.dp).align(Alignment.CenterHorizontally),
onClick = { useRed = !useRed },
) {
Text("Change Color")
}
Box(
modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
.size((100 * scale).dp)
.background(color)
)
}
}
InitialStateSample
@OptIn(ExperimentalTransitionApi::class)
fun InitialStateSample() {
// This composable enters the composition with a custom enter transition. This is achieved by
// defining a different initialState than the first target state using `MutableTransitionState`
@Composable
fun PoppingInCard() {
// Creates a transition state with an initial state where visible = false
val visibleState = remember { MutableTransitionState(false) }
// Sets the target state of the transition state to true. As it's different than the initial
// state, a transition from not visible to visible will be triggered.
visibleState.targetState = true
// Creates a transition with the transition state created above.
val transition = rememberTransition(visibleState)
// Adds a scale animation to the transition to scale the card up when transitioning in.
val scale by
transition.animateFloat(
// Uses a custom spring for the transition.
transitionSpec = { spring(dampingRatio = Spring.DampingRatioMediumBouncy) }
) { visible ->
if (visible) 1f else 0.8f
}
// Adds an elevation animation that animates the dp value of the animation.
val elevation by
transition.animateDp(
// Uses a tween animation
transitionSpec = {
// Uses different animations for when animating from visible to not visible, and
// the other way around
if (false isTransitioningTo true) {
tween(1000)
} else {
spring()
}
}
) { visible ->
if (visible) 10.dp else 0.dp
}
Card(
Modifier.graphicsLayer(scaleX = scale, scaleY = scale)
.size(200.dp, 100.dp)
.fillMaxWidth(),
elevation = elevation,
) {}
}
}