1) Principles of Animation
- What property am I animating?
- When should this property change?
- How should this property animate?
What to animate
Most animations are made up of changing different properties or values of a composable.
- Scale
- Translation
- Rotation
- Alpha
- Color
Break down the animation
Analyse which properties of what composables are changing over time.
When to animate
- On State Change
- On Launch
- Infinitely
- With Gesture
On State Change
- Store and manage the value change over time for you.
- Typically one-shot state changes.
animateColorAsState()
animateDpAsState()
animateFloatAsState()
animateSizeAsState()
animateRectAsState()
animate*AsState()
var clicked by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
targetValue = if (clicked) {
IntOffset(pxToMove, pxToMove)
} else {
IntOffset.Zero
},
label = "offset"
)
Box(Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.offset {
offset
}
.background(Color.Blue)
.size(100.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
clicked = !clicked
}
)
}
Transition
- Coordinated state updates that’ll happen at the same time, based on a state.
- Not independent of each other — happen at the same time.
SeekableTransition
— allows for seeking between states.
var targetState by remember { mutableStateOf(false) }
val transition = updateTransition(targetState, label = "transition")
val pxToMove = with(LocalDensity.current) {
100.dp.toPx().roundToInt()
}
val scale by transition.animateFloat (label = "scale") { state ->
when (state) {
false -> 1f
true -> 3f
}
}
val rotate by transition.animateFloat(label = "rotate") { state ->
when (state) {
false -> 0f
true -> 45f
}
}
val offset by transition.animateIntOffset(label = "offset") { state ->
when (state) {
false -> IntOffset.Zero
true -> IntOffset(pxToMove, pxToMove)
}
}
Box(Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
translationX = offset.x.toFloat()
translationY = offset.y.toFloat()
rotationZ = rotate
}
.background(Color.Blue)
.size(100.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
targetState = !targetState
}
)
}
On Launch
// Start out gray and animate to green/red based on `open`
val color = remember { Animatable(Color.Gray) }
var clicked by remember { mutableStateOf(false) }
LaunchedEffect(clicked) {
color.animateTo(if (clicked) Color.Green else Color.Red)
}
Box(Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.background(color.value)
.size(100.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
clicked = !clicked
}
)
}
Infinitely
val infiniteTransition = rememberInfiniteTransition(label = "InfiniteTransition")
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 8f,
animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
label = "FloatAnimation"
)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Android TPE",
modifier = Modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
transformOrigin = TransformOrigin.Center
},
style = LocalTextStyle.current.copy(
textMotion = TextMotion.Animated
)
)
}
With Gesture
- Interruptions of ongoing animation when touch begins.
- Continue animation after gesture is finished.
— Either decaying or snapping to a certain position.
— Needs to feel like a natural stop.
Drag gesture handling
fun Modifier.animateDraggable(
translation: Animatable<Float, AnimationVector1D>,
draggableState: DraggableState,
decay: DecayAnimationSpec<Float>,
screenWidth: () -> Float,
onUpdateBoxState: (BoxState) -> Unit,
) = this
.graphicsLayer {
translationX = translation.value
val scale = lerp(1f, 0.8f, translation.value / screenWidth())
scaleX = if (translationX > 0) scale else 1 / scale
scaleY = if (translationX > 0) scale else 1 / scale
}
.draggable(
state = draggableState,
orientation = Orientation.Horizontal,
onDragStopped = { velocity ->
val targetX = decay.calculateTargetValue(
translation.value,
velocity
)
launch {
val boundaryX =
if (targetX > screenWidth() * 0.5) screenWidth()
else if (targetX < screenWidth() * -0.5) -screenWidth()
else 0f
val canReachBoundaryWithTarget = (targetX > boundaryX && boundaryX == screenWidth())
|| (targetX < boundaryX && boundaryX == -screenWidth())
if (canReachBoundaryWithTarget) {
translation.animateDecay(
initialVelocity = velocity,
animationSpec = decay
)
} else {
translation.animateTo(boundaryX, initialVelocity = velocity)
}
onUpdateBoxState(if (boundaryX == screenWidth()) BoxState.Open else BoxState.Closed)
}
}
)
How to animate
Animation Specs
- Each animation is controlled in its execution by an
AnimationSpec
. - Stores info about the type (
Int
,Float
) and configuration. tween()
spring()
// Default for all Compose animationssnap()
keyframes()
repeatable()
tween()
Animating between two values. Useful when you want to set a duration in milliseconds for how long your animation should take.
var clicked by remember { mutableStateOf(false) }
val xOffset = remember {
Animatable(0f)
}
LaunchedEffect(clicked) {
xOffset.animateTo(
targetValue = if (clicked) 200f else 0f,
animationSpec = tween(
durationMillis = 300,
easing = LinearEasing
)
)
}
Box(Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.graphicsLayer {
translationX = xOffset.value
}
.background(Color.Blue)
.size(100.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
clicked = !clicked
}
)
}
Easing
- Rate of chang of your animation over time.
- Easing is an interface that takes a fraction
[0, 1]
and produces another value.
@Stable
fun interface Easing {
fun transform(fraction: Float): Float
}
LinearEasing
f(n) = n
- Doesn’t accelerate or decelerate, constant speed.
val xOffset = remember {
Animatable(0f)
}
LaunchedEffect(Unit) {
xOffset.animateTo(
targetValue = 200f,
animationSpec = tween(
durationMillis = 300,
easing = LinearEasing
)
)
}
- Start:
xOffset = 0
- Midpoint:
xOffset = 100f
- End:
XOffset = 200f
CubicBezierEasing
- Follows a bezier curve.
- Provide it with two control points (p1, p2).
val EaseInOut: Easing = CubicBezierEasing(0.42f, 0.0f, 0.58f, 1.0f)
spring()
Animate with physics-based properties. It is more natural than tween — as it calculates the velocity based on the stiffness and damping ratio.
tween vs spring
Spring animations: Configuration
- dampingRatio: how bouncy the spring should be, relative to its mass.
- stiffness: how fast the spring should move towards end.
2) Built-in Animation APIs
AnimatedVisibility
var visible by remember {
mutableStateOf(true)
}
AnimatedVisibility(visible) {
// Composable
}
animateContentSize()
var expanded by remember {
mutableStateOf(false)
}
Box(
modifier = Modifier
.background(Color.Blue)
.animateContentSize()
.height(if (expanded) 400.dp else 200.dp)
.fillMaxWidth()
) {
}
AnimatedContent
Animate changes between different Composables.
- Customize X order of animation.
- Customize transition between the different states.
@Composable
fun SearchContext(
tabSelected: KimojiScreen
) {
AnimatedContent(
targetState = tabSelected,
label = "AnimatedContent"
) { targetState ->
when (targetState) {
KimojiScreen.Add -> {}
KimojiScreen.Gallery -> {}
KimojiScreen.Diary -> {}
}
}
}
Enter & Exit Transitions
- Customise how an element enters the screen, and exits.
slideIntoContainer
,slideOutOfContainer
: slides the incoming/outgoing content based on the direction.
@Composable
fun SearchContext(
tabSelected: KimojiScreen
) {
AnimatedContent(
targetState = tabSelected,
label = "AnimatedContent",
transitionSpec = {
slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Up,
animationSpec = tween(durationMillis = 300, easing = EaseIn),
) with slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.Down,
animationSpec = tween(durationMillis = 300, easing = EaseOut),
)
}
) { targetState ->
when (targetState) {
KimojiScreen.Add -> {}
KimojiScreen.Gallery -> {}
KimojiScreen.Diary -> {}
}
}
}
3) Choosing an API
Most animations can be made with Animatable
or animate()
function.
Summary
- Analyse designs with three principles: what, when and how
- Gesture driven animations are typically made up of dragging on screen and flinging after the fact.
- Use built-in components to make animations easier.
- Use decision tree diagram to help pick an API.