Why Doesn't Smooth Scroll Of RecyclerView Work Well With Some Interpolator Classes?


In an effort to make a nice&short overview of the items on a horizontal RecyclerView, we want to have a bounce-like animation , that starts from some position, and goes to the beginning of the RecyclerView (say, from item 3 to item 0) .

The problem

For some reason, all Interpolator classes I try (illustration available here) don't seem to allow items to go outside of the RecyclerView or bounce on it.

More specifically, I've tried OvershootInterpolator , BounceInterpolator and some other similar ones. I even tried AnticipateOvershootInterpolator. In most cases, it does a simple scrolling, without the special effect. on AnticipateOvershootInterpolator , it doesn't even scroll...

What I've tried

Here's the code of the POC I've made, to show the issue:


class MainActivity : AppCompatActivity() {
    val handler = Handler()

    override fun onCreate(savedInstanceState: Bundle?) {
        val itemSize = resources.getDimensionPixelSize(R.dimen.list_item_size)
        val itemsCount = 6
        recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
                val imageView = ImageView([email protected])
                imageView.layoutParams = RecyclerView.LayoutParams(itemSize, itemSize)
                return object : RecyclerView.ViewHolder(imageView) {}

            override fun getItemCount(): Int = itemsCount

            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val itemToGoTo = Math.min(3, itemsCount - 1)
        val scrollValue = itemSize * itemToGoTo {
            recyclerView.scrollBy(scrollValue, 0)
                recyclerView.smoothScrollBy(-scrollValue, 0, BounceInterpolator())
            }, 500L)


    android:id="@+id/recyclerView" xmlns:android=""
    xmlns:app="" android:layout_width="match_parent"
    android:layout_height="@dimen/list_item_size" android:orientation="horizontal"

gradle file

apply plugin: ''
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.example.myapplication"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), ''

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.0-rc02'
    implementation 'androidx.core:core-ktx:1.0.0-rc02'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
    implementation 'androidx.recyclerview:recyclerview:1.0.0-rc02'

And here's an animation of how it looks for BounceInterpolator , which as you can see doesn't bounce at all :

enter image description here

Sample POC project available here

The question

Why doesn't it work as expected, and how can I fix it?

Could RecyclerView work well with Interpolator for scrolling ?

EDIT: seems it's a bug, as I can't use any "interesting" interpolator for RecyclerView scrolling, so I've reported about it here .



I would take a look at Google's support animation package. Specifically

It would look something like:

SpringAnimation(recyclerView, DynamicAnimation.SCROLL_X, 0f)


Looks like this doesn't work either. I looked at some of the source for RecyclerView and the reason that the bounce interpolator doesn't work is because RecyclerView isn't using the interpolator correctly. There's a call to computeScrollDuration the calls to the interpolator then get the raw animation time in seconds instead of the value as a % of the total animation time. This value is also not entirely predictable I tested a few values and saw anywhere from 100ms - 250ms. Anyway, from what I'm seeing you have two options (I've tested both)

  1. User another library such as

  2. Implement your own property and use the spring animation:

class ScrollXProperty : FloatPropertyCompat("scrollX") {

    override fun setValue(obj: RecyclerView, value: Float) {
        obj.scrollBy(value.roundToInt() - getValue(obj).roundToInt(), 0)

    override fun getValue(obj: RecyclerView): Float =

Then in your bounce, replace the call to smoothScrollBy with a variation of:

SpringAnimation(recyclerView, ScrollXProperty())


The second solution works out-of-box with no changes to your RecyclerView and is the one I wrote and tested fully.

More about interpolators, smoothScrollBy doesn't work well with interpolators (likely a bug). When using an interpolator you basically map a 0-1 value to another which is a multiplier for the animation. Example: t=0, interp(0)=0 means that at the start of the animation the value should be the same as it started, t=.5, interp(.5)=.25 means that the element would animate 1/4 of the way, etc. Bounce interpolators basically return values > 1 at some point and oscillate about 1 until finally settling at 1 when t=1.

What solution #2 is doing is using the spring animator but needing to update scroll. The reason SCROLL_X doesn't work is that RecyclerView doesn't actually scroll (that was my mistake). It calculates where the views should be based on a different calculation which is why you need the call to computeHorizontalScrollOffset. The ScrollXProperty allows you to change the horizontal scroll of a RecyclerView as though you were specifying the scrollX property in a ScrollView, it's basically an adapter. RecyclerViews don't support scrolling to a specific pixel offset, only in smooth scrolling, but the SpringAnimation already does it smoothly for you so we don't need that. Instead we want to scroll to a discrete position. See


Here's the code I used to test


Got the same concept working with interpolators:

class ScrollXProperty : Property<RecyclerView, Int>(, "horozontalOffset") {
    override fun get(`object`: RecyclerView): Int =

    override fun set(`object`: RecyclerView, value: Int) {
        `object`.scrollBy(value - get(`object`), 0)

ObjectAnimator.ofInt(recycler_view, ScrollXProperty(), 0).apply {
    interpolator = BounceInterpolator()
    duration = 500L

Demo project on GitHub was updated

I updated ScrollXProperty to include an optimization, it seems to work well on my Pixel but I haven't tested on older devices.

class ScrollXProperty(
        private val enableOptimizations: Boolean
) : Property<RecyclerView, Int>(, "horizontalOffset") {

    private var lastKnownValue: Int? = null

    override fun get(`object`: RecyclerView): Int =
            `object`.computeHorizontalScrollOffset().also {
                if (enableOptimizations) {
                    lastKnownValue = it

    override fun set(`object`: RecyclerView, value: Int) {
        val currentValue = lastKnownValue?.takeIf { enableOptimizations } ?: get(`object`)
        if (enableOptimizations) {
            lastKnownValue = value
        `object`.scrollBy(value - currentValue, 0)

The GitHub project now includes demo with the following interpolators:

<string-array name="interpolators">