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
}
)
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)
)