Animations are fundamental for mobile applications. It gives smooth user experience to end users. Jetpack Compose has various animation API's. Choose your suitable API based on your requirements.
In this tutorial we will learn about following animation APIs,
Animatable
Animate*AsState - AnimateDpAsState - AnimateColorAsState - AnimateFloatAsState
UpdateTransition
InfiniteTransition
1.Animatable
Animatable is a coroutine-based API for animating a single value. We can animate color or float values using this API. It's different from all other animation APIs, because you can use this API outside of your composable function.
Example:
@Composable
private fun AnimatableSample() {
var isAnimated by remember { mutableStateOf(false) }
val color = remember { Animatable(Color.DarkGray) }
// animate to green/red based on `button click`
LaunchedEffect(isAnimated) {
color.animateTo(if (isAnimated) Color.Green else Color.Red, animationSpec = tween(2000))
}
Box(
Modifier
.fillMaxWidth()
.fillMaxHeight(0.8f)
.background(color.value)
)
Button(
onClick = { isAnimated = !isAnimated },
modifier = Modifier.padding(top = 10.dp)
) {
Text(text = "Animate Color")
}
}
Explanation:
In LaunchEffect() we give a condition to change the color based on 'isAnimated' value. When you press the button, 'isAnimated' value gets changed and LaunchEffect() will trigger to change the color.
LaunchedEffect should be used when you want that some action must be taken when composable is first launched
Output:
In the above example, we switch the one color to another. But we need to give the some interval duration for color switching then only we can feel the animation, else it will switch color immediately and you can't notice the animation effects.
These kind of customization achieved by animationSpec.
color.animateTo(if (isAnimated) Color.Green else Color.Red, animationSpec = tween(2000))
Following functions are available to customize your animations:
tween
keyframes
spring
repeatable
infiniteRepeatable
snap
For more details about animationSpec: https://developer.android.com/jetpack/compose/animation#animationspec
2. Animate*AsState
It's used for animating a single value. It can be Dp, Color, Float, Integer, Offset, Rect, Size.
Following functions are available in animate*AsState.
2a. AnimateDpAsState
It returns a State object. The value of the state object will continuously be updated by the animation until the animation finishes.
Note: We can't stop/cancel the animation at runtime in AnimateDpAsState until we remove it from the tree.
@Composable
fun animateDpAsState(
targetValue: Dp,
animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
finishedListener: ((Dp) -> Unit)? = null
)
We will try to change the image size at runtime using animateDpAsState.
Step 1: For code simplify we create a separate function for circle image.
@Composable
fun CircleImage(imageSize: Dp) {
Image(
painter = painterResource(R.drawable.andy_rubin),
contentDescription = "Circle Image",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(imageSize)
.clip(CircleShape)
.border(5.dp, Color.Gray, CircleShape)
)
}
Step 2: AnimateDpAsState sample code:
@Composable
private fun AnimateDpAsState() {
val isNeedExpansion = rememberSaveable{ mutableStateOf(false) }
val animatedSizeDp: Dp by animateDpAsState(targetValue = if (isNeedExpansion.value) 350.dp else 100.dp)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircleImage(animatedSizeDp)
Button(
onClick = {
isNeedExpansion.value = !isNeedExpansion.value
},
modifier = Modifier
.padding(top = 50.dp)
.width(300.dp)
) {
Text(text = "animateDpAsState")
}
}
}
We change the targetValue based on 'isNeedExpansion'. We toggle the 'isNeedExpansion' when button clicked.
Output:
2b. AnimateColorAsState
It exactly same as animateDpAsState(). But the only difference is we give the color as the target value.
@Composable
fun animateColorAsState(
targetValue: Color,
animationSpec: AnimationSpec<Color> = colorDefaultSpring,
finishedListener: ((Color) -> Unit)? = null
)
Example:
@Composable
private fun AnimateColorAsState() {
var isNeedColorChange by remember { mutableStateOf(false) }
val startColor = Color.Blue
val endColor = Color.Green
val backgroundColor by animateColorAsState(
if (isNeedColorChange) endColor else startColor,
animationSpec = tween(
durationMillis = 2000,
delayMillis = 100,
easing = LinearEasing
)
)
Column {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.8f)
.background(backgroundColor)
)
Button(
onClick = { isNeedColorChange = !isNeedColorChange },
modifier = Modifier.padding(top = 10.dp)
) {
Text(text = "Switch Color")
}
}
}
Output:
2c. AnimateFloatAsState
It's also same as animateDpAsState(). But the only difference is we give the float value as the target value.
@Composable
fun animateFloatAsState(
targetValue: Float,
animationSpec: AnimationSpec<Float> = defaultAnimation,
visibilityThreshold: Float = 0.01f,
finishedListener: ((Float) -> Unit)? = null
)
visibilityThreshold - An optional threshold for deciding when the animation value is considered close enough to the targetValue.
Example:
@Composable
private fun AnimateAsFloatContent() {
var isRotated by rememberSaveable { mutableStateOf(false) }
val rotationAngle by animateFloatAsState(
targetValue = if (isRotated) 360F else 0f,
animationSpec = tween(durationMillis = 2500)
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Image(
painter = painterResource(R.drawable.fan),
contentDescription = "fan",
modifier = Modifier
.rotate(rotationAngle)
.size(150.dp)
)
Button(
onClick = { isRotated = !isRotated },
modifier = Modifier
.padding(top = 50.dp)
.width(200.dp)
) {
Text(text = "Rotate Fan")
}
}
}
In this example, we rotate the image angle from 0 to 360 usingfrom 0 to 360 using animateFloatAsState().
Output:
3. UpdateTransition
All the previous animations can run one animation at a time. But updateTransition() can animate one or multiple animations simultaneously.
In the following example, we will try to move the object position + scale the object with the help of updateTransition()
Step 1: Create a state object to trigger the animation state
var isAnimated by remember { mutableStateOf(false) }
Step 2: Define the updateTransition animation
val transition = updateTransition(targetState = isAnimated, label = "transition")
Step 3: Animation1 - Animate the offset (position)
val rocketOffset by transition.animateOffset(transitionSpec = {
if (this.targetState) {
tween(1000) // launch duration
} else {
tween(1500) // land duration
}
}, label = "rocket offset") { animated ->
if (animated)
Offset(200f, 0f)
else Offset(200f, 500f)
}
Step 4: Animation2 - Animate the size
val rocketSize by transition.animateDp(transitionSpec = {
tween(1000)
}, "") { animated ->
if (animated) 75.dp else 150.dp
}
Step 5: Use the Animation1 & Animation2 in your composable(view)
Image(
painter = painterResource(id = R.drawable.rocket),
contentDescription = "Rocket",
modifier = Modifier
.size(rocketSize)
.alpha(1.0f)
.offset(rocketOffset.x.dp, rocketOffset.y.dp)
)
Step 6: Update the animation state to trigger your animation
Button(
onClick = { isAnimated = !isAnimated }
) {
Text(text = if (isAnimated) "Land rocket" else "Launch rocket")
}
Full code:
@Composable
private fun TransitionAnimation() {
var isAnimated by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = isAnimated, label = "transition")
val rocketOffset by transition.animateOffset(transitionSpec = {
if (this.targetState) {
tween(1000) // launch duration
} else {
tween(1500) // land duration
}
}, label = "rocket offset") { animated ->
if (animated) Offset(200f, 0f) else Offset(200f, 500f)
}
val rocketSize by transition.animateDp(transitionSpec = {
tween(1000)
}, "") { animated ->
if (animated) 75.dp else 150.dp
}
Column(
modifier = Modifier
.fillMaxSize()
) {
Image(
painter = painterResource(id = R.drawable.rocket),
contentDescription = "Rocket",
modifier = Modifier
.size(rocketSize)
.alpha(1.0f)
.offset(rocketOffset.x.dp, rocketOffset.y.dp)
)
Button(
onClick = { isAnimated = !isAnimated },
modifier = Modifier.padding(top = 10.dp)
) {
Text(text = if (isAnimated) "Land rocket" else "Launch rocket")
}
}
}
Output
4. InfiniteTransition
It creates a InfiniteTransition that runs infinite child animations. Child animations can be added using InfiniteTransition.animateColor, InfiniteTransition.animateFloat, or InfiniteTransition.animateValue. Child animations will start running as soon as they enter the composition, and will not stop until they are removed from the composition.
We will try InfiniteTransition.animateFloat to animate the heart.
@Composable
fun InfiniteAnimation() {
val infiniteTransition = rememberInfiniteTransition()
val heartSize by infiniteTransition.animateFloat(
initialValue = 100.0f,
targetValue = 250.0f,
animationSpec = infiniteRepeatable(
animation = tween(800, delayMillis = 100,easing = FastOutLinearInEasing),
repeatMode = RepeatMode.Reverse
)
)
Image(
painter = painterResource(R.drawable.heart),
contentDescription = "heart",
modifier = Modifier
.size(heartSize.dp)
)
}
we give the initialValue =100 and targetValue = 250. So it will animate from 100 to 250 values.
RepeatMode:
The animation could either restart after each iteration (i.e. RepeatMode.Restart), or reverse after each iteration (i.e . RepeatMode.Reverse).
Output:
For more details refer the official documentation:
Source code: