为什么RecyclerView的平滑滚动不能与某些Interpolator类一起使用?

时间:2018-09-03 10:51:28

标签: android scroll android-recyclerview interpolation

背景

为了对水平RecyclerView上的项目进行简短的概述,我们希望有一个类似反弹的动画,该动画从某个位置开始,一直到RecyclerView的开始(例如,从项目3到项目0)。

问题

由于某种原因,我尝试的所有Interpolator类(可用插图here)似乎都不允许物品进入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)
        }
    }
}

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"/>

渐变文件

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

这是它如何寻找BounceInterpolator的动画,如您所见,它根本不会反弹:

enter image description here

可用的示例POC项目here

问题

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

RecyclerView是否可以与Interpolator一起滚动使用?


编辑:看来这是个错误,因为我不能在RecyclerView滚动中使用任何“有趣的”内插器,所以我已经对此进行了报道here

3 个答案:

答案 0 :(得分:6)

我将看一下Google的支持动画包。具体是https://developer.android.com/reference/android/support/animation/DynamicAnimation#SCROLL_X

它看起来像:

SpringAnimation(recyclerView, DynamicAnimation.SCROLL_X, 0f)
        .setStartVelocity(1000)
        .start()

更新:

看起来也不行。我查看了RecyclerView的一些来源,以及反弹插补器不起作用的原因是因为RecyclerView没有正确使用插补器。有一个对computeScrollDuration的调用,对插值器的调用随后获得了原始动画时间(以秒为单位),而不是该值占动画总时间的百分比。这个值也不是完全可以预测的,我测试了一些值,并看到了100ms-250ms的任何地方。无论如何,从我所看到的来看,您有两个选择(我都对它们进行了测试)

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

  2. 实现您自己的属性并使用春季动画:


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

然后在弹跳中,将对smoothScrollBy的呼叫替换为以下形式:

SpringAnimation(recyclerView, ScrollXProperty())
    .setSpring(SpringForce()
            .setFinalPosition(0f)
            .setStiffness(SpringForce.STIFFNESS_LOW)
            .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY))
    .start()

更新:

第二种解决方案是开箱即用的,无需更改RecyclerView,这是我编写并完全测试过的解决方案。

有关插值器的更多信息,smoothScrollBy与插值器不能很好地配合(可能是一个错误)。使用插值器时,您基本上会将0-1值映射到另一个值,该值是动画的乘数。示例:t = 0,interp(0)= 0表示在动画开始时该值应与动画开始时相同,t = .5,interp(.5)=。25表示该元素将设置动画1方式的/ 4,等等。反弹插值器基本上在某个点返回值> 1,并在大约1处振荡,直到t = 1时最终稳定为1。

解决方案#2正在使用弹簧动画器,但需要更新滚动条。 SCROLL_X不起作用的原因是RecyclerView实际上没有滚动(这是我的错误)。它根据不同的计算方法来计算视图的位置,这就是为什么需要调用computeHorizontalScrollOffset的原因。 ScrollXProperty允许您更改RecyclerView的水平滚动,就好像您在ScrollView中指定了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()

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

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>

答案 1 :(得分:0)

就像我说的那样,我不会诚实地期望发布候选版本(在您的情况下为recyclerview:1.0.0-rc02)不会出现任何问题,因为它已经在开发中。

  • 使用第三方库可能有效,但是我真的不这么认为,因为它们刚刚引入了androidx依赖关系,并且它们已经处于开发阶段,并且 不够稳定 供其他开发人员使用。

更新来自Google Developers的答案:

  

我们已将此问题传递给开发团队,并将更新此问题   并提供更多信息。

因此,最好等待新更新。

答案 2 :(得分:0)

您需要以某种方式启用过度滚动弹跳。

一种可能的解决方案是使用 https://github.com/chthai64/overscroll-bouncy-android

将其包含在您的项目中

implementation 'com.chauthai.overscroll:overscroll-bouncy:0.1.1'

然后在您的POV中将RecyclerView中的activity_main.xml更改为com.chauthai.overscroll.RecyclerViewBouncy

<?xml version="1.0" encoding="utf-8"?>
<com.chauthai.overscroll.RecyclerViewBouncy
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="@dimen/list_item_size"
    android:orientation="horizontal"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

更改后,您会在应用中看到跳动。