我们知道,RecyclerView 中已经添加了 notifyItemChange() notifyItemRemove() 等等单个条目更改的方法,大方向说,这个相对于 ListView 或者 notifyDataChange() 方法 , 它已经算是做到局部刷新。小方向再说,这些添加的刷新方法,其实默认都是带有动画效果,具体效果是 DefaultItemAnimator 来控制和处理,就是因为动画效果,让开发时会出现一些意料之外的状况。
假设我们现在有一个上传照片的场景,我们每一个 ViewHolder 带有一张图片,然后上面有一个进度条,进度根据上传进度实时返回。如果你使用 notifyItemChange() 来更新动画的话,那么会有两个问题:
- 第一,你会发现每刷新一次,整个布局都会闪动一下。
- 如果我们多次调用 notifyItemChange() 方法,条目会刷新多次吗?
- 第二,这个进度的数值我需要怎么传递才好呢?在 ViewHolder 的 Bean 对象中添加一个 progress 临时字段?
- 其实可以通过
notifyItemChanged(int position, @Nullable Object payload)
. 每次在onbind时拿取出 payloads 集合中最后一个值(最新的进度) - 那么,payload 参数怎么传递到 ViewHolder 中?
另外针对局部刷新,还有两个问题:
- 第一,notifyItemChange() 和 真正的局部刷新 同一个位置,ViewHolder 还是同一个对象吗?
- 第二,局部刷新是没有设置动画效果吗?
带着这些问题,开始 RecyclerView 这一部分源码探索,接下来的所有源码基于 Android API 27 Platform。
一、notifyItemChange() 第二个参数
上面说了半天 RecyclerView
真正的局部刷新,但是,到底怎么就是局部刷新呢?其实很简单,看看 notifyItemChange(int position)
另外一个重载函数。
public final void notifyItemChanged(int position, @Nullable Object payload) {
mObservable.notifyItemRangeChanged(position, 1, payload);
}
同时,在 Adapter 中,与 onBindViewHolder(@NonNull VH holder, int position)
相对应,也有一个重载函数。默认实现就走上面这个方法。
public void onBindViewHolder(@NonNull VH holder, int position,
@NonNull List<Object> payloads) {
onBindViewHolder(holder, position);
}
其实对于第一个额外问题(如果我们多次调用 notifyItemChange() 方法,条目会刷新多次吗?)
- 从上面两个方法中,我们就能猜到一些答案,多次调用应该只会回调刷新一次
- 你看传入的 payload 是一个 Object,但是到 onBindViewHolder() 方法时参数却成了一个集合,那应该就是有合并的操作。
- 另外再从性能上说
- 连着 notify 多次,就重新 measure layout 多次的话,这个开销也是很大并且没有必要(RecyclerView 严格控制 requestLayout() 方法调用),真没必要
二、RecyclerView.onItemRangeChanged
- 在调用 notifyItemChange() 方法(不管一个参数还是两个个参数)之后
- 最后都会走到 notifyItemRangeChanged(int positionStart, int itemCount,@Nullable Object payload)
- 最后回调到 RecyclerViewDataObserver. onItemRangeChanged() 方法。
//RecyclerView
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
// fallback to onItemRangeChanged(positionStart, itemCount) if app
// does not override this method.
onItemRangeChanged(positionStart, itemCount);
}
//RecyclerViewDataObserver
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
triggerUpdateProcessor();
}
}
// AdapterHelper
void triggerUpdateProcessor() {
if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}
在该方法中,有一个 triggerUpdateProcessor() 方法,它本质上说,就是去请求重新布局。
那就是说,这里只有 if 条件成立,才会去 requestLayout() ,接下来,搞清楚什么时候 if 成立,就能回答这个前置问题。
2.1 AdapterHelper.onItemRangeChanged 判断
/**
* @return True if updates should be processed.
*/
boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
if (itemCount < 1) {
return false;
}
mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
mExistingUpdateTypes |= UpdateOp.UPDATE;
return mPendingUpdates.size() == 1;
}
// 构造UpdateOp
UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
this.cmd = cmd;
this.positionStart = positionStart;
this.itemCount = itemCount;
this.payload = payload;
}
- 第一,
size==1
说明就第一次调用是才返回 true 才触发requestLayout()
; - 第二,
payload 参数
在这里被包装为对象,放入mPendingUpdates
这个集合中
第一个发现,彻底证明上诉猜测是正确的,即使你调用 notify 多次,其实只有第一次会触发 requestLayout()
2.2 requestLayout()里的measure layout
既然有 requestLayout() 调用,那么就回到 onMeasure() 和 onLayout() 这些方法中。
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
...
}
...
}
- 假设我们 RecyclerView 布局就是两个 match_parent 或者有一个精确值,那么执行的代码片段就是这样
主要看layout:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
dispatchLayout();
mFirstLayoutComplete = true;
}
void dispatchLayout() {
...
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
在 RecyclerView 的 onLayout() 方法中,一共执行三大步骤。从命名上已经能清楚看懂。对于三个 step ,每个方法上面都有详细注释。翻译过来就是说:
- 第一步时,处理 Adapter 更新,决定执行的动画效果,保存当前 Views 的信息,最后,如果必要的话,预测布局并保存相关信息;
- 第二步时,根据最终状态执行布局,并且可能执行多次;
- 第三步,保存 View 信息,执行动画,最后做一些清除重置操作。
2.2.1 dispatchLayoutStep1()
处理 Adapter 更新,决定执行的动画效果,保存当前 Views 的信息,最后,如果必要的话,预测布局并保存相关信息。
private void dispatchLayoutStep1() {
mState.assertLayoutStep(State.STEP_START);
startInterceptRequestLayout();
//1.更新 mRunSimpleAnimations 和 mRunPredictiveAnimations flag 其实还有其他一些骚操作
processAdapterUpdatesAndSetAnimationFlags();
//2.mInPreLayout 设置为 true 后面有用
mState.mInPreLayout = mState.mRunPredictiveAnimations;
...
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
continue;
}
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
//5.保存动画信息相关
mViewInfoStore.addToPreLayout(holder, animationInfo);
if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
&& !holder.shouldIgnore() && !holder.isInvalid()) {
//3.如果holder确定要更新,就把它添加到 oldChangeHolders 集合中
long key = getChangedHolderKey(holder);
mViewInfoStore.addToOldChangeHolders(key, holder);
}
}
}
if (mState.mRunPredictiveAnimations) {
...
//4.很重要,LayoutManager 开始工作
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = didStructureChange;
for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
final View child = mChildHelper.getChildAt(i);
final ViewHolder viewHolder = getChildViewHolderInt(child);
if (viewHolder.shouldIgnore()) {
continue;
}
if (!mViewInfoStore.isInPreLayout(viewHolder)) {
...
//5.保存动画信息相关
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
}
}
...
} ...
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}
- 第一点,更新动画相关标识位
mRunSimpleAnimations ,mRunPredictiveAnimations
,后面的操作都依赖它们 - 第二点,将
mInPreLayout
的状态和 mRunPredictiveAnimations
同步, 设置true。这个在后面的步骤中也需要使用 - 第三点,保存需要更新的 ViewHolder 到
oldChangeHolder
集合中 - 第四点,调用
LayoutManager. onLayoutChildren()
。 很重要,LayoutManager 开始工作 - 第五点,保存相关动画信息。
2.2.1.1 第一步 processAdapterUpdatesAndSetAnimationFlags()
/RecyclerView
private void processAdapterUpdatesAndSetAnimationFlags() {
...
// simple animations are a subset of advanced animations (which will cause a
// pre-layout step)
// If layout supports predictive animations, pre-process to decide if we want to run them
...
mAdapterHelper.preProcess();
...
boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
// 通常情况就是 ture
mState.mRunSimpleAnimations = mFirstLayoutComplete
&& mItemAnimator != null
&& (mDataSetHasChangedAfterLayout
|| animationTypeSupported
|| mLayout.mRequestedSimpleAnimations)
&& (!mDataSetHasChangedAfterLayout
|| mAdapter.hasStableIds());
// 通常情况就是 ture
mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
&& animationTypeSupported
&& !mDataSetHasChangedAfterLayout
&& predictiveItemAnimationsEnabled();
}
//AdapterHelper
void preProcess() {
mOpReorderer.reorderOps(mPendingUpdates);
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
...
case UpdateOp.UPDATE:
applyUpdate(op);
break;
}
...
}
mPendingUpdates.clear();
}
//AdapterHelper
private void postponeAndUpdateViewHolders(UpdateOp op) {
if (DEBUG) {
Log.d(TAG, "postponing " + op);
}
mPostponedList.add(op);
switch (op.cmd) {
...
case UpdateOp.UPDATE:
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
break;
default:
throw new IllegalArgumentException("Unknown update op type for " + op);
}
}
- 首先执行 mAdapterHelper.preProcess() 后,会将刚刚上文说到的onItemRangeChanged() 方法中的 payload 包装成的 UpdateOp 对象,到这里,要开始处理这个对象
- cmd 对应我们的操作,这里就是 update,后面就是 notifyItemRangeChange() 方法中对应的参数。
AdapterHelper 最后会使用 callback 回调到 RecyclerView 中,在 RecyclerView 中执行 viewRangeUpdate() 方法。这个 callback 是 RecyclerView 在创建时就已经设置:
//RecyclerView 初始化是调用
void initAdapterManager() {
mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
....
@Override
public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
viewRangeUpdate(positionStart, itemCount, payload);
mItemsChanged = true;
}
});
}
//RecyclerView
void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
final int childCount = mChildHelper.getUnfilteredChildCount();
final int positionEnd = positionStart + itemCount;
for (int i = 0; i < childCount; i++) {
final View child = mChildHelper.getUnfilteredChildAt(i);
final ViewHolder holder = getChildViewHolderInt(child);
if (holder == null || holder.shouldIgnore()) {
continue;
}
if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
// 很重要,在这里更新了 Flag 然后将 payload 传递到 Viewholder 中
holder.addFlags(ViewHolder.FLAG_UPDATE);
holder.addChangePayload(payload);
// lp cannot be null since we get ViewHolder from it.
((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
}
}
mRecycler.viewRangeUpdate(positionStart, itemCount);
}
在 viewRangeUpdate() 方法中:
holder.addFlags(ViewHolder.FLAG_UPDATE)
-
holder
加上FLAG_UPDATE
标识,请先记住,这个标识很重要。
holder.addChangePayload(payload);
- 然后,关键问题之一来了,payload 通过 addChangePayload() 方法直接加到对应 holder 中
2.2.1.2 第三步,保存需要更新的 ViewHolder 到 oldChangeHolder 集合中
processAdapterUpdatesAndSetAnimationFlags() 后部分,会设置 mRunSimpleAnimations 和 mRunPredictiveAnimations 两个标识为 true
从而会触发第三步:
if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
&& !holder.shouldIgnore() && !holder.isInvalid()) {
//3.如果holder确定要更新,就把它添加到 oldChangeHolders 集合中
long key = getChangedHolderKey(holder);
mViewInfoStore.addToOldChangeHolders(key, holder);
}
2.2.1.3 调用 LayoutManager. onLayoutChildren()。 很重要,LayoutManager 开始工作
在 LayoutManger 的 onLayoutChildren() 中有大量代码,这里就看我们关心的两行代码,其实就是两个:
detachAndScrapAttachedViews()
-
fill()
方法。
//LinearLayoutManger
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
detachAndScrapAttachedViews(recycler);
fill(recycler, mLayoutState, state, false);
...
}
- detachAndScrapAttachedViews() 这个方法最后会反向遍历所有 View 依次调用 Recycler 的 scrapView() 方法
- 关于 Recycler ,可以说是RecyclerView 的核心之一
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
// 如果不需要更新 放到 mAttachedScrap 中
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
...
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
// 需要更新 放到 mChangedScrap 中
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
- 如果不需要更新,会加到 mAttachedScrap 全家桶中
- 需要更新的,就会放到 mChangedScrap 中。
- 为什么要加入到这些集合中呢,因为后面 fill() 的时候会通过这些集合去找对应的 holder,生产对应的 View 最后真正添加到 RecyclerView 控件中
-
fill()
方法中,最终会调用tryGetViewHolderForPositionByDeadline()
方法找到 ViewHolder,拿到对应 View,然后addView()
//Recycler
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 在 dispatchLayoutStep1 中 设置为 mState.mInPreLayout = mState.mRunPredictiveAnimations
// 0. 从 mChangedScrap 集合中寻找
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1. Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
...
final int type = mAdapter.getItemViewType(offsetPosition);
// 2. Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
// 3. 从 mViewCacheExtension 查找,mViewCacheExtension 默认为 null
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
...
}
}
if (holder == null) { // fallback to pool
...
//4. 从 RecycledViewPool 中查找
holder = getRecycledViewPool().getRecycledView(type);
...
}
if (holder == null) {
...
//5. 老实创建
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
holder 的查找是一个漫长过程:
- **注意这里第 0 步,只有 isPreLayout() 为 true 才会从 mChangedScrap 集合中查找 ViewHolder **
- 在 dispatchLayoutStep1() 中,
mState.mInPreLayout = mState.mRunPredictiveAnimations
默认会设置为 true ,所以会执行到这里。
- 第一步从 mAttachedScrap mHiddenViews mCachedViews 这些集合中查找;
- 第二步通过 独立 id 再次查找;
- 第三步,可能的话,通过 mViewCacheExtension 拓展查找,这个可以通过 RecyclerView 设置;
- 第四部,从 RecycledViewPool 中查找;
- 最后,通过 adapter 创建。
2.2.2 dispatchLayoutStep2()
根据最终状态执行布局,并且可能执行多次。
private void dispatchLayoutStep2() {
...
// 注意,这里 mInPreLayout 设置为 false
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
...
}
相比第一步预备,第二步其实来得简单得多,核心就是
- 将 mInPreLayout 设置为 false
- 然后重新调用 LayoutManager 的 onLayoutChildren() 方法
过程就如上分析,但是,因为这里 mInPreLayout 字段为 false ,而我们之前修改的 ViewHolder 是被添加到 mChangedScrap 集合中,但是因为 mInPreLayout 为 false, 它不会再去 mChangedScrap 查找ViewHolder
所以 旧的 ViewHolder 无法被复用,它会从下面步骤中获取ViewHolder 返回。也就是说,当我们通常使用 notifyItemChangge(pisition) 一个参数的方法之后,刷新时它使用的不是同一个 ViewHolder
2.2.3 dispatchLayoutStep3()
保存 View 信息,执行动画效果,最后做一些清除操作。
private void dispatchLayoutStep3() {
...
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
// traverse list in reverse because we may call animateChange in the loop which may
// remove the target view holder.
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
...
long key = getChangedHolderKey(holder);
//获取当前 holder 动画信息
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
//获取 olderViewHolder
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
final boolean oldDisappearing = mViewInfoStore.isDisappearing(
oldChangeViewHolder);
final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
//如果新旧一样,都是消失,那就直接执行
if (oldDisappearing && oldChangeViewHolder == holder) {
// run disappear animation instead of change
mViewInfoStore.addToPostLayout(holder, animationInfo);
} else {
// 获取之前保存的信息
final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
oldChangeViewHolder);
// 这里一存一取 完成信息覆盖
mViewInfoStore.addToPostLayout(holder, animationInfo);
ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
if (preInfo == null) {
handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
} else {
//添加执行动画效果
animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
oldDisappearing, newDisappearing);
}
}
} else {
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}
// 重点,真正开始执行添加的动画效果
mViewInfoStore.process(mViewInfoProcessCallback);
}
//回收 和 重置
...
}
在 dispatchLayoutStep1() 中,我们保存了一些信息,现在终于要派上用场。
关于动画相关逻辑,已经添加相关注释。
最后需要注意,如果需要执行动画,将会执行 animateChange() 方法,该方法会完成动画相关的创建,并不会直接执行,而是到最后 mViewInfoStore.process(mViewInfoProcessCallback)
调用,才开始真正的获取相关动画信息,并执行。
// RecyclerView
private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,
@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo,
boolean oldHolderDisappearing, boolean newHolderDisappearing) {
oldHolder.setIsRecyclable(false);
if (oldHolderDisappearing) {
addAnimatingView(oldHolder);
}
if (oldHolder != newHolder) {
if (newHolderDisappearing) {
addAnimatingView(newHolder);
}
// 这里很有意思,有点儿像绕口令,这种设定是为了后面做相关释放
oldHolder.mShadowedHolder = newHolder;
addAnimatingView(oldHolder);
mRecycler.unscrapView(oldHolder);
newHolder.setIsRecyclable(false);
newHolder.mShadowingHolder = oldHolder;
}
if (mItemAnimator.animateChange(oldHolder, newHolder, preInfo, postInfo)) {
// 到这里真正执行相关动画
postAnimationRunner();
}
}
// DefaultItemAnimator
@Override
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
if (oldHolder == newHolder) {
// 如果新旧 holder 相同时,就添加一个 move 动画效果
// 如果偏移量为 0 直接返回 false 不执行动画效果
return animateMove(oldHolder, fromX, fromY, toX, toY);
}
// 如果新旧不一样 那么就需要 计算出偏移量,然后创建一个 ChangeInfo
final float prevTranslationX = oldHolder.itemView.getTranslationX();
final float prevTranslationY = oldHolder.itemView.getTranslationY();
final float prevAlpha = oldHolder.itemView.getAlpha();
resetAnimation(oldHolder);
int deltaX = (int) (toX - fromX - prevTranslationX);
int deltaY = (int) (toY - fromY - prevTranslationY);
// recover prev translation state after ending animation
oldHolder.itemView.setTranslationX(prevTranslationX);
oldHolder.itemView.setTranslationY(prevTranslationY);
oldHolder.itemView.setAlpha(prevAlpha);
if (newHolder != null) {
// carry over translation values
resetAnimation(newHolder);
newHolder.itemView.setTranslationX(-deltaX);
newHolder.itemView.setTranslationY(-deltaY);
newHolder.itemView.setAlpha(0);
}
// 添加到 动画 集合中,等待接下来真正运行
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
return true;
}
@Override
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
int toX, int toY) {
final View view = holder.itemView;
fromX += (int) holder.itemView.getTranslationX();
fromY += (int) holder.itemView.getTranslationY();
resetAnimation(holder);
int deltaX = toX - fromX;
int deltaY = toY - fromY;
// 如果平移偏移量都是 0 那么就不执行动画效果
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
//false 不会触发动画效果
return false;
}
if (deltaX != 0) {
view.setTranslationX(-deltaX);
}
if (deltaY != 0) {
view.setTranslationY(-deltaY);
}
// 添加动画效果 等待执行
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
上面三个方法,分别是具体的动画添加过程,其中有些注意点,如果 oldHolder 和 newHolder 相等,并且相关偏移量为零,那么不会添加和执行相关动画效果。
- 如果有相关动画效果,会创建加入到 DefaultItemAnimator 的集合中,然后 mItemAnimator.animateChange() 方法返回 true ,最后调用 postAnimationRunner() 方法执行。
- 在 postAnimationRunner() 方法的 Runnable 中最后会调用 mItemAnimator.runPendingAnimations() ,最后将会执行到 animateChangeImpl() 方法
//DefaultItemAnimator
void animateChangeImpl(final ChangeInfo changeInfo) {
final ViewHolder holder = changeInfo.oldHolder;
final View view = holder == null ? null : holder.itemView;
final ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
//旧View存在的话,执行相关动画
if (view != null) {
final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
getChangeDuration());
mChangeAnimations.add(changeInfo.oldHolder);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchChangeStarting(changeInfo.oldHolder, true);
}
@Override
public void onAnimationEnd(Animator animator) {
//释放
oldViewAnim.setListener(null);
view.setAlpha(1);
view.setTranslationX(0);
view.setTranslationY(0);
//回调 RecyclerView
dispatchChangeFinished(changeInfo.oldHolder, true);
mChangeAnimations.remove(changeInfo.oldHolder);
dispatchFinishedWhenDone();
}
}).start();
}
//新 View 执行相关动画
if (newView != null) {
final ViewPropertyAnimator newViewAnimation = newView.animate();
mChangeAnimations.add(changeInfo.newHolder);
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
.alpha(1).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchChangeStarting(changeInfo.newHolder, false);
}
@Override
public void onAnimationEnd(Animator animator) {
//回收释放
newViewAnimation.setListener(null);
newView.setAlpha(1);
newView.setTranslationX(0);
newView.setTranslationY(0);
//回调 RecyclerView
dispatchChangeFinished(changeInfo.newHolder, false);
mChangeAnimations.remove(changeInfo.newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
}
// RecyclerView inner
private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener {
ItemAnimatorRestoreListener() {
}
@Override
public void onAnimationFinished(ViewHolder item) {
item.setIsRecyclable(true);
//前面 ViewHolder 绕口令式设置,在这里做最后释放
if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh
item.mShadowedHolder = null;
}
// always null this because an OldViewHolder can never become NewViewHolder w/o being
// recycled.
item.mShadowingHolder = null;
if (!item.shouldBeKeptAsChild()) {
if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) {
removeDetachedView(item.itemView, false);
}
}
}
}
在 animateChangeImpl() 方法中,分别为 oldView 和 newView 执行相关动画,最后回调到 RecyclerView 中的 onAnimationFinished() 方法中,完成对 ViewHolder 之前动画相关联 holder 的释放。
2.3 回头看之前提出的局部刷新的两个问题
基于上面第三步分析的结论,如果 oldHolder 和 newHolder 相等,并且偏移量为零,那么不会添加和执行相关动画效果,再结合实际情况
- 我们不妨大胆猜测第一个问题的答案,它们使用的是同一个 ViewHolder 对象
- 接着针对第二个问题,如果使用同一个对象,并且偏移值为 0 ,那么就不会执行相关动画效果
但是,这个结论似乎和我们分析第二步时得出的结论有出入:而我们之前修改的 ViewHolder 是被添加到 mChangedScrap 集合中,但是因为 mInPreLayout 此时设置为 false, 它不会再去 mChangedScrap 查找ViewHolder,也就是说,当我们通常使用 notifyItemChangge() 一个参数的方法之后,它使用的不是同一个 ViewHolder。
到底是哪里错了呢?其实都没错,上面说的是使用 notifyItemChangge(position) 一个参数的方法时的情况
2.3.1 局部刷新时,我们使用的可是两个参数的方法。
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
// 如果不需要更新 放到 mAttachedScrap 中
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
// 没有更新 或者 可以被复用
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
...
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
// 需要更新 放到 mChangedScrap 中
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
//RecyclerView
boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
viewHolder.getUnmodifiedPayloads());
}
//ItemAnimator
@Override
public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
@NonNull List<Object> payloads) {
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
}
针对第二步中,调用的 scrapView() 方法我们再看一次,我们可以看到,在 if 判断中,如果 holder 确实需要被更新,那么它也可能被添加到 mAttachedScrap 集合中,只要 canReuseUpdatedViewHolder(holder) 这个方法能返回 true 。
当我们使用局部刷新,payload 不为空 ,这个时候,如果 ViewHolder 需要更新,它的 更新标识 的确会被加入,但是同时canReuseUpdatedViewHolder() 也会返回 true ,所以,这个时候 ViewHolder 不会被添加到 mChangedScrap 集合中,而是加入 mAttachedScrap 集合中, 最终保证了使用同一个ViewHolder
三、结论
- 当你使用局部刷新
notifyItemChanged(int position, @Nullable Object payload)
时,前后都是同一个 ViewHolder ,如果位置没有变化,就不会执行动画效果; - 而当你不使用局部刷新时
notifyItemChanged(int position)
,使用的不是同一个 ViewHolder ,不管位置是否变化,都会执行相关动画,所以你看到的 itemView 会闪烁一下。 - 当我们多次调用 notifyItemChange() 方法时,也不会多次触发 requestLayout() 和回调 bindViewHolder()
参考文献