为什么不能使用一些Interpolator类顺利滚动RecyclerView呢?

and*_*per 16 android interpolation scroll android-recyclerview

背景

为了对水平RecyclerView上的项目进行简洁概述,我们希望有一个类似于弹跳的动画,从某个位置开始,然后转到RecyclerView的开头(例如,从第3项到第0项) .

问题

出于某种原因,我尝试的所有Interpolator类(此处提供的插图)似乎都不允许项目超出RecyclerView或反弹.

更具体地说,我尝试过OvershootInterpolator,BounceInterpolator和其他一些类似的东西.我甚至尝试过AnticipateOvershootInterpolator.在大多数情况下,它只进行简单的滚动,没有特殊效果.在AnticipateOvershootInterpolator上,它甚至不滚动...

我试过的

以下是我所制作的POC的代码,以显示问题:

MainActivity.kt

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        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(this@MainActivity)
                imageView.setImageResource(android.R.drawable.sym_def_app_icon)
                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.post {
            recyclerView.scrollBy(scrollValue, 0)
            handler.postDelayed({
                recyclerView.smoothScrollBy(-scrollValue, 0, BounceInterpolator())
            }, 500L)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

activity_main.xml中

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView" xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent"
    android:layout_height="@dimen/list_item_size" android:orientation="horizontal"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
Run Code Online (Sandbox Code Playgroud)

gradle文件

apply plugin: 'com.android.application'
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'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    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'
}
Run Code Online (Sandbox Code Playgroud)

这是一个关于它如何寻找BounceInterpolator的动画,正如你所看到的那样根本不会反弹:

在此输入图像描述

此处提供样本POC项目

这个问题

为什么它没有按预期工作,我该如何解决?

RecyclerView能否与Interpolator一起滚动?


编辑:似乎这是一个错误,因为我不能使用任何"有趣"插补器进行RecyclerView滚动,所以我在这里报告了它.

The*_*mer 7

我会看看谷歌的支持动画包.具体来说,请访问https://developer.android.com/reference/android/support/animation/DynamicAnimation#SCROLL_X

它看起来像:

SpringAnimation(recyclerView, DynamicAnimation.SCROLL_X, 0f)
        .setStartVelocity(1000)
        .start()
Run Code Online (Sandbox Code Playgroud)

更新:

看起来这也不起作用.我查看了RecyclerView的一些来源以及反弹插补器不起作用的原因是因为RecyclerView没有正确使用插值器.有一个电话computeScrollDuration呼叫到内插,然后在数秒而非价值为总动画时间的百分比的原始动画时间.这个值也不是完全可预测的我测试了几个值,并且看到了100ms到250ms的任何地方.无论如何,从我所看到的你有两个选择(我已经测试了两个)

  1. 用户另一个库,如https://github.com/EverythingMe/overscroll-decor

  2. 实现自己的属性并使用spring动画:


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 =
            obj.computeHorizontalScrollOffset().toFloat()
}
Run Code Online (Sandbox Code Playgroud)

然后在你的反弹中,将呼叫替换为smoothScrollBy以下变体:

SpringAnimation(recyclerView, ScrollXProperty())
    .setSpring(SpringForce()
            .setFinalPosition(0f)
            .setStiffness(SpringForce.STIFFNESS_LOW)
            .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY))
    .start()
Run Code Online (Sandbox Code Playgroud)

更新:

第二个解决方案是开箱即用的,不会对您的RecyclerView进行任何更改,而是我编写并完全测试的解决方案.

有关插值器的更多信息,smoothScrollBy与插值器不兼容(可能是一个bug).使用插值器时,您基本上将0-1值映射到另一个值,这是动画的乘数.示例:t = 0,interp(0)= 0表示在动画开始时,该值应与其开始时相同,t = .5,interp(.5)=.25表示该元素将为1设置动画反弹插补器基本上在某点返回值> 1并且振荡约1直到t = 1时最终稳定在1.

#2正在做的解决方案是使用弹簧动画师,但需要更新滚动.SCROLL_X不起作用的原因是RecyclerView实际上没有滚动(这是我的错误).它根据您需要调用的不同计算来计算视图的位置computeHorizontalScrollOffset.将ScrollXProperty允许你改变一个RecyclerView的水平滚动,就像你指定scrollX在一个滚动型属性,它基本上是一个适配器.RecyclerViews不支持滚动到特定的像素偏移,仅支持平滑滚动,但SpringAnimation已经为您顺利完成,因此我们不需要.相反,我们想要滚动到一个离散的位置.请参阅https://android.googlesource.com/platform/frameworks/support/+/247185b98675b09c5e98c87448dd24aef4dffc9d/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java#387

更新:

这是我用来测试的代码https://github.com/yperess/StackOverflow/tree/52148251

更新:

使用内插器的概念相同:

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

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

ObjectAnimator.ofInt(recycler_view, ScrollXProperty(), 0).apply {
    interpolator = BounceInterpolator()
    duration = 500L
}.start()
Run Code Online (Sandbox Code Playgroud)

GitHub上的演示项目已更新

我更新ScrollXProperty了包括优化,它似乎在我的Pixel上运行良好,但我还没有在旧设备上测试过.

class ScrollXProperty(
        private val enableOptimizations: Boolean
) : Property<RecyclerView, Int>(Int::class.java, "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)
    }
}
Run Code Online (Sandbox Code Playgroud)

GitHub项目现在包含带有以下插值器的演示:

<string-array name="interpolators">
    <item>AccelerateDecelerate</item>
    <item>Accelerate</item>
    <item>Anticipate</item>
    <item>AnticipateOvershoot</item>
    <item>Bounce</item>
    <item>Cycle</item>
    <item>Decelerate</item>
    <item>Linear</item>
    <item>Overshoot</item>
</string-array>
Run Code Online (Sandbox Code Playgroud)