Android RecyclerView 局部刷新分析

JohnAndrea 发布于1年前

前情回顾

Android RecycleView轻松实现下拉刷新、加载更多

Android RecyclerView 定制单选多选模式

SwipeRefreshLayout 在 RecyclerView 空白时下拉失效分析

之前写的 PowerAdapter 和 SelectPowerAdapter 从创建到现在,已经两年多,期间发生了翻天覆地的变化。一开始,我把 SwipeRefreshLayout 和 RecyclerView 直接耦合在一起,布局就写一个控件,爽。因为那会儿业务场景是那样,没有考虑灵活性。后来改业务头部不能直接用 SwipeRefreshLayout ,突然才意识到这样局限性太大。于是乎 Android 打造专属的下拉刷新 加载更多 就这么出现,的确也有实际的应用场景。甚至还狂热地做了 那会 UC 浏览器下拉刷新的效果

。在这之后,我将头部拆分开, PowerAdapter 什么的就用于简化实现加载更多已经多布局填充。 SelectPowerAdapter 继承自 PowerAdapter ,写的及其简陋。用于实现简单的单选和多选。

更新

仓库请戳 PowerRecyclerView

这俩 Adapter 就是对于已有 Adapter 功能的装饰,方便调用者实现一些常用功能。再这次之前,陆陆续续已经丰富了一些功能,比如说 SelectAdaper 中以前真是很简陋,现在慢慢已经能实现 单选 多选 反选 选中删除 限制最大选中数量 等基本功能。 PowerAdapter 则将 增删改查等基本功能完善。

因为没有相关规范,一些方法那会儿很随意。现在一些方法被废除,比如说之前写的 adapter.attachRecyclerView() ,为什么要废除呢,因为 adapter 直接就有 onAttachedToRecyclerView() 的方法,所以,根本就不需要添加这个方法。那会疏(cai)忽

(ji)和想当然就直接加上。

还有最重要就是加入了局部刷新这个好东西,一开始是没有考虑到这个地方,后面看文档,才发现这么好一功能差点儿被遗忘。怎么理解局部刷新和使用,将是接下来文章的重点。

我们知道, RecyclerView 中已经添加了 notifyItemChange() notifyItemRemove() 等等单个条目更改的方法,大方向说,这个相对于 ListView 或者 notifyDataChange() 方法 , 它已经算是做到局部刷新。小方向再说,这些添加的刷新方法,其实默认都是带有动画效果,具体效果是 DefaultItemAnimator 来控制和处理,就是因为动画效果,让开发时会出现一些意料之外的状况。

假设我们现在有一个上传照片的场景,我们每一个 ViewHolder 带有一张图片,然后上面有一个进度条,进度根据上传进度实时返回。如果你使用 notifyItemChange() 来更新动画的话,那么会有两个问题: 第一,你会发现每刷新一次,整个布局都会闪动一下。第二,这个进度的数值我需要怎么传递才好呢?在 ViewHolder 的 Bean 对象中添加一个 progress 临时字段?

针对上面两个问题,我其实还额外想问一个问题,也算前置问题。 如果我们多次调用 notifyItemChange() 方法,条目会刷新多次吗?

另外针对局部刷新,还有两个问题, 第一, 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);
    }

好了,这就是 RecyclerView 局部刷新相关 API 的差异。其实对于第一个额外问题(如果我们多次调用 notifyItemChange() 方法,条目会刷新多次吗?),从上面两个方法中,我们就能猜到一些答案,多次调用应该只会回调刷新一次,你看传入的 payload 是一个 Object ,但是到 onBindViewHolder() 方法时参数却成了一个集合,那应该就是有合并的操作。另外再从性能上说,连着 notify 多次,就重新 measure layout 多次的话,这个开销也是很大并且没有必要( RecyclerView 严格控制 requestLayout() 方法调用),真没必要。结果真是这样吗,直接看相关源码。

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

在调用 notifyItemChange() 方法(不管一个参数还是两个个参数)之后,最后都会走到 notifyItemRangeChanged(int positionStart, int itemCount,@Nullable Object payload) ,最后回调到 RecyclerViewDataObserver. onItemRangeChanged() 方法。在该方法中,有一个 triggerUpdateProcessor() 方法,它本质上说,就是去请求重新布局。那就是说,这里只有 if 条件成立,才会去 requestLayout() ,接下来,搞清楚什么时候 if 成立,就能回答这个前置问题。

/**
 * @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;
}

到这里,两个发现: 第一, size==1 说明就第一次调用是才返回 true 才触发 requestLayout() ;第二, payload 参数在这里被包装为对象,放入 mPendingUpdates 这个集合中。 第一个发现,彻底证明上诉猜测是正确的,即使你调用 notify 多次,其实只有第一次会触发 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 或者有一个精确值,那么执行的代码片段就是这样。接着再看看 onLayout() 。

@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 信息,执行动画,最后做一些清除重置操作。

道理我都懂,但是还是过不好这一生。这是另外一个极简翻译

由于篇(neng)幅(li)有(bu)限(xing),接下来对 step 方法只做本文相关及局部刷新相关代码的解析(只关心上面提到的几个问题), RecyclerView 代码太 TM 多了,一次是啃不完,搞不好一辈子可能也啃不完。

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 同步。这个在后面的步骤中也需要使用。第三点,保存需要更新的 ViewHolder 到 oldChangeHolder 集合中。第四点,调用 LayoutManager. onLayoutChildren() 。第五点,保存相关动画信息。

脑子不能乱,我们现在就关心三个问题,第一个, payload 参数怎么传递到 ViewHolder 中;第二个,动画效果是否和 payload 有关系;第三个 ViewHolder 刷新时到底是不是同一个对象。

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

上面几个方法,涉及到 RecyclerView 和 Adapter 互动,首先执行 mAdapterHelper.preProcess() 后,会将刚刚上文说到的 onItemRangeChanged() 方法中的 payload 包装成 UpdateOp 对象,到这里,要开始处理这个对象。

UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
        this.cmd = cmd;
        this.positionStart = positionStart;
        this.itemCount = itemCount;
        this.payload = payload;
    }

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 加上 FLAG_UPDATE 标识,请先记住,这个标识很重要。然后,关键问题之一来了, payload 通过 addChangePayload() 方法直接加到对应 holder 中。**一个核心问题得到解决。

接着回到 processAdapterUpdatesAndSetAnimationFlags() 后部分,设置 mRunSimpleAnimations 和 mRunPredictiveAnimations 两个标识为 true 。再返回 dispatchLayoutStep1() 方法中第三点。

if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                && !holder.shouldIgnore() && !holder.isInvalid()) {
            //3.如果holder确定要更新,就把它添加到 oldChangeHolders 集合中
            long key = getChangedHolderKey(holder);
            mViewInfoStore.addToOldChangeHolders(key, holder);
        }

上面说了,** holder 如果需要被更新,那么 FLAG_UPDATE 就会被添加,然后 holder.isUpdated() 方法就会返回 true 。所以第三点条件符合,就会执行。**

接着是第四点:

if (mState.mRunPredictiveAnimations) {
    ...
    //4.很重要,LayoutManager 开始工作
    mLayout.onLayoutChildren(mRecycler, mState);

在 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 的核心之一,单独开一篇文章讲缓存复用 Recycler 机制都不过分,这里我们关心局部刷新相关就好。

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 创建。

到这里, dispatchLayoutStep1() 方法差不多结束。

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 ( tryGetViewHolderForPositionByDeadline() 方法中第 0 步操作将不在执行,所以 旧的 ViewHolder 无法被复用,它会从下面步骤中获取 ViewHolder 返回。也就是说,当我们通常使用 notifyItemChangge(pisition) 一个参数的方法之后,刷新时它使用的不是同一个 ViewHolder 。

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 的释放。

到这里,第三步分析基本完成。再回头看之前提出的局部刷新的两个问题:

第一, notifyItemChange() 和 局部刷新 ViewHolder 是同一个对象吗?第二,局部刷新是没有设置动画效果吗?

基于上面第三步分析的结论, 如果 oldHolder 和 newHolder 相等,并且偏移量为零,那么不会添加和执行相关动画效果 ,再结合实际情况,我们不妨大胆猜测第一个问题的答案,它们使用的是同一个 ViewHolder 对象。接着针对第二个问题,如果使用同一个对象,并且偏移值为 0 ,那么就不会执行相关动画效果。

但是,这个结论似乎和我们分析第二步时得出的结论有出入: 而我们之前修改的 ViewHolder 是被添加到 mChangedScrap 集合中,但是因为 mInPreLayout 此时设置为 false , 它不会再去 mChangedScrap 查找 ViewHolder ( tryGetViewHolderForPositionByDeadline() 方法中第 0 步操作,所以 旧的 ViewHolder 无法被复用,它会从下面步骤中获取 ViewHolder 返回。也就是说,当我们通常使用 notifyItemChangge() 一个参数的方法之后,它使用的不是同一个 ViewHolder 。

到底是哪里错了呢?其实都没错,上面说的是使用 notifyItemChangge(position) 一个参数的方法时的情况,完全正确。局部刷新时,我们使用的可是两个参数的方法。

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

针对第二步中,调用的 scrapView() 方法我们再看一次,我们可以看到,在 if 判断中,如果 holder 确实需要被更新,那么它也可能被添加到 mAttachedScrap 集合中,只要 anReuseUpdatedViewHolder(holder) 这个方法能返回 true 。

//RecyclerView
boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
    return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
            viewHolder.getUnmodifiedPayloads());
}

RecyclerView 最后会调用 ItemAnimator 中的 canReuseUpdatedViewHolder() 方法,我们看看具体实现:

@Override
public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
        @NonNull List<Object> payloads) {
    return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
}

所噶,当我们使用局部刷新,payload 不为空 ,这个时候,如果 ViewHolder 需要更新,它的 更新标识 的确会被加入,但是同时canReuseUpdatedViewHolder() 也会返回 true ,所以,这个时候 ViewHolder 不会被添加到 mChangedScrap 集合中,而是加入 mAttachedScrap 集合中,真是程序员的小巧思。

所以,当你使用局部刷新时,前后都是同一个 ViewHolder ,如果位置没有变化,就不会执行动画效果;而当你不使用局部刷新时,使用的不是同一个 ViewHolder ,不管位置是否变化,都会执行相关动画,所以你看到的 itemView 会闪烁一下。当我们多次调用 notifyItemChange() 方法时,也不会多次触发 requestLayout() 和回调 bindViewHolder()

到此,上面提到的疑问都解决,至于提到那个场景,就可以使用 局部刷新 来处理,首先不会再有闪烁,那是在执行动画,其次,是那个传值问题,完全就可以使用 payload 参数来传递。每次拿取出 payloads 集合中最后一个值(最新的进度),然后更新到进度条,这个设计也算得上程序员的小巧思嘛(狗头)。

查看原文: Android RecyclerView 局部刷新分析

  • purplekoala
  • orangekoala
  • 徐星星
  • bigdog
  • redlion
  • yellowbird
  • yellowpanda
  • JudithArthur
  • SapirHugh