如何實現RecyclerView Item動畫?
這個問題想必有很多人都會講,我可以用ItemAnimator實現啊,這是RecyclerView官方定義的接口,專門擴展Item動畫的,那我為什么要尋求另外一種方法實現呢?因為最近反思了一個問題,其實很多人都有這個思維定律,那就是官方的一定是好的,真的是這樣嗎?下面我來從另一個角度說明官方的ItemAnimator是真的不好用
ItemAnimator 棄用理由
理由一


- 第一張圖是最牛逼的星星最多的wasabeef/recyclerview-animators,基類有713行代碼,你知道這個類打包出來多大嗎?有20多kb,相當恐怖的好嗎?
- 第二個是官方提供的默認動畫,也是將近700行
理由是:代碼過于臃腫
理由二
既然我想用ItemAnimator接口,且官方有一個DefaultItemAnimator,為什么我不能擴展DefaultItemAnimator,而是要實現SimpleItemAnimator,寫個700行代碼才能夠捋明白一個Item的動畫?總之,當我知道繼承SimpleItemAnimator后,實現的和DefaultItemAnimator幾乎一樣的時候,我內心是拒絕的,我不想看到這些冗余的代碼,也許是我有那么一點點潔癖
理由是:復用率太低,感覺官方根本沒當回事(也許是我學習沒到位,沒有看到它好的地方)
理由三
notifyDataSetChanged不支持ItemAnimator動畫,我不討論這么設計是真的好和壞,但起碼它是我不選擇ItemAnimator的另一個理由
等等吧,不知道還能吐槽什么了?雖然有這些缺點,可我們總會遇到不得不用的時候,你說呢?
layoutAnimation 棄用理由
這個用的比較少吧,大部分都是在用ItemAnimator,我們直接看個例子,然后再說為什么要棄用它
step1
android:duration="@integer/anim_duration_medium">
android:fromYDelta="-20%"
android:toYDelta="0"
android:interpolator="@android:anim/decelerate_interpolator"
/>
android:fromAlpha="0"
android:toAlpha="1"
android:interpolator="@android:anim/decelerate_interpolator"
/>
android:fromXScale="105%"
android:fromYScale="105%"
android:toXScale="100%"
android:toYScale="100%"
android:pivotX="50%"
android:pivotY="50%"
android:interpolator="@android:anim/decelerate_interpolator"
/>
step2
<?xml version="1.0" encoding="utf-8"?>
xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/item_animation_fall_down"
android:delay="15%"
android:animationOrder="normal"
/>
step3
int resId = R.anim.layout_animation_fall_down;
LayoutAnimationController animation = AnimationUtils.loadLayoutAnimation(ctx, resId);
recyclerview.setLayoutAnimation(animation);
很簡單對吧,當我運行demo的時候,似乎看起來效果很好哦,可最后我才發現一些問題,我決定不使用它了
理由一
動畫只加載第一屏?這個能不能改觀我沒有深入研究哦,可這樣一個效果我也無法忍受,但我上啦的時候,為什么下面未顯示的Item動畫就沒了呢?這就是我棄用的理由
理由二
當我用GridLayoutManger 的時候,我還要在定義一套 layhoutAnimation,雖然只是增加了一個xml文件,可這比起我接下來介紹的實現方案,那就差了一個檔次,所以我選擇棄用
最簡單的Animation動畫方案
這個方案的優勢:
- 代碼超級簡潔
- 動畫的定制度更高,沒一個Item都可以輕松的且變著花樣的加載動畫
- 可實現預加載動畫,可實現更新的動畫
- 輕松實現一個接一個的加載動畫
- 緩存更加輕量級,減少內存開銷
缺點:當然也有缺點,這個看具體使用場景的取舍,也許是可以支持的,但目前我還沒有想到如何做到。
- 沒有刪除動畫
- 沒有移動動畫
對的目前就這倆。
實現原理
很簡單,就是給View加載一個Animation,通過xml配置
實現效果
代碼
step1
從下往上移動的動畫
<?xml version="1.0" encoding="utf-8"?>
android:duration="@integer/anim_duration_long">
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:fromYDelta="50%p"
android:toYDelta="0"
/>
android:fromAlpha="0"
android:toAlpha="1"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
/>
step2
//從零開始計數,用來實現一個接一個的延遲動畫(簡單點就是:在一個加載一半時,下一個才執行)
private var delayPosition = 0
//緩存Animation,避免重復loadAnimation,減少開銷
private val animationArray = SparseArray()
//加載xml動畫,并放入緩存中
private fun loadAnimation(context: Context, @AnimRes itemAnimationRes: Int, key: Int): Animation {
return animationArray[key] ?: AnimationUtils.loadAnimation(context, itemAnimationRes).apply {
animationArray.append(key, this)
}
}
//清理緩存
fun RecyclerView.onDestroy(){
animationArray.clear()
}
//執行動畫
fun RecyclerView.ViewHolder.animationWithDelayOffset(
isEnableAnimation: Boolean,
@AnimRes itemAnimationRes: Int,
delayOffset: Int
) {
if (isEnableAnimation) {
//清理調之前的動畫
itemView.clearAnimation()
//當前item positon
val currentPosition = ++delayPosition
//計算下一個Item需要delay的時間
val delay = currentPosition * delayOffset / 2
itemView.animation =
loadAnimation(itemView.context, itemAnimationRes, currentPosition).apply {
//延遲多久開始執行
startOffset = delay.toLong()
//加載完成后將計數制成零
setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(p0: Animation?) {
}
override fun onAnimationEnd(p0: Animation?) {
delayPosition = 0
}
override fun onAnimationStart(p0: Animation?) {
}
})
//如果已經執行一次才會調用start,因為第一次用AnimationUtils.loadAnimation加載的時候會自動執行一次
if (this.hasEnded()) {
this.start()
}
}
}
}
//這個是我的DefaultViewHolder,我可以拿到ViewModel是否是第一次isFirstInit,這樣就可以實現只有第一次初始化后才會執行哦
fun DefaultViewHolder.firstAnimation(
@AnimRes itemAnimationRes: Int = R.anim.item_animation_from_right,
delayOffset: Int = 200
) = animationWithDelayOffset(
getViewModel()?.isFirstInit ?: false,
itemAnimationRes,
delayOffset
)
//拿到ViewModel是否是第一次isFirstInit,這樣就可以實現只有第二次加載執行哦
fun DefaultViewHolder.updateAnimation(
@AnimRes itemAnimationRes: Int = R.anim.item_animation_scale
) = animation(!(getViewModel()?.isFirstInit ?: false), itemAnimationRes)
代碼里有個細節處理《加載完成后將計數制成零》,這里是因為,你第一次進頁面的時候,所有的Item的都按照順序執行完畢后,由于delay數很大,導致你滑動的時候,會出現很久才加載進來的動畫哦,這里我是想用handler的postdelay實現,這樣就可以做到只要有接著的動畫執行,就不會被重制成0,保證下次動畫的執行一定是在上一個Item的后面,這樣確實是一個問題,也許在我驗證夠多的場景后就切過去了,嘿嘿。但目前來看,這個效果實現的很滿意,慢慢重構和完善。當然我的這種實現方式,確實是比較簡單而且效果還不錯,既有LayoutAnimation的影子,又有ItemAnimator的功能,豈不是很不錯。
step3
應用,一行代碼搞定

配置更新時候的動畫,一行搞定
感覺到簡單了吧,這么順滑的動畫實現,你不想體驗下嗎?
Demo地址,歡迎體驗
https://github.com/ibaozi-cn/RecyclerViewAdapter
接下來會解決什么問題?
這么用難道確實比ItemAnimator好嗎?當然會有一些問題吧,比如什么時候需要中斷,什么時候需要重新加載,甚至到底什么時候清理掉緩存更合理呢?之后還需要一些更全面的實戰來解決這問題,也會借助ItemAnimator的實現原則來考慮當前動畫如何做到合理的生命周期管理。
作者
i校長
- 簡書 https://www.jianshu.com/u/77699cd41b28
- 掘金 https://juejin.im/user/131597127135687
- 個人網站 http://jetpack.net.cn ?、 http://ibaozi.cn