为什么 RecyclerView 的平滑滚动不能很好地与某些 Interpolator 类 一起工作?
Why doesn't smooth scroll of RecyclerView work well with some Interpolator classes?
背景
为了对水平 RecyclerView 上的项目做一个漂亮而简短的概述,我们想要一个类似弹跳的动画,它从某个位置开始,然后转到 RecyclerView 的开头(比如,从项目3 到项目 0) .
问题
出于某种原因,所有 Interpolator classes I try (illustration available 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"/>
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'
}
下面是它如何查找 BounceInterpolator 的动画,如您所见,它根本没有反弹:
示例 POC 项目可用 here
问题
为什么它没有按预期工作,我该如何解决?
RecyclerView 可以与 Interpolator 一起很好地滚动吗?
编辑:这似乎是一个错误,因为我不能使用任何 "interesting" 插值器来滚动 RecyclerView,所以我已经报告了它 here 。
我会看一下 Google 的支持动画包。具体来说 https://developer.android.com/reference/android/support/animation/DynamicAnimation#SCROLL_X
它看起来像:
SpringAnimation(recyclerView, DynamicAnimation.SCROLL_X, 0f)
.setStartVelocity(1000)
.start()
更新:
看来这也不行。我查看了 RecyclerView 的一些源代码,反弹插值器不起作用的原因是 RecyclerView 没有正确使用插值器。调用 computeScrollDuration
调用插值器,然后获取以秒为单位的原始动画时间,而不是作为总动画时间百分比的值。这个值也不是完全可预测的,我测试了几个值,发现在 100 毫秒到 250 毫秒之间。无论如何,据我所知,您有两种选择(我都测试过)
实现自己的属性并使用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()
}
然后在您的弹跳中,将对 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 等等。反弹插值器基本上 return 值在某个点 > 1 并在 1 左右振荡,直到 t=1 时最终稳定在 1。
#2 的解决方案是使用 spring 动画师但需要更新滚动。 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/Whosebug/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>
就像我说的,老实说,我不希望 Release Candidate(recyclerview:1.0.0-rc02
在你的情况下)会正常工作而不会引起任何问题,因为它已经在开发中.
- 使用第三方库可能行得通,但我真的不这么认为,因为它们刚刚引入了
androidx
依赖项并且它们已经在开发中并且 不够稳定,无法被其他开发者使用。
更新 Google 开发者的回答:
We have passed this to the development team and will update this issue
with more information as it becomes available.
所以,最好等待新的更新。
您需要以某种方式启用滚动反弹。
可能的解决方案之一是使用
https://github.com/chthai64/overscroll-bouncy-android
将其包含在您的项目中
implementation 'com.chauthai.overscroll:overscroll-bouncy:0.1.1'
然后在您的 POV 中将 activity_main.xml
中的 RecyclerView
更改为 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"/>
此更改后,您将在应用中看到弹跳。
背景
为了对水平 RecyclerView 上的项目做一个漂亮而简短的概述,我们想要一个类似弹跳的动画,它从某个位置开始,然后转到 RecyclerView 的开头(比如,从项目3 到项目 0) .
问题
出于某种原因,所有 Interpolator classes I try (illustration available 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"/>
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'
}
下面是它如何查找 BounceInterpolator 的动画,如您所见,它根本没有反弹:
示例 POC 项目可用 here
问题
为什么它没有按预期工作,我该如何解决?
RecyclerView 可以与 Interpolator 一起很好地滚动吗?
编辑:这似乎是一个错误,因为我不能使用任何 "interesting" 插值器来滚动 RecyclerView,所以我已经报告了它 here 。
我会看一下 Google 的支持动画包。具体来说 https://developer.android.com/reference/android/support/animation/DynamicAnimation#SCROLL_X
它看起来像:
SpringAnimation(recyclerView, DynamicAnimation.SCROLL_X, 0f)
.setStartVelocity(1000)
.start()
更新:
看来这也不行。我查看了 RecyclerView 的一些源代码,反弹插值器不起作用的原因是 RecyclerView 没有正确使用插值器。调用 computeScrollDuration
调用插值器,然后获取以秒为单位的原始动画时间,而不是作为总动画时间百分比的值。这个值也不是完全可预测的,我测试了几个值,发现在 100 毫秒到 250 毫秒之间。无论如何,据我所知,您有两种选择(我都测试过)
实现自己的属性并使用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()
}
然后在您的弹跳中,将对 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 等等。反弹插值器基本上 return 值在某个点 > 1 并在 1 左右振荡,直到 t=1 时最终稳定在 1。
#2 的解决方案是使用 spring 动画师但需要更新滚动。 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/Whosebug/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>
就像我说的,老实说,我不希望 Release Candidate(recyclerview:1.0.0-rc02
在你的情况下)会正常工作而不会引起任何问题,因为它已经在开发中.
- 使用第三方库可能行得通,但我真的不这么认为,因为它们刚刚引入了
androidx
依赖项并且它们已经在开发中并且 不够稳定,无法被其他开发者使用。
更新 Google 开发者的回答:
We have passed this to the development team and will update this issue with more information as it becomes available.
所以,最好等待新的更新。
您需要以某种方式启用滚动反弹。
可能的解决方案之一是使用 https://github.com/chthai64/overscroll-bouncy-android
将其包含在您的项目中
implementation 'com.chauthai.overscroll:overscroll-bouncy:0.1.1'
然后在您的 POV 中将 activity_main.xml
中的 RecyclerView
更改为 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"/>
此更改后,您将在应用中看到弹跳。