Say hi to the bounce effect in RecyclerView without headaches

Juan Mengual
androidxx
4 min readFeb 14, 2021

--

Pixel launcher added sometime ago this very nice effect to the app drawer which replaces our beloved android over scroll effect with the items moving and bouncing when the scrollable content finishes.
It has been a typical iOS detail in the past and probably a few years ago I would have been reluctant to implement it but I have to admit that I really think it provides better feeling to the user. Let’s see how to implement it in one simple class that you can add to any RecyclerView.

First things first, credits

This implementation has been adapted from the one published here by Leo Wu, most of the work has been done by him. My addition has been to achieve the effect involving only the class which, in my opinion, should be in charge of handling all the logic, instead of making the ViewHolder take a part on it. So big thanks to Leo Wu for his article.

How does it work?

In the past I’ve seen several ways of achieving this but usually those consisted in subclassing RecyclerView or playing with the adapter and item animations, which to me felt as partial solutions. I expect RecyclerView to have something like its Decorations or LayoutManager, something that you can attach to your RecyclerView and takes care of the job. Well, it exists and is called EdgeEffectFactory.

recyclerView.apply {
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
adapter = MyAdapter()
edgeEffectFactory
= BounceEdgeEffectFactory()
}

Thats all we need in our recyclerView to make the items bounce instead of displaying the Edge Overlay. Now let’s take a look to the implementation.

It just discovered that bounce effect can be achieved without subclassing RecyclerView or playing with the adapter (Photo by Daniel Bernard on Unsplash)

Achieve the bounce with physic based animations

The bounce animations can be implemented really easy with SpringAnimation. This animation mimics physics, so we’ll be defining things like the force we are applying or the stiffness of the spring we are using.

val anim: SpringAnimation = SpringAnimation(recyclerView, SpringAnimation.TRANSLATION_Y)
.setSpring(SpringForce()
.setFinalPosition(0f)
.setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
.setStiffness(SpringForce.STIFFNESS_LOW)
)

When the user pulls the content more than the end of the recycler, we will translate the content and later apply this animation to move the content bouncing back to the rest position (which is translation 0).

When the user flings the scroll, we’ll play the animation adding some extra start velocity, which will produce the desired effect.

Detect user scroll actions with EdgeEffect class

The class responsible for this is EdgeEffect and we will be creating one with the proper factory.

/** The magnitude of translation distance while the list is over-scrolled. */
private const val OVERSCROLL_TRANSLATION_MAGNITUDE = 0.2f

/** The magnitude of translation distance when the list reaches the edge on fling. */
private const val FLING_TRANSLATION_MAGNITUDE = 0.5f

/**
* Replace edge effect by a bounce
*/
class BounceEdgeEffectFactory : RecyclerView.EdgeEffectFactory() {

override fun createEdgeEffect(recyclerView: RecyclerView, direction: Int): EdgeEffect {

return object : EdgeEffect(recyclerView.context) {

// A reference to the [SpringAnimation] for this RecyclerView used to bring the item back after the over-scroll effect.
var anim: SpringAnimation? = null

override fun onPull(deltaDistance: Float) {
super.onPull(deltaDistance)
handlePull(deltaDistance)
}

override fun onPull(deltaDistance: Float, displacement: Float{
super.onPull(deltaDistance, displacement)
handlePull(deltaDistance)
}
private fun handlePull(deltaDistance: Float) {
// Translate the recyclerView with the distance
val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
val translationYDelta = sign * recyclerView.width * deltaDistance * OVERSCROLL_TRANSLATION_MAGNITUDE
recyclerView.translationY += translationYDelta

translationAnim?.cancel()
}
override fun onRelease() {
super.onRelease()
// The finger is lifted. Start the animation to bring translation back to the resting state.
if (recyclerView.translationY != 0f) {
anim = createAnim()?.also { it.start() }
}
}

override fun onAbsorb(velocity: Int) {
super.onAbsorb(velocity)

// The list has reached the edge on fling.
val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
val translationVelocity = sign * velocity * FLING_TRANSLATION_MAGNITUDE
anim?.cancel()
anim = createAnim().setStartVelocity(translationVelocity)?
.also { it.start() }
}

private fun createAnim() =
SpringAnimation(recyclerView, SpringAnimation.TRANSLATION_Y)
.setSpring(SpringForce()
.setFinalPosition(0f)
.setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
.setStiffness(SpringForce.STIFFNESS_LOW))
...
  • onPull() is called when the user is pulling the content more than the limit of the recyclerView. In this case, we’ll translate the whole recyclerView.
  • onRelease() is called when the users lift the finger after a pull and this is the time to play our animation. The animation will move the content to the rest state.
  • onAbsorb() gets called when the user flings the content of the scroll view and reaches the edge when there is still force. When this happens, we want the content to move a little down and the bounce back to the final position, this is done setting some start velocity to the animation.

There is still some extra methods we can override to end our implementation:

override fun draw(canvas: Canvas?): Boolean {
// don't paint the usual edge effect
return false
}

override fun isFinished(): Boolean {
// Without this, will skip future calls to onAbsorb()
return anim?.isRunning?.not() ?: true
}

Those are pretty self explanatory.

Wrapping up

And thats it, all the logic is contained in a class which can be added to any RecyclerView. You can check a sample app and the full code in the following repo:
https://github.com/juanmeanwhile/BounceRecyclerView

Best!

--

--