✨ Subscribe for component library

Add Shake Animations to your Composable

Make your Composables dance

Add Shake Animations to your Composable

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!

Mastodon