Customizing Transitions
Litho provides several APIs to customise many aspects of transitions and animate different components together.
Sequence, Stagger, and Parallel sets
Transitions can be composed using the following:
Sequence- all transitions are run in order. Each transition must end before the following one starts.Stagger- all transitions are run in order. There is a fixed delay between starting each transition, and they may overlap.Parallel- all transitions start at the same time but may end at different times.
The following code uses a stagger set to animate three different rectangles.
class StaggerTransitionComponent : KComponent() {
override fun ComponentScope.render(): Component {
val shown = useState { false }
useTransition(
Transition.stagger(
100,
Transition.create(YellowKey).animate(AnimatedProperties.Y),
Transition.create(BlueKey).animate(AnimatedProperties.Y),
Transition.create(MagentaKey).animate(AnimatedProperties.Y),
),
)
return Row(
alignItems = if (shown.value) YogaAlign.FLEX_END else YogaAlign.FLEX_START,
justifyContent = YogaJustify.CENTER,
style = Style.heightPercent(100f).onClick { shown.update { !it } },
) {
child(ColorCell(color = NiceColor.Yellow, style = Style.transitionKey(context, YellowKey)))
child(ColorCell(color = NiceColor.Blue, style = Style.transitionKey(context, BlueKey)))
child(ColorCell(color = NiceColor.Magenta, style = Style.transitionKey(context, MagentaKey)))
}
}
}
The following animation shows this component in action.
Transition sets can be nested within each other, for example, you could have a 'stagger' of 'parallel' animation sets.
Sequences and Staggers support interrupting behaviour, which ensures a component will never jump and will always end up in the correct end position.
The sets can also be used to animate different properties of the same component, as shown in the following example:
class StaggerTransitionSameComponent : KComponent() {
override fun ComponentScope.render(): Component {
val shown = useState { false }
// Animate different properties of the same component with staggered timing
useTransition(
Transition.stagger(
50,
Transition.create(YellowKey).animate(AnimatedProperties.Y),
Transition.create(YellowKey).animate(AnimatedProperties.X),
),
)
return Row(
alignItems = if (shown.value) YogaAlign.FLEX_END else YogaAlign.FLEX_START,
justifyContent = if (shown.value) YogaJustify.FLEX_END else YogaJustify.FLEX_START,
style = Style.heightPercent(100f).onClick { shown.update { !it } },
) {
child(ColorCell(color = NiceColor.Yellow, style = Style.transitionKey(context, YellowKey)))
}
}
}
The following animation shows this component in action.
Animators
Animators affect the rate in which new values are pushed to the animated components. An Animator can be added to any transition with the .animator() builder method.
Litho provides two Animators:
- Spring-based - see the SpringTransitionAnimator
- Timing-based - see the TimingTransitionAnimator
These Animators can be configured to cover most use cases.
It's possible to configure a custom transition by implementing Transition.TransitionAnimator.
By default, all transitions in Litho run by the SpringTransitionAnimator, which is physics based and naturally interruptible. You can tune the parameters of this spring by creating another Animator using Transition.springWithConfig() and pass custom tension and friction.
In addition, the TimingTransitionAnimator enables you to set a total time and an Interpolator. To do this, use the builder Transition.timing() that receives the total time and an optional Interpolator.
The default interpolator for the TimingTransitionAnimator is an AccelerateDecelerateInterpolator.
// Run transitions in parallel with different animators
useTransition(
Transition.parallel(
// Spring animator with custom tension and friction
Transition.create(YellowKey)
.animate(AnimatedProperties.Y)
.animator(Transition.springWithConfig(120.0, 12.0)),
// Timing animator with fixed duration
Transition.create(BlueKey)
.animate(AnimatedProperties.Y)
.animator(Transition.timing(1000)),
// Timing animator with bounce interpolator
Transition.create(MagentaKey)
.animate(AnimatedProperties.Y)
.animator(Transition.timing(1000, BounceInterpolator())),
),
)
The following animation shows this component in action.
TransitionEnd callback
A Listener can be added to receive a callback when an individual transition has ended. Use eventHandler<TransitionEndEvent> to create a handler and pass it to .transitionEndHandler().
The TransitionEndEvent will be called with the transition key and the specific AnimatedProperty that has been animated for that key. If multiple instances of AnimatedProperty are added to the same transition, and all of them run at the same time, a callback will be executed for each. This callback may be useful to loop animations by updating state.
class SequenceTransitionLoopComponent : KComponent() {
override fun ComponentScope.render(): Component {
val shown = useState { false }
val isLooping = useState { false }
// Create an event handler for transition end
val onTransitionEnd =
eventHandler<TransitionEndEvent> { event ->
// When the last animation completes, toggle state to loop
if (isLooping.value) {
shown.update { !it }
}
}
// Animate transitions in sequence with a callback on the last one
useTransition(
Transition.sequence(
Transition.create(YellowKey).animate(AnimatedProperties.Y),
Transition.create(BlueKey).animate(AnimatedProperties.Y),
Transition.create(MagentaKey)
.animate(AnimatedProperties.Y)
.transitionEndHandler(onTransitionEnd),
),
)
return Row(
alignItems = if (shown.value) YogaAlign.FLEX_END else YogaAlign.FLEX_START,
justifyContent = YogaJustify.CENTER,
style =
Style.heightPercent(100f).onClick {
isLooping.update { !it }
shown.update { !it }
},
) {
child(ColorCell(color = NiceColor.Yellow, style = Style.transitionKey(context, YellowKey)))
child(ColorCell(color = NiceColor.Blue, style = Style.transitionKey(context, BlueKey)))
child(ColorCell(color = NiceColor.Magenta, style = Style.transitionKey(context, MagentaKey)))
}
}
}
A transitionEndHandler can also be added to Transition.allLayout() where the same logic applies:
val onTransitionEnd = eventHandler<TransitionEndEvent> { event ->
// Handle transition end
}
useTransition(Transition.allLayout().transitionEndHandler(onTransitionEnd))