androidandroid-jetpack-composeandroid-animationjetpack-compose-animationandroid-jetpack-compose-animation

Android Compose create shake animation


I am trying to make shaking animation of shape in Jetpack Compose. I want to use this animation to show error when user enters invalid Pin code. But all I can find is slide in, slide out animations and some scale animations. Any ideas how I can accomplish this?

Update: After @Thracian answer. I used code as below, shaking my items horizontally:

fun Modifier.shake(enabled: Boolean, onAnimationFinish: () -> Unit) = composed(
    factory = {
        val distance by animateFloatAsState(
            targetValue = if (enabled) 15f else 0f,
            animationSpec = repeatable(
                iterations = 8,
                animation = tween(durationMillis = 50, easing = LinearEasing),
                repeatMode = RepeatMode.Reverse
            ),
            finishedListener = { onAnimationFinish.invoke() }
        )

        Modifier.graphicsLayer {
            translationX = if (enabled) distance else 0f
        }
    },
    inspectorInfo = debugInspectorInfo {
        name = "shake"
        properties["enabled"] = enabled
    }
)

Solution

  • enter image description here

    Gif is slower than actual animation unfortunately but it gives an idea of outcome.

    This can be done in many ways. You should change scaleX or scaleY or both in short time duration to have a shake effect. If you wish to have rotation change rotationZ of Modifier.graphicsLayer either

     @Composable
    private fun ShakeAnimationSamples() {
        Column(modifier = Modifier
            .fillMaxSize()
            .padding(10.dp)) {
    
            var enabled by remember {
                mutableStateOf(false)
            }
            val scale by animateFloatAsState(
                targetValue = if (enabled) .9f else 1f,
                animationSpec = repeatable(
                    iterations = 5,
                    animation = tween(durationMillis = 50, easing = LinearEasing),
                    repeatMode = RepeatMode.Reverse
                ),
                finishedListener = {
                    enabled = false
                }
            )
    
            val infiniteTransition = rememberInfiniteTransition()
            val scaleInfinite by infiniteTransition.animateFloat(
                initialValue = 1f,
                targetValue = .85f,
                animationSpec = infiniteRepeatable(
                    animation = tween(30, easing = LinearEasing),
                    repeatMode = RepeatMode.Reverse
                )
            )
    
            val rotation by infiniteTransition.animateFloat(
                initialValue = -10f,
                targetValue = 10f,
                animationSpec = infiniteRepeatable(
                    animation = tween(30, easing = LinearEasing),
                    repeatMode = RepeatMode.Reverse
                )
            )
    
            Icon(
                imageVector = Icons.Default.NotificationsActive,
                contentDescription = null,
                tint = Color.White,
                modifier = Modifier
                    .graphicsLayer {
                        scaleX = if (enabled) scale else 1f
                        scaleY = if (enabled) scale else 1f
                    }
                    .background(Color.Red, CircleShape)
                    .size(50.dp)
                    .padding(10.dp)
            )
    
            Icon(
                imageVector = Icons.Default.NotificationsActive,
                contentDescription = null,
                tint = Color.White,
                modifier = Modifier
                    .graphicsLayer {
                        scaleX = scaleInfinite
                        scaleY = scaleInfinite
                        rotationZ = rotation
                    }
                    .background(Color.Red, CircleShape)
                    .size(50.dp)
                    .padding(10.dp)
            )
    
    
    
            Button(onClick = { enabled = !enabled }) {
                Text("Animation enabled: $enabled")
            }
    
        }
    }
    

    Also you can do it as a Modifier either

    fun Modifier.shake(enabled: Boolean) = composed(
    
        factory = {
            
            val scale by animateFloatAsState(
                targetValue = if (enabled) .9f else 1f,
                animationSpec = repeatable(
                    iterations = 5,
                    animation = tween(durationMillis = 50, easing = LinearEasing),
                    repeatMode = RepeatMode.Reverse
                )
            )
    
            Modifier.graphicsLayer {
                scaleX = if (enabled) scale else 1f
                scaleY = if (enabled) scale else 1f
            }
        },
        inspectorInfo = debugInspectorInfo {
            name = "shake"
            properties["enabled"] = enabled
        }
    )
    

    Usage

    Icon(
        imageVector = Icons.Default.NotificationsActive,
        contentDescription = null,
        tint = Color.White,
        modifier = Modifier
            .shake(enabled)
            .background(Color.Red, CircleShape)
            .size(50.dp)
            .padding(10.dp)
    )