Subscribe for UI recipes
Add Shake Animations to your Composable
Make your Composables dance
Shake animations can add an engaging and dynamic touch in your UI. They are used to identify an element in need of the user's attention.
Jetpack Compose makes this very easy to accomplish with its animation functions. In this article, we will look into how to achieve this plus build a system to easily create shake animations using a custom Modifier
.
And finally, we will learn how to make an interactive animation like this:
Simple shake animation
For this, we will use an Animatable
which we will animate back and forth when a value changes.
@Composable
fun Shaker() {
val shake = remember { Animatable(0f) }
var trigger by remember { mutableStateOf(0L) }
LaunchedEffect(trigger) {
if (trigger != 0L) {
for (i in 0..10) {
when (i % 2) {
0 -> shake.animateTo(5f, spring(stiffness = 100_000f))
else -> shake.animateTo(-5f, spring(stiffness = 100_000f))
}
}
shake.animateTo(0f)
}
}
Box(
modifier = Modifier
.clickable { trigger = System.currentTimeMillis() }
.offset { x = IntOffset(shake.value.roundToInt(), y = 0) }
.padding(horizontal = 24.dp, vertical = 8.dp)
) {
Text(text = "Shake me")
}
}
We create the Animatable
shake
and initialize it to 0. We also create a trigger that is also initialized to 0. A shake animation is started when the trigger
value changes to a non-zero number.
This prevents the shake animation starting at the first composition.
When the trigger value changes to a non-zero number, we animate the shake
for 10 times from 5f
to -5f
. After the loop, we reset shake
back to zero.
We use the value from shake
to offset the composable on the x-axis.
Finally, we just change the value of trigger
on click to a new unique value, ex. the current time, and a shake animation is triggered.
The end result is this, a button that shakes when clicked.
A custom Modifier
The implementation above works well when animating just one item. But what if we would like to animate multiple items in our app. We don't want to re-write this logic for each and every composable we want to shake.
Also, we could add more configuration so that we can shake more than just along the x-axis
First, we will create a ShakeController
class and a function to create it within composition.
@Composable
fun rememberShakeController(): ShakeController {
return remember { ShakeController() }
}
class ShakeController {
var shakeConfig: ShakeConfig? by mutableStateOf(null)
private set
fun shake(shakeConfig: ShakeConfig) {
this.shakeConfig = shakeConfig
}
}
ShakeController
has a shakeConfig
that defines the parameters of the shake animation, and a shake
function which we can call to trigger a shake.
With this, we can create a ShakeController
like so:
val shakeController = rememberShakeController()
Before looking at how to trigger a shake, let's see how ShakeConfig
is defined.
data class ShakeConfig(
val iterations: Int,
val intensity: Float = 100_000f,
val rotate: Float = 0f,
val rotateX: Float = 0f,
val rotateY: Float = 0f,
val scaleX: Float = 0f,
val scaleY: Float = 0f,
val translateX: Float = 0f,
val translateY: Float = 0f,
val trigger: Long = System.currentTimeMillis(),
)
This is a data class that we can use to define multiple shake animations. We pass in the iterations, intensity and animation values (rotate, scale, etc.). We will use all these values to create animations.
You can animate more than just these values here. This can be extended to cover other animatable properties
Finally we create a custom Modifier
that we can simply pass in a ShakeController
and it applies all the animations based on our shakeConfig
fun Modifier.shake(shakeController: ShakeController) = composed {
shakeController.shakeConfig?.let { shakeConfig ->
val shake = remember { Animatable(0f) }
LaunchedEffect(shakeController.shakeConfig) {
for (i in 0..shakeConfig.iterations) {
when (i % 2) {
0 -> shake.animateTo(1f, spring(stiffness = shakeConfig.intensity))
else -> shake.animateTo(-1f, spring(stiffness = shakeConfig.intensity))
}
}
shake.animateTo(0f)
}
this
.rotate(shake.value * shakeConfig.rotate)
.graphicsLayer {
rotationX = shake.value * shakeConfig.rotateX
rotationY = shake.value * shakeConfig.rotateY
}
.scale(
scaleX = 1f + (shake.value * shakeConfig.scaleX),
scaleY = 1f + (shake.value * shakeConfig.scaleY),
)
.offset {
IntOffset(
(shake.value * shakeConfig.translateX).roundToInt(),
(shake.value * shakeConfig.translateY).roundToInt(),
)
}
} ?: this
}
This simplifies our previous Shaker
by reducing the amount of code to this:
@Composable
fun Shaker() {
val shakeController = rememberShakeController()
Box(
modifier = Modifier
.clickable {
shakeController.shake(ShakeConfig(10, translateX = 5f))
}
.shake(shakeController)
.padding(horizontal = 24.dp, vertical = 8.dp)
) {
Text(text = "Shake me")
}
}
Doing it this way reduces the complexity of creating even more shake animations with more properties being animated.
Log in shake animation
To showcase how we can use this Modifier in our work, let's create a log in animation that shakes for a wrong password and nods for the correct one.
How do we interpret these expressions onto a button?
Let's exercise your necks to find out!
Exercise time!
Wherever you are, try shaking your head side to side for no and nodding up and down for yes. Apart from getting weird looks from others, what did you notice?
The position of your face moves and rotates in 3D space. We shall use this information to model our button's movements after.
We can use the rotate and translate parameters to move the button like a human face.
For a wrong password, we will shake the button side to side with a slight rotation. More accurately, translate it a few pixels along the X-axis and rotate it some degrees around the Y-axis.
The ShakeConfig
would look like this:
ShakeConfig(
iterations = 4,
intensity = 2_000f,
rotateY = 15f,
translateX = 40f,
)
And for the correct password, we will nod the button up and down also with some rotation. This will mean translate along the Y-axis and rotate around the X-axis.
The ShakeConfig
will look like this:
ShakeConfig(
iterations = 4,
intensity = 1_000f,
rotateX = -20f,
translateY = 20f,
)
And with that, we now have an animated button with the swagger of a decapitated head. We just need to add some UI around it to collect a password and a state to keep track of the correct password. Here is the full code:
sealed class LogInState {
object Input : LogInState()
object Wrong : LogInState()
object Correct : LogInState()
}
val red = Color(0xFFDD5D5D)
val green = Color(0xFF79DD5D)
val white = Color(0xFFF7F7F7)
@Composable
fun LoginExample() {
var password by remember { mutableStateOf("") }
var logInState: LogInState by remember { mutableStateOf(LogInState.Input) }
val color: Color by animateColorAsState(
when (logInState) {
LogInState.Correct -> green
LogInState.Input -> white
LogInState.Wrong -> red
}, label = "Button color"
)
val shakeController = rememberShakeController()
TextField(
value = password,
onValueChange = {
logInState = LogInState.Input
password = it
},
isError = logInState == LogInState.Wrong,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
)
Box(modifier = Modifier.height(12.dp))
Box(
modifier = Modifier
.padding(8.dp)
.shake(shakeController = shakeController)
.border(2.dp, color, RoundedCornerShape(5.dp))
.background(color = color.copy(alpha = .1f), shape = RoundedCornerShape(5.dp))
.pointerInput(Unit) {
detectTapGestures {
logInState = when (password) {
"password" -> LogInState.Correct
else -> LogInState.Wrong
}
when (logInState) {
LogInState.Correct -> {
shakeController.shake(
ShakeConfig(
iterations = 4,
intensity = 1_000f,
rotateX = -20f,
translateY = 20f,
)
)
}
LogInState.Wrong -> {
shakeController.shake(
ShakeConfig(
iterations = 4,
intensity = 2_000f,
rotateY = 15f,
translateX = 40f,
)
)
}
LogInState.Input -> {}
}
}
} .clip(RoundedCornerShape(5.dp))
.padding(horizontal = 24.dp, vertical = 8.dp),
contentAlignment = Alignment.Center,
) {
AnimatedContent(
targetState = logInState,
transitionSpec = {
slideInVertically(spring(stiffness = Spring.StiffnessMedium)) { -it } + fadeIn() with
slideOutVertically(spring(stiffness = Spring.StiffnessHigh)) { it } + fadeOut() using SizeTransform(
clip = false
)
},
contentAlignment = Alignment.Center
) { logInState ->
Text(
text = when (logInState) {
LogInState.Correct -> "Success"
LogInState.Input -> "Login"
LogInState.Wrong -> "Try Again"
},
color = Color.White,
fontWeight = FontWeight.Medium,
)
}
}}
And with that, you now know how to shake your composables. Try it yourself and hope you come up with something delightful.
Thanks for reading and good luck!