Touch Feedback Animation like Spotify in Jetpack Compose

Build a Bouncing Click Effect in Jetpack Compose

ilyas ipek
wereprotein

--

Bouncing click effect

It’s essential to show the user click feedback bla bla bal, Long story short I like Spotify's bouncing click effect more than the default Ripple effect so let’s do it 🤩

Along the way, we will explore some modifiers and animation too…

It feels like a real button right? :)

To do it we will create a reusable Modifier (similar to the clickable modifier) that decreases the scale and opacity when the UI element is pressed.
I will go ahead and put all the code and then explain it…

/**
* Adds a bouncing click effect to a Composable.
*
* @param enabled whether the UI element should be enabled and clickable
* @param onClick the action to perform when the UI element is clicked
*/
fun Modifier.bouncingClickable(
enabled: Boolean = true,
onClick: () -> Unit
) = composed {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

val animationTransition = updateTransition(isPressed, label = "BouncingClickableTransition")
val scaleFactor by animationTransition.animateFloat(
targetValueByState = { pressed -> if (pressed) 0.94f else 1f },
label = "BouncingClickableScaleFactorTransition",
)
val opacity by animationTransition.animateFloat(
targetValueByState = { pressed -> if (pressed) 0.7f else 1f },
label = "BouncingClickableOpacityTransition"
)

this
.graphicsLayer {
this.scaleX = scaleFactor
this.scaleY = scaleFactor
this.alpha = opacity
}
.clickable(
interactionSource = interactionSource,
indication = null,
enabled = enabled,
onClick = onClick
)
}
  1. We’re using an interactionSource to detect if the composable is pressed using collectIsPressedAsState() function.
  2. Then since we have multiple animations that we want to run when isPressed == true, we use updateTransition() that provides us with extension functions to run multiple animations.
  3. To create the animations, we use animateFloat() extension to create two animations, one for scaleFactor and one for opacity that we will use to animate our UI element.
  4. To change the scale & alpha we used graphicsLayer Modifier which is usually used to apply transformations and animations to a UI element. (check out this great article to learn more)
  5. Finally, we add a clickable modifier to handle click and remove the default ripple effect by setting indication param to null

Note: You can play with the scaleFactor & opacity values to make it more suitable for your use-case.

That’s it, let’s use it 🤩

Box(
modifier = Modifier
.bouncingClickable {
println("Clicked...")
}
.size(100.dp)
.clip(CircleShape)
.background(Color.Black)
) {}

And in case you wanna make it more bouncy, we can add a spring animation spec to our scaleFactor

val scaleFactor by animationTransition.animateFloat(
targetValueByState = { pressed -> if (pressed) 0.94f else 1f },
label = "BouncingClickableScaleFactorTransition",
transitionSpec = {
spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
}
)

It doesn’t look very different in the gif but believe me it looks better :)

That’s it, I hope you loved the blog ❤️

See ya

--

--

Android developer @teknasyon, writes about Android development and productivity.