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) .
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...
Here's the code of the POC I've made, to show the issue:
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 file
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'
}
And here's an animation of how it looks for BounceInterpolator , which as you can see doesn't bounce at all :
Sample POC project available here
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 can do this by: final int height=recyclerView. getChildAt(0). getHeight(); recyclerView.
Advantages of RecyclerView over listview : Contains ViewHolder by default. Easy animations. Supports horizontal , grid and staggered layouts.
To help you build apps with lists, Android provides the RecyclerView . RecyclerView is designed to be very efficient, even with large lists, by reusing, or recycling, the views that have scrolled off the screen.
I would take a look at Google's support animation package. Specifically https://developer.android.com/reference/android/support/animation/DynamicAnimation#SCROLL_X
It would look something like:
SpringAnimation(recyclerView, DynamicAnimation.SCROLL_X, 0f)
.setStartVelocity(1000)
.start()
UPDATE:
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)
User another library such as https://github.com/EverythingMe/overscroll-decor
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 =
obj.computeHorizontalScrollOffset().toFloat()
}
Then in your bounce, replace the call to smoothScrollBy
with a variation of:
SpringAnimation(recyclerView, ScrollXProperty())
.setSpring(SpringForce()
.setFinalPosition(0f)
.setStiffness(SpringForce.STIFFNESS_LOW)
.setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY))
.start()
UPDATE:
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 https://android.googlesource.com/platform/frameworks/support/+/247185b98675b09c5e98c87448dd24aef4dffc9d/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java#387
UPDATE:
Here's the code I used to test https://github.com/yperess/StackOverflow/tree/52148251
UPDATE:
Got the same concept working with interpolators:
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()
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>(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)
}
}
The GitHub project now includes demo with the following interpolators:
<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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With