RecyclerView 浅析

205 阅读24分钟

回收、复用、预加载

当 RecyclerView 上下滑动时那些移出屏幕的 ViewHolder 会进行回收,新进入屏幕的 ViewHolder 会进行复用(如果有缓存),那么对于回收复用的源码分析就要从触摸方法中开始找切入点,先看一下 onTouchEvent 方法中处理滑动的调用流程:

RecyclerView.java

public boolean onTouchEvent(MotionEvent e) {
    // ...
    // mLayout 即为 LayoutManager 可以看出不设置 LayoutManager 是无法触发滑动的
    if (mLayout == null) {
        return false;
    }
    // ...
    switch (action) {
        case MotionEvent.ACTION_DOWN: {}
        case MotionEvent.ACTION_MOVE: {
            // ...
            // 根据 Down 事件中保存的信息计算出滑动距离
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;
            // 滑动前先进行方向判断
            if (mScrollState != SCROLL_STATE_DRAGGING) {
                boolean startScroll = false;
                // ...
                if (canScrollVertically) { // 是否可以垂直滑动
                    if (dy > 0) {
                        dy = Math.max(0, dy - mTouchSlop);
                    } else {
                        dy = Math.min(0, dy + mTouchSlop);
                    }
                    if (dy != 0) {
                        startScroll = true;
                    }
                }
                if (startScroll) { // 标记开始滑动
                    setScrollState(SCROLL_STATE_DRAGGING);
                }
            }
            
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                // ...
                // scrollByInternal 是处理滑动的方法 !!!
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        e, TYPE_TOUCH)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                // ...
            }
        }
        // ...
}

// scrollByInternal 源码部分
boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
    // ...
    if (mAdapter != null) {
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        // scrollStep 方法中计算出了滑动消耗的距离,并且赋值给了 mReusableIntPair
        scrollStep(x, y, mReusableIntPair);
        consumedX = mReusableIntPair[0];
        consumedY = mReusableIntPair[1];
        unconsumedX = x - consumedX;
        unconsumedY = y - consumedY;
    }
    // ...
}

// scrollStep 源码部分
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    // ...
    int consumedX = 0;
    int consumedY = 0;
    // 不管是水平还是垂直滑动 最终都调用到了 LayoutManager 的 scrollXXXBy 方法中
    if (dx != 0) {
        consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
        consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }
    //...
    // 赋值
    if (consumed != null) {
        consumed[0] = consumedX;
        consumed[1] = consumedY;
    }
}

从上面源码部分中可以看出,RecyclerView 的滑动最终是交给 LayoutManager 去处理,我们以 LinearLayoutManager 为例继续跟踪源码:

LinearLayoutManager.java

public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
        RecyclerView.State state) {
    // ...
    return scrollBy(dx, recycler, state);
}

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
        RecyclerView.State state) {
    // ...
    return scrollBy(dy, recycler, state);
}

不论是水平还是垂直滑动都调用到了 scrollBy 方法,继续跟踪源码:

LinearLayoutManager.java

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ...
    // 注意这里这个 fill 方法
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    // ...
    return scrolled;
}

fill 是填充的意思,当发生滑动时,就需要有新的 View 来填充滑动的部分,来看一下 fill 的源码:

LinearLayoutManager.java

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    final int start = layoutState.mAvailable;
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // ...
        // 回收 
        recycleByLayoutState(recycler, layoutState);
    }
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        // ...
        // 复用
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        // ...
    }
}

在 RecyclerView 滑动时回收和复用操作会同时触发,有移除屏幕的 ViewHolder 就需要有新的 ViewHolder 来填补,这个不难理解。

下面来分别看下回收和复用的方法调用流程。

recycleByLayoutState(recycler, layoutState);

LinearLayoutManager.java

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    // ...
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
        // 回收头部 View
        recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
    } else {
        // 回收尾部 View 两个方法逻辑都一样
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}

// recycleViewsFromEnd 和 recycleViewsFromStart 逻辑整体一样 就只贴一个了
private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset,
        int noRecycleSpace) {
    final int childCount = getChildCount();
    // ...
    // 是否倒置 (LinearLayoutManager 构造器中可以设置)
    // 无论 if 还是 else 最终都调用到了 recycleChildren
    if (mShouldReverseLayout) {
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedStart(child) < limit
                    || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
                // 根据 i 来回收 View
                recycleChildren(recycler, 0, i);
                return;
            }
        }
    } else {
        for (int i = childCount - 1; i >= 0; i--) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedStart(child) < limit
                    || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
                // 根据 i 来回收 View
                recycleChildren(recycler, childCount - 1, i);
                return;
            }
        }
    }
}

private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
    // ...
    // 最终调用到了 removeAndRecycleViewAt 方法
    if (endIndex > startIndex) {
        for (int i = endIndex - 1; i >= startIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    } else {
        for (int i = startIndex; i > endIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    }
}

LayoutManager.java

public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
    final View view = getChildAt(index); //根据 index 找到 View
    removeViewAt(index); // 从 RV 中移除
    recycler.recycleView(view); // 交给 recycler 进行回收
}

layoutChunk(recycler, state, layoutState, layoutChunkResult);

LinearLayoutManager.java

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    // ...
}

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    // 最终通过 recycler 的 getViewForPosition 方法获取到了 View
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

Recycler

从上述的源码调用流程中可以看出,回收复用的具体逻辑都放在了 Recycler 中,Recycler 和 LayoutManager 一样是 RecyclerView 的内部类,先来看下 Recycler 中的一些定义:

RecyclerView.java

public final class Recycler {
    // 一级缓存
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null;
    
    // 二级缓存
    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

    // 三级缓存
    private ViewCacheExtension mViewCacheExtension;

    // 四级缓存
    RecycledViewPool mRecyclerPool;
    // ...
}

在这里先对这些定义有个大致的了解:

  1. mAttachedScrap、mChangedScrap 屏幕上的 ViewHolder 缓存;
  2. mCachedViews 刚刚移除屏幕的 ViewHolder 会加入进来,默认容量为 2,超出容量后最先放入的会移出,并且加入 mRecyclerPool 缓存池中;
  3. mViewCacheExtension 供开发者自定义的缓存,一般不实现(在回收源码流程中并没有使用);
  4. mRecyclerPool 缓存池;

RecycledViewPool

mAttachedScrap、mCachedViews 就是简单的 ArrayList,存储 ViewHolder,RecycledViewPool 就需要查看源码来了解下了:

RecycledView.java

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;
    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP; // mScrapHeap 的容量 默认为 5
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
    // 缓存池容器
    SparseArray<ScrapData> mScrap = new SparseArray<>();
    // ...
}

可以看出 RecycledViewPool 内部的容器为 SparseArray,存储类型是其内部类 ScrapData,而 ScrapData 内部使用 ArrayList 来存储 ViewHolder。当 RecyclerView 多类型 item 进行缓存时,直接以 itemViewType 作为 key,ScrapData 作为 value 对 ViewHolder 进行缓存,默认情况下每种不同的 ViewHolder 可以分别缓存 5 个。

回收

回收源码从 Recycler.recycleView(view) 直接看起:

RecyclerView.java

public void recycleView(@NonNull View view) {
    // View 的 LayoutParams 为 RecyclerView.LayoutParams 
    // 从其内部可以直接获取到 ViewHolder
    ViewHolder holder = getChildViewHolderInt(view);
    if (holder.isTmpDetached()) {
        // 清除动画资源等 并且触发 adapter 的 onViewDetachedFromWindow 方法
        removeDetachedView(view, false);
    }
    // 如果 ViewHolder 存在于 mAttachedScrap 或者 mChangedScrap 中就移除、清除相关 flag
    if (holder.isScrap()) {
        holder.unScrap();
    } else if (holder.wasReturnedFromScrap()) {
        holder.clearReturnedFromScrapFlag();
    }
    // 回收核心方法
    recycleViewHolderInternal(holder);
    // 停止动画
    if (mItemAnimator != null && !holder.isRecyclable()) {
        mItemAnimator.endAnimation(holder);
    }
}

void recycleViewHolderInternal(ViewHolder holder) {
    // ...
    final boolean transientStatePreventsRecycling = holder
            .doesTransientStatePreventRecycling();
    // 这里提一下 可以重写 adapter 的 onFailedToRecycleView 方法返回 ture 来达到强制回收 ViewHolder
    final boolean forceRecycle = mAdapter != null
            && transientStatePreventsRecycling
            && mAdapter.onFailedToRecycleView(holder); 
    boolean cached = false;
    boolean recycled = false;
    // ...
    // 如果可以回收
    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // 先获取 mCachedViews 的 size
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // 超出容量后移除头部的 item
                // 注意这个方法 !!! 内部还将移除的 ViewHolder 加入到了 RecyclerViewPool 中
                recycleCachedViewAt(0);
                cachedViewSize--;
            }
    
            int targetCacheIndex = cachedViewSize;
            // 这个 if 中是预加载机制相关代码 后面解释
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                // when adding the view, skip past most recently prefetched views
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                targetCacheIndex = cacheIndex + 1;
            }
            // 添加到 mCachedViews 缓存中
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            // 如果没有添加到 mCachedView 中则直接加入 RecycledViewPool 中
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        // ...
        // 不能回收就什么也不做
    }
    // 清除保存的 ViewHolder 动画相关的信息
    mViewInfoStore.removeViewHolder(holder);
    if (!cached && !recycled && transientStatePreventsRecycling) {
        holder.mBindingAdapter = null;
        holder.mOwnerRecyclerView = null;
    }
}

void recycleCachedViewAt(int cachedViewIndex) {
    // 根据 index 获取到要从 mCachedViews 中移除的 ViewHolder
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
    // 添加到 RecycledViewPool
    addViewHolderToRecycledViewPool(viewHolder, true);
    // 从 mCachedViews 移除
    mCachedViews.remove(cachedViewIndex);
}

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
    View itemView = holder.itemView;
    // ...
    if (dispatchRecycled) {
        // 触发 adapter 的 onViewRecycled 方法
        dispatchViewRecycled(holder);
    }
    // ...
    // 添加到缓存池
    getRecycledViewPool().putRecycledView(holder);
}

public void putRecycledView(ViewHolder scrap) {
    // 根据 itemViewType 获取缓存池中对应的 ArrayList
    // 没有的话会 new 一个 ScrapData 添加到 mScrap 中
    final int viewType = scrap.getItemViewType();
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
    // 判断是否超出容量
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
        return;
    }
    // ...
    // 清除 ViewHolder 的状态信息
    scrap.resetInternal();
    // 添加到缓存池
    scrapHeap.add(scrap);
}

注释中有详细的解释,在这里就简单总结下整体回收的流程:

  1. 如果要回收的 ViewHolder 存在于一级缓存 mAttachedScrap、mChangedScrap 中则从中移除;
  2. 将 ViewHolder 加入二级缓存 mCacheViews 中,如果容量超出则移除 index 为 0 的 ViewHolder,并且将其添加到 RecyclerViewPool 中;
  3. 如果 ViewHolder 在第 2 步中没有成功添加到 mCacheViews 中则直接添加进 RecyclerViewPool 缓存;
  4. RecyclerViewPool 中会根据 ItemViewType 获取对应 ViewHolder 的 ArrayList 容器(如果还没初始化则直接 new 一个)判断容量后添加进去,添加前会清除 ViewHolder 的一些状态信息;

复用

熟悉了回收流程后趁热打铁来看一下复用的源码流程,从 Recycler.getViewForPosition(position) 直接看起:

RecyclerView.java

public View getViewForPosition(int position) {
    // 调用了重载方法
    return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
    // 调用了 tryGetViewHolderForPositionByDeadline 获取到 ViewHolder
    // 将 ViewHolder 的 itemView 返回
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

// 复用机制的核心源码
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    // ...
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 如果是预布局状态通过一级缓存 mChangedScrap 中查找
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 二级缓存 内部会先从 mAttachedScrap 中尝试获取,获取不到则尝试从 mCacheViews 中寻找
    // 注意这里是通过 position 寻找
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        // ...
    }
    // 二级缓存 跟上面的区别是这次根据 mHasStableIds 寻找
    // adapter 重写 getItemId 方法设置 id
    if (holder == null) {
        // ...
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            // ...
        }
        
        // 三级缓存 自定义缓存
        if (holder == null && mViewCacheExtension != null) {
            // 注意这里返回的是 View
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                // 通过 RecyclerView.LayoutParams 获取其 ViewHolder
                holder = getChildViewHolder(view);
                // ...
            }
        }
        
        // 四级缓存
        if (holder == null) { // fallback to pool
            holder = getRecycledViewPool().getRecycledView(type);
            // ...
        }
        
        // 四级缓存都没有获取到
        if (holder == null) {
            // ...
            // 通过 adapter onCreateViewHolder 方法创建 ViewHolder
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            // 预加载机制相关代码 后面解释
            if (ALLOW_THREAD_GAP_WORK) {
                // only bother finding nested RV if prefetching
                RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                if (innerView != null) {
                    holder.mNestedRecyclerView = new WeakReference<>(innerView);
                }
            }
            // ...
            }
        }
    }
    // ...
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        // 如果没有绑定过数据 则调用 adapter onBindViewHolder 进行绑定
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    
    // 给 ViewHolder 的 itemView 设置 LayoutParams
    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    // 这里的 LayoutParams 是 RecyclerView.LayoutParams
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        // LayoutParams 由 LayoutManager 返回
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }
    rvLayoutParams.mViewHolder = holder; // 将 ViewHolder 赋值给 LayoutParams 
    rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
    return holder;
}

总结下,源码中四级缓存的方法分别为:

  1. 一级缓存 getChangedScrapViewForPosition,根据 position 从 mChangedScrap 中获取;
  2. 二级缓存 getScrapOrHiddenOrCachedHolderForPosition、getScrapOrCachedViewForId,优先根据 position 从 mAttachScrap、mCacheViews 中获取,其次根据 itemId 再次从 mAttachScrap、mCacheViews 中获取;
  3. 三级缓存 mViewCacheExtension.getViewForPositionAndType,自定义缓存,一定注意这里返回的是 View;
  4. 四级缓存 getRecycledViewPool().getRecycledView,根据 itemViewType 获取对应 ViewHolder 的缓存 ArrayList,再从其中尝试获取。

每级缓存对应的方法源码比较易懂,篇幅原因就不挨着贴出来了。

预加载

image.png

预加载是 SDK 21 后对 RecyclerView 的一项优化。

在了解与加载机制之前,需要先简单了解下 Android 开启硬件加速后的 UI 渲染流程:

image.png

  1. 当 VSync 信号到来后 UI Thread 处理输入事件、动效、测量、布局、绘制等等,将这些封装成 Render Node 同步给 Render Thread; 2. Render Thread 会将这些绘制指令转为 OpenGL 指令,最终通过 GPU 绘制到 Graphic Buffer; 3. SurfaceFlinger 拿到 buffer 后进行 layer 合并,最终将数据交给 HAL 层绘制到设备屏幕上; 4. 当下一次 VSync 信号到来后重复上述流程;

UI Thread 将 Render Node 同步给 Render Thread 后等待的过程就是预加载要利用的空隙:

image.png

原理大概就是这样,具体来看一下源码中是如何实现:

RecyclerView.java

protected void onAttachedToWindow() {
    // ...
    // SDK >= 21
    if (ALLOW_THREAD_GAP_WORK) {
        // sGapWorker 是一个 ThreadLocal
        mGapWorker = GapWorker.sGapWorker.get();
        if (mGapWorker == null) { //初始化 GapWorker
            mGapWorker = new GapWorker();
            // ...
            GapWorker.sGapWorker.set(mGapWorker);
        }
        mGapWorker.add(this);
    }
}

GapWorker 实现了 Runnable 接口,GapWorker 的使用在 onTouchEvent 的 MOVE 事件中:

RecyclerView.java

public boolean onTouchEvent(MotionEvent e) {
    // ...
    switch (action) {
        // ...
        case MotionEvent.ACTION_MOVE: {
                // ...
            if (mGapWorker != null && (dx != 0 || dy != 0)) {
                mGapWorker.postFromTraversal(this, dx, dy);
            }
        }
}

来看一下 postFromTraversal 源码:

GapWorker.java

void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
    if (recyclerView.isAttachedToWindow()) {
        // ...
        if (mPostTimeNs == 0) {
            mPostTimeNs = recyclerView.getNanoTime();
            // GapWorker 实现了 Runnable
            // 通过 View.post 执行 run 方法
            recyclerView.post(this);
        }
    }
    // ...
}

继续查看 GapWorker 的 run 方法实现:

GapWorker.java

public void run() {
    try {
        // Trace
        Trace.beginSection(RecyclerView.TRACE_PREFETCH_TAG);

        long lastFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos(
                mRecyclerViews.get(0).getDrawingTime());
        if (lastFrameVsyncNs == 0) {
            return;
        }
        // 获取下次 VSync 信号到来的时间
        long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs;
        // 尝试预加载
        prefetch(nextFrameNs);
    }
    // ...
}

void prefetch(long deadlineNs) {
    buildTaskList(); // 构建任务
    flushTasksWithDeadline(deadlineNs); // 执行
}

private void flushTasksWithDeadline(long deadlineNs) {
    for (int i = 0; i < mTasks.size(); i++) {
        final Task task = mTasks.get(i);
        if (task.view == null) {
            break; // done with populated tasks
        }
        // 循环执行
        flushTaskWithDeadline(task, deadlineNs);
        task.clear();
    }
}

private void flushTaskWithDeadline(Task task, long deadlineNs) {
    long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
    // prefetchPositionWithDeadline 方法中进行了预加载
    RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
            task.position, taskDeadlineNs);
    // ...
    }
}

private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
        int position, long deadlineNs) {
    // ...
    RecyclerView.Recycler recycler = view.mRecycler;
    // 这里又调用到了 tryGetViewHolderForPositionByDeadline
    RecyclerView.ViewHolder holder = recycler.tryGetViewHolderForPositionByDeadline(
            position, false, deadlineNs);
    
    // 预加载创建 View 成功之后
    if (holder != null) {
        if (holder.isBound()) { // 绑定数据成功 放入 mCacheViews 中
            recycler.recycleView(holder.itemView);
        } else { // 如果仅仅创建成功 没绑定数据 就放入 RecyclerViewPool 中
            recycler.addViewHolderToRecycledViewPool(holder, false);
        }
    }
    return holder;
}

再次回到 方法中:

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    if (holder == null) {
        long start = getNanoTime();
        // 这里是根据 ViewHolder 的平均创建时间判断
        // 判断是否有足够的时间进行预加载操作
        if (deadlineNs != FOREVER_NS
                && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
            // abort - we have a deadline we can't meet
            return null;
        }
        // 时间够 则预加载成功
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
        // ...
    }
    // ...
    // 利用剩余的时间尝试绑定数据
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    // ...
}

可以看出预加载之后对 ViewHolder 的缓存和前面回收复用部分是相符合的。预加载机制一般不需要我们额外编写代码即可实现,这部分内容就当扩展了解吧。

测量、布局、绘制、预布局

onMeasure

RecyclerView.java

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    // 第一种情况:没有设置 LayoutManager
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    // 第二种情况:设置的 LayoutManager 开启自动测量
    if (mLayout.isAutoMeasureEnabled()) {
        // ...
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        // ...
        mLastAutoMeasureSkippedDueToExact =
                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) {
            return;
        }

        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
        }
        
        mLayout.setMeasureSpecs(widthSpec, heightSpec);
        mState.mIsMeasuring = true;
        dispatchLayoutStep2();
        // 需要二次测量
        if (mLayout.shouldMeasureTwice()) {
            // ...
            dispatchLayoutStep2();
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        }
        // ...
    } else {  // 第三种情况:设置的 LayoutManager 没有开启自动测量
        // ...
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        // ...
        mState.mInPreLayout = false; // clear
    }
}

源码只贴出了重要部分,稍微总结下:

  1. 没有设置 LayoutManager 时,调用 defaultOnMeasure 方法;
  2. 设置 LayoutManager 并且开启自动测量时,调用 LayoutManager 的 onMeasure 方法,并且会执行 dispatchLayoutStep1()、dispatchLayoutStep2();
  3. 设置 LayoutManager 且没有开始自动测量时,仅调用了 LayoutManager 的 onMeasure 方法;

先来看一下 LayoutManager 的 onMeasure 方法:

RecyclerView.java

public abstract static class LayoutManager{
    // ...
    public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,int heightSpec) {
        mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
    }
}

默认实现和第一种情况一样,调用了 defaultOnMeasure 方法,而且 sdk 中给我们提供的三种 LayoutManager(LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager)均没有重写 onMeasure 方法。

接着就来看看 defaultOnMeasure 的源码:

RecyclerView.java

void defaultOnMeasure(int widthSpec, int heightSpec) {
    // 通过 LayoutManager.chooseSize 获取的宽和高的值
    final int width = LayoutManager.chooseSize(widthSpec,
            getPaddingLeft() + getPaddingRight(), // 横向内边距
            ViewCompat.getMinimumWidth(this)); // 反射获取是否设置最小宽度
    final int height = LayoutManager.chooseSize(heightSpec,
            getPaddingTop() + getPaddingBottom(), // 纵向内边距
            ViewCompat.getMinimumHeight(this)); // 反射获取是否设置最小宽度
    // 设置宽高
    setMeasuredDimension(width, height);
}

接着看一下 LayoutManager.chooseSize 是如何获取宽高的:

public static int chooseSize(int spec, int desired, int min) {
    final int mode = View.MeasureSpec.getMode(spec);
    final int size = View.MeasureSpec.getSize(spec);
    switch (mode) {
        case View.MeasureSpec.EXACTLY:
            return size;
        case View.MeasureSpec.AT_MOST:
            return Math.min(size, Math.max(desired, min));
        case View.MeasureSpec.UNSPECIFIED:
        default:
            return Math.max(desired, min);
    }
}

这段代码就不用解释了吧?自定义 View 时经常会根据 mode 不同来处理宽高的最终值。

测量这部分到目前为止的代码都比较简单,测量对于宽高这部分并没有特殊处理,剩余重要逻辑都在 dispatchLayoutStep1()、dispatchLayoutStep2() 方法中,这里先不对其进行详细解释,因为下面的 onLayout 中还有一个 dispatchLayoutStep3() 方法。

稍微总结下,测量部分除非有特殊的自定义 LayoutManager 对宽高有自定义需求,一般情况都会走默认的 defaultOnMeasure 方法,和大部分自定义 View 相同根据 mode 确定宽高。

onLayout

自定义 View 的第二大流程 onLayout,直接看源码:

RecyclerView.java

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
    dispatchLayout(); //  只有一处方法调用 分发布局
    TraceCompat.endSection();
    mFirstLayoutComplete = true;
}

void dispatchLayout() {
    // ...
    // 在 onMeasure 中这个值被设置为 true
    mState.mIsMeasuring = false;
    // ...
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates()
            || needsRemeasureDueToExactSkip
            || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3(); // mState.mLayoutStep 的值在里面被设置为 State.STEP_START
}

onLayout 中的逻辑并不复杂,逻辑都放在了 dispatchLayout 中,而 dispatchLayout 中又根据各种判断确保了 dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3 都会执行。

onDraw

RecyclerView 重写了 draw 方法,那么就先看一下 draw 方法:

RecyclerView.java

public void draw(Canvas c) {
    super.draw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    
    // ...
}

draw 方法中获取了所有的 ItemDecoration 也就是“分割线”,调用了其 onDrawOver 方法。

draw 的源码中会继续调用 onDraw 方法,继续看一下 onDraw 方法:

RecyclerView.java

public void onDraw(Canvas c) {
    super.onDraw(c);
    // draw 方法中调用了 ItemDecoration 的 onDrawOver
    // onDraw 方法又调用了 ItemDecoration 的 onDraw 
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

可以看出 draw 和 onDraw 主要对分割线进行了绘制,由于 draw 方法先执行,那么也就意味着 ItemDecoration 的 onDrawOver 方法会先绘制,之后再执行其 onDraw 方法。

dispatchLayoutStep1、2、3

dispatchLayoutStep1

概述:处理 Adapter 更新,决定哪个动画应该被执行,保存当前的视图信息,如果需要的话进行预布局并保存相关信息。

RecyclerView.java

private void dispatchLayoutStep1() {
    // 确认布局步骤 在 dispatchLayoutStep3 会设置为 STEP_START
    // 并且 onLayout 调用 dispatchLayoutStep1 之前也进行了判断
    mState.assertLayoutStep(State.STEP_START);
    // 获取剩余的滚动距离(横竖向)
    fillRemainingScrollValues(mState);
    // onMeasure 中标记为 true 这里再置为 false
    mState.mIsMeasuring = false;
    startInterceptRequestLayout();
    // ViewInfoStore 用于保存动画相关信息
    // 清除保存的信息 
    mViewInfoStore.clear();
    // 标记进入布局或者滚动状态 内部是int类型进行++操作
    onEnterLayoutOrScroll();
    // 适配器更新和动画预处理 设置 mState.mRunSimpleAnimations 和 mState.mRunPredictiveAnimations 的值
    processAdapterUpdatesAndSetAnimationFlags();
    // 保存焦点信息
    saveFocusInfo();
    // 一些信息保存
    mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
    mItemsAddedOrRemoved = mItemsChanged = false;
    // 预布局标志 和 mRunPredictiveAnimation 有关 这里先记住 后面会解释什么是预布局
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
    mState.mItemCount = mAdapter.getItemCount();
    findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
    
    // 下面两个 if 都是动画预处理 保存信息等等
    // mRunSimpleAnimations 可以理解为 需要执行动画
    if (mState.mRunSimpleAnimations) {
        // ...
        int count = mChildHelper.getChildCount();
        for (int i = 0; i < count; ++i) {
            // ...
            // 保存执行动画所需的信息 (预布局时的信息)
            mViewInfoStore.addToPreLayout(holder, animationInfo);
            // ...
        }
    }
    // mRunPredictiveAnimations 可以理解为 需要执行动画的情况下需要进行预布局
    // 换而言之需要拿到动画执行前后的各种信息(坐标等等)
    if (mState.mRunPredictiveAnimations) {
        // ...
        // 这里如果需要预布局就调用 LayoutManager 的 onLayoutChildren 开始布局
        // 注意 mState.mInPreLayout = mRunPredictiveAnimations
        // 当 mRunPredictiveAnimations 为 ture 时 mInPreLayout 同样为 true
        mLayout.onLayoutChildren(mRecycler, mState);
        // ...
    } else {
        clearOldPositions();
    }
    onExitLayoutOrScroll();
    // 和 startInterceptRequestLayout 成对使用 貌似是防止多次 requestLayout
    stopInterceptRequestLayout(false);
    // 标记 STEP_START 完成 可以执行 dispatchLayoutStep2
    mState.mLayoutStep = State.STEP_LAYOUT;
}

dispatchLayoutStep1 整体上都是预布局处理,对动画信息的保存等等,ViewInfoStore 是用于存储 item 动画相关信息,后面的博客中会分析。注意重点,如果需要执行动画将会执行预布局,也就是调用 mLayout.onLayoutChildren 之前 mInPreLayout 为 true。

dispatchLayoutStep2

概述:预布局状态结束,开始真正的布局。

RecyclerView.java

private void dispatchLayoutStep2() {
    startInterceptRequestLayout(); // 和 stopInterceptRequestLayout 成对出现
    onEnterLayoutOrScroll(); // 上面已经说过了
    // 判断 State ,在 dispatchLayoutStep1 已经标记为 STEP_LAYOUT
    mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
    mAdapterHelper.consumeUpdatesInOnePass();
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
    if (mPendingSavedState != null && mAdapter.canRestoreState()) {
        if (mPendingSavedState.mLayoutState != null) {
            mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState);
        }
        mPendingSavedState = null;
    }
    // 预布局标记为 fasle
    mState.mInPreLayout = false;
    // 开始布局 这里是真正的测量和布局items
    mLayout.onLayoutChildren(mRecycler, mState);
    mState.mStructureChanged = false;
    mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
    // 标记 STEP_ANIMATIONS
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false); // 和 startInterceptRequestLayout 成对出现
}

dispatchLayoutStep2 方法代码不多,注意重点,在调用 mLayout.onLayoutChildren(mRecycler, mState) 之前将 mState.mInPreLayout 预布局标记为 false。

到这里可以看出预布局过程就发生在 dispatchLayoutStep1、2 之间。

dispatchLayoutStep3

概述:执行 item 动画以及布局完成后的收尾工作。

RecyclerView.java

private void dispatchLayoutStep3() {
    // 判断状态是否为 STEP_ANIMATIONS
    mState.assertLayoutStep(State.STEP_ANIMATIONS);
    // 和 stopInterceptRequestLayout 成对出现
    startInterceptRequestLayout(); 
    // 和 onExitLayoutOrScroll 成对出现
    onEnterLayoutOrScroll();
    // 标记为 STEP_START, 在步骤 1 中会判断是否为 STEP_START
    mState.mLayoutStep = State.STEP_START;
    if (mState.mRunSimpleAnimations) {
        for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
            ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            // ...
            // 保存动画信息
            // 布局完成后的坐标等等
            mViewInfoStore.addToPostLayout(holder, animationInfo);
            // ...
            }
        }
        // 执行 item 动画
        mViewInfoStore.process(mViewInfoProcessCallback);
    }
    // 清理 mAttachedScrap 和 mChangedScraop 缓存
    mLayout.removeAndRecycleScrapInt(mRecycler);
    // item 数量
    mState.mPreviousLayoutItemCount = mState.mItemCount;
    // 相关标记设为初始值
    mDataSetHasChangedAfterLayout = false;
    mDispatchItemsChangedEvent = false;
    mState.mRunSimpleAnimations = false;
    mState.mRunPredictiveAnimations = false;
    mLayout.mRequestedSimpleAnimations = false;
    // ...
    // 布局完成回调
    mLayout.onLayoutCompleted(mState);
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
    // 清理
    mViewInfoStore.clear();
    // ...
}

mAttachedScrap 和 mChangedScrap

回收部分源码执行时,并没有用到 mAttachedScrap 和 mChangedScrap,复用时却优先在他们俩容器中寻找缓存。现在在布局步骤3 dispatchLayoutStep3 中也对其进行了清空,那么说明在 dispatchLayoutStep3 之前对其肯定有过回收的操作。

布局步骤1、2、3中,大部分逻辑都在 mLayout.onLayoutChildren 中,但其是一个空实现,所以,就以其开发中最常用到的实现类 LinearLayoutManager 源码来分析看看:

LinearLayoutManager.java

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){
    // ...
    // 
    detachAndScrapAttachedViews(recycler);
    // ...
    // fill 在第一篇博客中提到了 填充布局 算是回收复用的入口
    fill(recycler, mLayoutState, state, false);
}

public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
    // 从这里可以看出将所有的可见的 item 都回收到了 mAttachedScrap 或者 mChangedScrap 中
    final int childCount = getChildCount();
    for (int i = childCount - 1; i >= 0; i--) {
        final View v = getChildAt(i);
        scrapOrRecycleView(recycler, i, v);
    }
}

在 onLayoutChildren 中对可见的 item 都进行了回收操作,并且紧接着执行了 fill 进行了填充布局操作。由上述对 dispatchLayout1、2、3 的源码分析可以得知,dispatchLayout3 对 mAttachedScrap 和 mChangedScrap 进行了清空操作,dispatchLayout1 调用 onLayoutChildren 进行预布局操作,而 dispatchLayout2 调用 onLayoutChildren 进行真正的布局操作。

那么显而易见,mAttachedScrap 和 mChangedScrap 是对可见 item 的缓存,目的在于预布局、真正的布局阶段复用,不用重新绑定数据。

预布局

先大概说一下预布局的使用场景,如下图所示:

image.png

假如屏幕中有一个 RecyclerView 且其有三个 item,当删除 item3 时,item4 会递补出现在屏幕内。这是开发中非常常见的情况吧,一般执行删除或者新增操作,我们都会添加动画让其显得不生硬,那么思考下 item4 是什么时候添加到屏幕上的呢?

回到 LinearLayoutManager 的 fill 方法查看源码:

LinearLayoutManager.java

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    //...
    // 可用空间
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    // 当 remainingSpace > 0 会继续循环
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        // ...
        // 布局
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        // 计算可用空间
        // 注意这里的判断条件
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            remainingSpace -= layoutChunkResult.mConsumed;
        }
    }
}

在计算可用空间时,有三个判断条件:

  1. !layoutChunkResult.mIgnoreConsumed
  2. layoutState.mScrapList != null
  3. !state.isPreLayout()

重点看 1 和 3,先说 3 吧,如果是预布局状态,也就是 dispatchLayoutStep1 调用进来时第三个条件是 false。至于条件 1 还需要看一下 layoutChunk 方法源码:

LinearLayoutManager.java

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    // ...
    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
    // ...
    // 源码最后部分有这么一处判断
    // 如果 viewholder 被标记为了移除或者改变 mIgnoreConsumed 设为 true
    if (params.isItemRemoved() || params.isItemChanged()) {
        result.mIgnoreConsumed = true;
    }
    result.mFocusable = view.hasFocusable();
}

看完这段代码再回到上面图示中的场景:

image.png

当 item3 被删除时,在预布局阶段它所占用的空间会忽略不计,那么 fill 方法中在计算可用空间时就会多走一次 while 循环,从而多添加一个 item。

那么 dispatchStep1 即可称之为预布局阶段,此时将要移除的 item3 以及即将添加到屏幕上的 item4 的预布局阶段的位置信息等等保存,在 dispatchStep2 真正布局阶段保存完成删除操作后的位置信息等等,即可在 dispatchStep3 中根据两个信息之间的差异做出对应的 item 动画。

LayoutManager

布局

RecyclerView 将布局这个任务完全交给了 LayoutManager,根据上面的回顾可知布局逻辑在 onLayoutChildren 方法,直接查看下 LinearLayoutManager 的 onLayoutChildren 方法源码:

LinearLayoutManager.java

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
        if (state.getItemCount() == 0) { // 没有 items 就全部回收
            removeAndRecycleAllViews(recycler);
            return;
        }
    }
    // mPendingScrollPosition 设置需要滚动到第几个item
    if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
        mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
    }
    // 初始化 LayoutState ,为 null 则直接 new 出来
    ensureLayoutState();
    // 标记为不回收
    mLayoutState.mRecycle = false;
    // 是否倒序(构造函数中可以设置)
    resolveShouldLayoutReverse();
    // 获取焦点 View
    final View focused = getFocusedChild();
    // 这个 if 整体是计算锚点信息
    if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
            || mPendingSavedState != null) {
        mAnchorInfo.reset();
        // 是否从末尾开始 mStackFromEnd 可以设置
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
        // 计算锚点相关信息
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        mAnchorInfo.mValid = true;
    } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
            >= mOrientationHelper.getEndAfterPadding()
            || mOrientationHelper.getDecoratedEnd(focused)
            <= mOrientationHelper.getStartAfterPadding())) {
        // 这个 else if 为了处理软键盘弹出压缩布局后的情况 我没有细研究
        mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
    }
    
    // 根据滑动值判断布局方向
    mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
            ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    mReusableIntPair[0] = 0;
    mReusableIntPair[1] = 0;
    // 计算额外的布局空间 也就是预布局的情况下 需要额外计算
    calculateExtraLayoutSpace(state, mReusableIntPair);
    int extraForStart = Math.max(0, mReusableIntPair[0])
            + mOrientationHelper.getStartAfterPadding();
    int extraForEnd = Math.max(0, mReusableIntPair[1])
            + mOrientationHelper.getEndPadding();
    // 看第一个判断条件也就知道了 是处理预布局的
    if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
            && mPendingScrollPositionOffset != INVALID_OFFSET) {
        final View existing = findViewByPosition(mPendingScrollPosition);
        if (existing != null) {
            final int current;
            final int upcomingOffset;
            // ...
            // 最后计算出了两个方向的 额外布局空间
            if (upcomingOffset > 0) {
                extraForStart += upcomingOffset;
            } else {
                extraForEnd -= upcomingOffset;
            }
        }
    }
    // ...
    // 锚点计算完成回调
    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    // 回收屏幕上可见 item 到 scrap 中
    detachAndScrapAttachedViews(recycler);
    // ...
    if (mAnchorInfo.mLayoutFromEnd) { // 从末尾开始布局
        // 更新锚点信息 AnchorInfo 对象中存储着锚点的位置、偏移、方向等等
        updateLayoutStateToFillStart(mAnchorInfo);
        // 设置预布局计算出的额外填充空间
        mLayoutState.mExtraFillSpace = extraForStart;
        // fill 方法填充
        fill(recycler, mLayoutState, state, false);
        // ...
        // 再次更新锚点信息 和上次不同 上次上 锚点向 start 方向,这次上 锚点向 end 方向
        updateLayoutStateToFillEnd(mAnchorInfo);
        // 设置与布局 end 方向的 额外填充空间
        mLayoutState.mExtraFillSpace = extraForEnd;
        // fill 方法中填充
        fill(recycler, mLayoutState, state, false);
        // ...
    } else { // 从头部开始布局
        // 和上面 if 反过来 先更新 end 方向锚点信息
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtraFillSpace = extraForEnd;
        // end 方向布局
        fill(recycler, mLayoutState, state, false);
        // ...
        // 下面是向 start 方向布局的逻辑
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtraFillSpace = extraForStart;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        fill(recycler, mLayoutState, state, false);
        // ...
    }
    
    if (!state.isPreLayout()) { // 不是预布局 则布局完成回调
        mOrientationHelper.onLayoutComplete();
    } else { // 预布局则重置锚点信息
        mAnchorInfo.reset();
    }
    // 保存这次布局是否从底部填充
    mLastStackFromEnd = mStackFromEnd;
}

实际添加 View 以及布局重点逻辑都在 fill 方法中,在之前的博客中以及提到过:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    //...
    // 可用空间
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    // 当 remainingSpace > 0 会继续循环
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        // ...
        // 布局
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        // 计算可用空间
        // 注意这里的判断条件
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            remainingSpace -= layoutChunkResult.mConsumed;
        }
    }
}

layoutChunk 是获取、添加 View 的方法:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    // 获取 View 在第一篇有详细说这个方法
    View view = layoutState.next(recycler);
    // ...
    // view 的 LayoutParams 中存储着 ViewHolder
    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
    if (layoutState.mScrapList == null) {
        // 添加 View
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addView(view);
        } else {
            addView(view, 0);
        }
    }
    // ...
    // 测量 view
    measureChildWithMargins(view, 0, 0);
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    // 根据方向计算 view 的位置
    int left, top, right, bottom;
    if (mOrientation == VERTICAL) {
        if (isLayoutRTL()) {
            right = getWidth() - getPaddingRight();
            left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
        } else {
            left = getPaddingLeft();
            right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
        }
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            bottom = layoutState.mOffset;
            top = layoutState.mOffset - result.mConsumed;
        } else {
            top = layoutState.mOffset;
            bottom = layoutState.mOffset + result.mConsumed;
        }
    }
    // ...
    // 摆放 View
    layoutDecoratedWithMargins(view, left, top, right, bottom);
    // 如果即将被移除 则标记忽略 不占用可用空间
    if (params.isItemRemoved() || params.isItemChanged()) {
        result.mIgnoreConsumed = true;
    }
    result.mFocusable = view.hasFocusable();
}

锚点

image.png

如图所示,绿色的位置为锚点,共分为三种情况。第三种情况上下滑动是最常见的情况。在上述源码中进行填充的 if else 中不论进入哪个判断都会有两次 fill,就如图中第三种情况所示,会根据锚点的位置结合 stackFromEnd 的值先后进行两次填充。

下面来进入源码来了解一下 mAnchorInfo:

final AnchorInfo mAnchorInfo = new AnchorInfo();

在 LinearLayoutManager 中定义时就完成了初始化,看下其源码:

static class AnchorInfo {
    OrientationHelper mOrientationHelper; // 辅助类获取itemView的相关信息
    int mPosition; // 锚点的位置 也就是对应的 itemView 在 rv 中的索引
    int mCoordinate; // 偏移量
    boolean mLayoutFromEnd; // 是否从末尾开始填充
    boolean mValid; // 是否有效

    AnchorInfo() { // 构造函数直接调用了 reset 也就知道这些变量的初始值了
        reset();
    }
    
    void reset() {
        mPosition = RecyclerView.NO_POSITION;
        mCoordinate = INVALID_OFFSET;
        mLayoutFromEnd = false;
        mValid = false;
    }
    //...
}

LayoutState

在 onLayoutChildren 的源码中也多次使用了 LayoutState 中的变量,直接看一下源码:

static class LayoutState {
    // ...
    // 是否要回收
    boolean mRecycle = true;
    // 偏移量
    int mOffset;
    // 要填充的空间值(像素)
    int mAvailable;
    // 当前位置
    int mCurrentPosition;
    // 适配器遍历方向
    int mItemDirection;
    // 布局填充方向
    int mLayoutDirection;
    // 滚动的偏移量
    int mScrollingOffset;
    // 预布局需要额外填充的空间大小
    int mExtraFillSpace = 0;
    int mNoRecycleSpace = 0;
    // 预布局标记
    boolean mIsPreLayout = false;
    // 上一次滚动的距离
    int mLastScrollDelta;
    // 我查看了他的赋值 其实就是 Relcycer 的 mAttachScrap
    List<RecyclerView.ViewHolder> mScrapList = null;
    boolean mInfinite;
    // ...
}

可以看出 LayoutState 就是一个布局状态类,将一些关键的布局方向、偏移量等等作为成员变量。

滑动

RecyclerView 的滑动是交给 LayoutManager 的 scrollHorizontallyBy 和 scrollVerticallyBy 两个方法执行,分别处理水平、垂直方向的滑动,那么就直接进入到 LinearLayoutManager 的 scrollHorizontallyBy 方法(垂直方向的逻辑都一样就只看一个了)查看其源码:

LinearLayoutManager.java

public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
        RecyclerView.State state) {
    if (mOrientation == VERTICAL) { // 判断了下方向
        return 0;
    }
    //  调用了 scrollBy
    return scrollBy(dx, recycler, state);
}

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ...
    // 滑动方向
    final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    // 距离
    final int absDelta = Math.abs(delta);
    updateLayoutState(layoutDirection, absDelta, true, state);
    // fill 方法很熟悉了 主要处理 itemView 的摆放以及回收复用
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    // ...
    final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
    // 交给 mOrientationHelper 的 offsetChildren 方法处理滑动
    mOrientationHelper.offsetChildren(-scrolled);
    // ...
    return scrolled;
}

OrientationHelper 是一个抽象类,offsetChildren 也是一个抽象方法,那么直接在 LinearLayoutManager 中查看其初始化逻辑:

LinearLayoutManager.java

public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
        int defStyleRes) {
    Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
    // 在构造方法中 设置了方向
    setOrientation(properties.orientation);
    setReverseLayout(properties.reverseLayout);
    setStackFromEnd(properties.stackFromEnd);
}

public void setOrientation(@RecyclerView.Orientation int orientation) {
    // ...
    if (orientation != mOrientation || mOrientationHelper == null) {
        // 通过 OrientationHelper 静态方法 createOrientationHelper 初始化
        mOrientationHelper = OrientationHelper.createOrientationHelper(this, orientation);
        // ...
    }
}

OrientationHelper.java

public static OrientationHelper createOrientationHelper(
        RecyclerView.LayoutManager layoutManager, @RecyclerView.Orientation int orientation) {
    switch (orientation) {
        case HORIZONTAL: // 水平方向
            return createHorizontalHelper(layoutManager);
        case VERTICAL: // 垂直
            return createVerticalHelper(layoutManager);
    }
    throw new IllegalArgumentException("invalid orientation");
}

// 由于分析水平方向,所以只查看其水平方向的创建源码
public static OrientationHelper createHorizontalHelper(
        RecyclerView.LayoutManager layoutManager) {
    return new OrientationHelper(layoutManager) {
        // ...
        public void offsetChildren(int amount) {
            // 又调用回了 LayoutManager
            mLayoutManager.offsetChildrenHorizontal(amount);
        }
        // ...
    };
}

LayoutManager.java

public void offsetChildrenHorizontal(@Px int dx) {
    if (mRecyclerView != null) {
        // 又调用到了 RecyclerView 中
        mRecyclerView.offsetChildrenHorizontal(dx);
    }
}

RecyclerView.java

public void offsetChildrenHorizontal(@Px int dx) {
    final int childCount = mChildHelper.getChildCount();
    for (int i = 0; i < childCount; i++) {
        // 循环获取 View 调用 View 的 offsetLeftAndRight 方法进行偏移操作
        mChildHelper.getChildAt(i).offsetLeftAndRight(dx);
    }
}

经过这一系列的源码调用,最终滑动还是交给 RecyclerView 去遍历子 View 设置偏移来实现的,其实可以看出当我们自定义 LayoutManager 时需要处理滑动时,水平方向调用 offsetChildrenHorizontal 那么垂直方向自然是调用 offsetChildrenVertical。

回收复用

LinearLayoutManager fill 方法的回收调用流程:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    //...
    // 可用空间
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    // 当 remainingSpace > 0 会继续循环
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        // ...
        // 布局
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        // 计算可用空间
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            remainingSpace -= layoutChunkResult.mConsumed;
        }
        
        // 在上面布局、计算可用空间的操作完成后
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            // 通过 recycleByLayoutState 去回收移除屏幕的 View
            recycleByLayoutState(recycler, layoutState);
        }
        // ...
    }
}

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    if (!layoutState.mRecycle || layoutState.mInfinite) {
        return;
    }
    int scrollingOffset = layoutState.mScrollingOffset;
    int noRecycleSpace = layoutState.mNoRecycleSpace;
    // 和布局时一样 分两个方向
    // 以水平滑动为例吧 左滑则左侧item移除屏幕需要回收,右滑同理
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
        recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
    } else {
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}

private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset,
        int noRecycleSpace) {
    final int childCount = getChildCount();
    // 计算出回收的触发距离
    final int limit = mOrientationHelper.getEnd() - scrollingOffset + noRecycleSpace;
    if (mShouldReverseLayout) {
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            // 遍历找出需要回收的子 View 的最大索引
            if (mOrientationHelper.getDecoratedStart(child) < limit
                    || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
                // 通过 recycleChildren 回收
                recycleChildren(recycler, 0, i);
                return;
            }
        }
    }
    // ...
}

private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
    // 根据传入的索引 开始遍历回收
    if (endIndex > startIndex) {
        for (int i = endIndex - 1; i >= startIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    } else {
        for (int i = startIndex; i > endIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    }
}

public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
    final View view = getChildAt(index);
    removeViewAt(index); // 移除 View
    recycler.recycleView(view); // 最终调用到了 recycler.recycleView
}

自定义 LayoutManager

image.png

其实算一个比较简单的需求,和自定义 ViewGroup 很像只要计算好每个 item 的位置,在第几页、第几列、第几行按位置摆放即可,重点在于理解自定义 LayoutManager 的步骤,相比于自定义 ViewGroup 多了一个回收复用的步骤,并且滑动也不需要自己去实现。

布局逻辑就不说了,一页一页的排布,仅仅是根据 item 的 index 计算它的位置。

滑动和回收复用是关联在一起的,一般自定义 LayoutManager 要保证子 View 当数量,也就是 childCount 不超过屏幕可见的 item 数量,也就意味着当滑动结束后 item 的坐标不在屏幕可见范围内就应该回收,而新滑入屏幕的 View 应该优先从缓存池中获取达到复用的目的。

布局

首先要根据 index 能算出 item 在第几行第几列,接着再根据行列计算出 item 的位置:

// 在第几页
var page = ...
// 在分页的第几个位置
val pageIndex = ...
// 第几列
val colum = ...
// 第几行
val row = ...
// 位置
itemRect.left = page * 一页的宽度 + colum * itemView的宽度
itemRect.right = itemRect.left + itemView的宽度
itemRect.top = row * itemView的高度
itemRect.bottom = itemRect.top + itemView的高度

接着根据滑动的距离,计算出可见范围的 Rect,遍历 items 只要在可见范围内则添加到屏幕上即可:

// 可见范围
val outRect = Rect(滑动偏移量, 0, 滑动偏移量 + 一页的宽度, 一页的高度)
while (index < itemCount) {
    val itemRect
    if (Rect.intersects(outRect, itemRect)) { // 在范围内
        // 添加 测量 布局 itemView 即可
    }
    index++
}

滑动

滑动 LayoutManager 给出了接口:

// 是否可以水平滑动
public boolean canScrollHorizontally() {
    return false;
}
// 水平滑动触发 需要返回消费的滑动距离
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
    return 0;
}
// 是否可以垂直滑动
public boolean canScrollVertically() {
    return false;
}
// 垂直滑动触发
public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
    return 0;
}

重写对应方法返回 true 即可,实现水平滑动可以重写 scrollHorizontallyBy 在其中利用 offsetChildrenHorizontal 方法进行滑动,但是要注意边界检测(如果做无限循环的 LayoutManager 就不需要检测边界了)。

回收

回收复用是重中之重,在合适的时机(当前Demo的场景当 item 不在可见范围内即可回收),LayoutManager 并不处理回收,而是都要交给 Recycler 去处理,LayoutManager 也给我们提供了一些 Api:

detachAndScrapAttachedViews // 回收所有可见的 View
detachAndScrapView // 回收指定 View
detachView // 轻量级回收,用于马上要 attach 回来的情况

复用

复用一般只需要调用 recycler.getViewForPosition 即可,会根据 RecyclerView 的缓存机制一层一层的获取缓存。

class NavigationGridLayoutManager : RecyclerView.LayoutManager() {

    private var mItemViewWidth = 0 // itemView 宽度
    private var mItemViewHeight = 0 // itemView 高度

    private val mColumCount = 5 // 列数
    private val mRowCount = 2 // 行数
    private var mPageCount = 0 // 页面数
    private var mPageItemSize = 0 // 一页能放多少个 item = mRowCount * mColumCount

    private var mPageWidth = 0 // 一页的宽度
    private var mPageHeight = 0 // 一页的高度
    private var mOffsetHorizontal = 0 // 水平滑动偏移量 用于计算可见范围 布局子 View
    private var mMaxOffsetHorizontal = 0 // 水平滑动最大偏移量 滑动边界

    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
        return RecyclerView.LayoutParams(
            RecyclerView.LayoutParams.WRAP_CONTENT,
            RecyclerView.LayoutParams.WRAP_CONTENT
        )
    }

    override fun onMeasure(
        recycler: RecyclerView.Recycler,
        state: RecyclerView.State,
        widthSpec: Int,
        heightSpec: Int
    ) {
        // 这里只考虑水平滑动的场景 因为需要均分高度 高度一定要是测量值 所以重写 onMeasure 检查 heightMode
        val heightSize = MeasureSpec.getSize(heightSpec)
        var heightMode = MeasureSpec.getMode(heightSpec)
        if (heightMode != MeasureSpec.EXACTLY && heightSize > 0) {
            heightMode = MeasureSpec.EXACTLY
        }
        super.onMeasure(
            recycler,
            state,
            widthSpec,
            MeasureSpec.makeMeasureSpec(heightSize, heightMode)
        )
    }

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
        if (state.isPreLayout) {
            return
        }
        if (itemCount == 0) {
            detachAndScrapAttachedViews(recycler)
            return
        }
        // 获取一页的宽高、itemView均分后的宽高、一页的item数量、页面数量
        mPageWidth = width - paddingLeft - paddingRight
        mPageHeight = height - paddingTop - paddingBottom
        mItemViewWidth = mPageWidth / mColumCount
        mItemViewHeight = mPageHeight / mRowCount
        mPageItemSize = mRowCount * mColumCount
        mPageCount = itemCount / mPageItemSize
        if (itemCount % mPageItemSize > 0) {
            mPageCount++
        }
        // 最大滑动边界
        mMaxOffsetHorizontal = (mPageCount - 1) * mPageWidth
        // 模仿 LinearLayoutManager 在 fill 方法中进行填充布局的操作
        fill(recycler, state, 0)
    }
    
    // 允许水平滑动
    override fun canScrollHorizontally(): Boolean {
        return true
    }
    
    // 水平滑动处理
    override fun scrollHorizontallyBy(
        dx: Int,
        recycler: RecyclerView.Recycler,
        state: RecyclerView.State
    ): Int {
        // 因为有边界,所以需要获取实际滑动消费的距离
        val newX: Int = mOffsetHorizontal + dx
        var result = dx
        if (newX > mMaxOffsetHorizontal) {
            result = mMaxOffsetHorizontal - mOffsetHorizontal
        } else if (newX < 0) {
            result = 0 - mOffsetHorizontal
        }
        mOffsetHorizontal += result // 记录滑动的偏移量 用于计算可见范围
        offsetChildrenHorizontal(-result) // 滑动子View
        fill(recycler, state, result) // 填充布局
        return result
    }

    private fun fill(recycler: RecyclerView.Recycler, state: RecyclerView.State, dx: Int) {
        if (state.isPreLayout) {
            return
        }
        // 先将屏幕上的 View 全部分离进缓存
        detachAndScrapAttachedViews(recycler)
        // 计算出可见范围
        val outRect = Rect(
            mOffsetHorizontal,
            0,
            mPageWidth + mOffsetHorizontal,
            mPageHeight
        )
        // 遍历 item
        // 注意:这么写性能很烂 如果有 itemCount 增大后 这个循环会导致严重的卡顿
        // 篇幅原因就简单处理了 遍历了所有 View
        // 正确做法:根据可见范围计算出 遍历的区间
        var startPosition = 0
        while (startPosition < itemCount) {
            val itemRect = Rect()
            // 在第几页
            val page = startPosition / mPageItemSize
            // 在分页的第几个位置
            val pageIndex = (startPosition) % mPageItemSize
            // 第几列
            val colum = pageIndex % mColumCount
            // 第几行
            val row = pageIndex / mColumCount
            // 位置
            itemRect.left = page * mPageWidth + colum * mItemViewWidth
            itemRect.right = itemRect.left + mItemViewWidth
            itemRect.top = row * mItemViewHeight
            itemRect.bottom = itemRect.top + mItemViewHeight
            // 是否在可见范围内
            if (Rect.intersects(outRect, itemRect)) {
                val itemView = recycler.getViewForPosition(startPosition) // 获取 View
                addView(itemView) // 添加 View
                measureChildWithMargins(itemView, 0, 0) // 测量
                layoutDecoratedWithMargins( // 对 View 进行布局
                    itemView,
                    itemRect.left - mOffsetHorizontal,
                    itemRect.top,
                    itemRect.right - mOffsetHorizontal,
                    itemRect.bottom
                )
            }
            startPosition++ 
        }
    }
}

ItemDecoration

由于 ItemDecoration 的源码很简单,直接进入源码:

public abstract static class ItemDecoration {
    
    public void onDraw(Canvas c, RecyclerView parent,State state) {
        onDraw(c, parent);
    }

    public void onDrawOver(Canvas c, RecyclerView parent, State state) {
        onDrawOver(c, parent);
    }

    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
        getItemOffsets(outRect, ((LayoutParams)view.getLayoutParams()).getViewLayoutPosition(), parent);
    }
    
    // 方法标记删除 使用上面的 onDraw
    @Deprecated
    public void onDraw(Canvas c, RecyclerView parent) {
    }

    // 方法标记删除 使用上面的 onDrawOver
    @Deprecated
    public void onDrawOver(Canvas c, RecyclerView parent) {
    }

    // 方法标记删除 使用上面的 getItemOffsets
    @Deprecated
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        outRect.set(0, 0, 0, 0);
    }
}

onDraw、onDrawOver

RecyclerView.java

public void draw(Canvas c) {
    super.draw(c);
    // draw 方法上来就循环 mItemDecorations 逐个调用了 onDrawOver 方法
    // mItemDecorations 就不贴代码了 就是个 ArrayList 存储 ItemDecoration 对象
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    // ...
}

View 的 draw 方法中会调用 onDraw,那么接着看一下 onDraw 方法:

RecyclerView.java

public void onDraw(Canvas c) {
    super.onDraw(c);
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        // 同样是遍历 这次调用的是 ItemDecoration 的 onDraw 方法
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

RecyclerView 的 draw 方法先调用了 super.draw(),在父类 View 的 draw 方法中先调用了 onDraw,然后再执行RecyclerView 重写后剩余 draw 方法中的代码,所以 ItemDecoration onDraw 方法是先执行,onDrawOver 则是后执行。

getItemOffsets

public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
    outRect.set(0, 0, 0, 0);
}

先来个示意图来理解一下 getItemOffsets 中 outRect 的含义:

getItemOffsets 设置偏移前后的区别:

image.png

就是给 itemView 设置了一个内部偏移,说的再简单点就是 itemView 内容的绘制区域缩小了。

接着查看下其在 RecyclerView 的调用时机,在 RecyclerView 中搜索后发现仅有一处调用:

RecyclerView.java

Rect getItemDecorInsetsForChild(View child) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    // ...
    // 从 LayoutParams 中取出
    final Rect insets = lp.mDecorInsets;
    insets.set(0, 0, 0, 0);
    final int decorCount = mItemDecorations.size();
    for (int i = 0; i < decorCount; i++) {
        mTempRect.set(0, 0, 0, 0);
        // 遍历获取
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
        // 对 insets 进行累加
        insets.left += mTempRect.left;
        insets.top += mTempRect.top;
        insets.right += mTempRect.right;
        insets.bottom += mTempRect.bottom;
    }
    lp.mInsetsDirty = false;
    return insets;
}

可以看出每个 ItemDecoration 设置的偏移量都进行累加存储在了 LayoutParams 的 mDecorInsets 中,继续查看下 getItemDecorInsetsForChild 的调用时机:

RecyclerView.java


// 测量 View
public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    // 获取 mDecorInsets 累加完成后的结果
    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;
    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
            getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
            canScrollHorizontally());
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
            getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
            canScrollVertically());
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}

// 和上面 measureChild 是一样的 
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
    //...
    // 区别在于 widthSpec heightSpec 计算时增加了 Margin
    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
            getPaddingLeft() + getPaddingRight()
                    + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
            canScrollHorizontally());
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
            getPaddingTop() + getPaddingBottom()
                    + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
            canScrollVertically());
    //...
}

从上面的代码不难看出,在测量子 View(也就是 itemView)时,整体占用的宽高是包含设置的偏移量的。

示意图

最后再来个示意图将三个方法结合起来理解下:

image.png

  • onDraw 绘制的内容在最底层,itemView 绘制在其内容之上,而 onDrawOver 绘制的内容在最上层;

  • 图中 onDrawOver 虽然绘制在了 itemView 之中,但其实可以超出 itemView,我怕会有歧义特别说明一下;

  • 如果想让 onDraw 绘制的内容不被 itemView 遮挡就重写 getItemOffsets 方法设置合适的偏移;

自定义 ItemDecoration

image.png

进度条首先要保证不被 item 遮挡,那么绘制的逻辑要写在 onDrawOver 保证其绘制在最上层。其次也要保证进度条不遮挡 item,需要重写 getItemOffsets 对 item 设置底部偏移。

绘制逻辑比较简单用 drawLine 画进度条背景、进度条即可。进度等于 当滑动距离 / 最大滑动距离 ,获取这两个值可以通过 RecyclerView 的 重写 computeHorizontalScrollOffsetcomputeHorizontalScrollRange 两个方法,可以看下源码:

RecyclerView.java

public class RecyclerView {
    // ...
    public int computeHorizontalScrollOffset() {
        if (mLayout == null) {
            return 0;
        }
        return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollOffset(mState) : 0;
    }
    
    @Override
    public int computeHorizontalScrollRange() {
        if (mLayout == null) {
            return 0;
        }
        return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollRange(mState) : 0;
    }
    // ...
}

可以看出 RecyclerView 又交给了 LayoutManager 去获取,而 LayoutManager 中两个方法默认返回 0,需要我们自己重写实现计算。

class NavigationGridLayoutManager : RecyclerView.LayoutManager() {
    // ...
    override fun computeHorizontalScrollOffset(state: RecyclerView.State): Int {
        return mOffsetHorizontal
    }
    
    override fun computeHorizontalScrollRange(state: RecyclerView.State): Int {
        return mMaxOffsetHorizontal
    }
}

接着自定义 ItemDecoration:

class NavigationItemDecoration : RecyclerView.ItemDecoration() {

    private val mPaint = Paint().apply {
        strokeCap = Paint.Cap.ROUND
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
        // 滑动偏移量
        val scrollOffset = parent.computeHorizontalScrollOffset().toFloat()
        // 最大滑动距离
        val scrollRange = parent.computeHorizontalScrollRange().toFloat()
        // 进度条高度
        val progressHeight = 4f.dp
        // 进度条整体宽度
        val progressBgWidth = 100f.dp
        // 进度条宽度
        val progressWidth = 10f.dp

        // 先画进度条背景
        mPaint.color = Color.LTGRAY
        mPaint.strokeWidth = progressHeight

        val progressBgStartX = parent.width / 2f - progressBgWidth / 2
        val progressBgEndX = parent.width / 2f + progressBgWidth / 2
        val progressBgStartY = parent.height - progressHeight
        val progressBgEndY = parent.height - progressHeight
        c.drawLine(progressBgStartX, progressBgStartY, progressBgEndX, progressBgEndY, mPaint)

        // 画进度条
        mPaint.color = Color.GREEN
        val progressStartX = min(
            progressBgEndX - progressWidth,
            progressBgStartX + progressBgWidth * (scrollOffset / scrollRange)
        )
        val progressEndX = progressStartX + progressWidth
        val progressStartY = progressBgStartY
        val progressEndY = progressBgEndY
        c.drawLine(progressStartX, progressStartY, progressEndX, progressEndY, mPaint)
    }

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        // 设置底部偏移 防止被遮挡
        outRect.set(0, 0, 0, 10.dp)
    }
}

ItemAnimator

一般我们给 RecyclerView 设置动画会调用 setItemAnimator 方法,直接看一下源码:

RecyclerView.java

// 默认动画 DefaultItemAnimator
ItemAnimator mItemAnimator = new DefaultItemAnimator();

public void setItemAnimator(@Nullable ItemAnimator animator) {
    if (mItemAnimator != null) { // 先结束动画,取消监听
        mItemAnimator.endAnimations();
        mItemAnimator.setListener(null);
    }
    mItemAnimator = animator; // 赋值
    if (mItemAnimator != null) { // 重新设置监听
        mItemAnimator.setListener(mItemAnimatorListener);
    }
}

可以看出 RecyclerView 默认提供了 DefaultItemAnimator,先不着急分析它,首先我们要分析出动画的执行流程以及动画的信息是怎么处理的。先了解以下这么几个类作为基础。

ItemHolderInfo

RecylerView.java

public static class ItemHolderInfo {

    public int left;
    public int top;
    public int right;
    public int bottom;
    @AdapterChanges
    public int changeFlags;

    public ItemHolderInfo() {
    }

    public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder) {
        return setFrom(holder, 0);
    }
    
    @NonNull
    public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder,
            @AdapterChanges int flags) {
        final View view = holder.itemView;
        this.left = view.getLeft();
        this.top = view.getTop();
        this.right = view.getRight();
        this.bottom = view.getBottom();
        return this;
    }
}

ItemHolderInfo 作为 RecyclerView 的内部类,代码非常简单,向外暴露 setFrom 方法,用于存储 ViewHolder 的位置信息;

InfoRecord

InfoRecord 是 ViewInfoStore 的内部类(下一小节分析),代码也非常简单:

ViewInfoStore.java

static class InfoRecord {
    // 一些 Flag 定义
    static final int FLAG_DISAPPEARED = 1; // 消失
    static final int FLAG_APPEAR = 1 << 1; // 出现
    static final int FLAG_PRE = 1 << 2; // 预布局
    static final int FLAG_POST = 1 << 3; // 真正布局
    static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED;
    static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;
    static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST;
    // 这个 flags 要记住!!
    // 后面多次对其进行赋值,且执行动画时也根据 flags 来判断动画类型;
    int flags;
    // ViewHolder 坐标信息
    RecyclerView.ItemAnimator.ItemHolderInfo preInfo; // 预布局阶段的
    RecyclerView.ItemAnimator.ItemHolderInfo postInfo; // 真正布局阶段的
    // 池化 提高效率
    static Pools.Pool<InfoRecord> sPool = new Pools.SimplePool<>(20);
    // ...
    // 其内部的一些方法都是复用池相关 特别简单 就不贴了
}

不难看出,InfoRecord 功能和他的名字一样信息记录,主要记录了预布局、真正布局两个阶段的 ViewHodler 的位置信息(ItemHolderInfo)。

ViewInfoStore

class ViewInfoStore {
    // 将 ViewHodler 和 InfoRecord 以键值对形式存储
    final SimpleArrayMap<RecyclerView.ViewHolder, InfoRecord> mLayoutHolderMap =
            new SimpleArrayMap<>();
    // 根据坐标存储 ViewHodler 看名字也看得出是 旧的,旧是指:
    // 1.viewHolder 被隐藏 但 未移除
    // 2.隐藏item被更改
    // 3.预布局跳过的 item
    final LongSparseArray<RecyclerView.ViewHolder> mOldChangedHolders = new LongSparseArray<>();
    
    // mLayoutHolderMap 中添加一项 如果有就改变 InfoRecord 的值
    // 下面很多方法都是类似功能 下面的就不贴 if 里面那段了
    void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        if (record == null) { // 没有就构建一个 加入 map
            record = InfoRecord.obtain();
            mLayoutHolderMap.put(holder, record);
        }
        record.preInfo = info; 
        record.flags |= FLAG_PRE; // 跟方法名对应的 flag
    }

    // 调用 popFromLayoutStep 传递 FLAG_PRE
    RecyclerView.ItemAnimator.ItemHolderInfo popFromPreLayout(RecyclerView.ViewHolder vh) {
        return popFromLayoutStep(vh, FLAG_PRE);
    }
    
    // 调用 popFromLayoutStep 传递 FLAG_POST
    RecyclerView.ItemAnimator.ItemHolderInfo popFromPostLayout(RecyclerView.ViewHolder vh) {
        return popFromLayoutStep(vh, FLAG_POST);
    }
    
    // 上面两个方法都调用的这里 flag 传递不同
    private RecyclerView.ItemAnimator.ItemHolderInfo popFromLayoutStep(RecyclerView.ViewHolder vh, int flag) {
        int index = mLayoutHolderMap.indexOfKey(vh);
        if (index < 0) {
            return null;
        }
        // 从map中获取
        final InfoRecord record = mLayoutHolderMap.valueAt(index);
        if (record != null && (record.flags & flag) != 0) {
            record.flags &= ~flag;
            final RecyclerView.ItemAnimator.ItemHolderInfo info;
            if (flag == FLAG_PRE) { // 根据 flag 获取对应的 ItemHolderInfo
                info = record.preInfo;
            } else if (flag == FLAG_POST) {
                info = record.postInfo;
            } else {
                throw new IllegalArgumentException("Must provide flag PRE or POST");
            }
            // 如果没有包含两个阶段的flag 直接回收
            if ((record.flags & (FLAG_PRE | FLAG_POST)) == 0) {
                mLayoutHolderMap.removeAt(index);
                InfoRecord.recycle(record);
            }
            return info;
        }
        return null;
    }
    // 向 mOldChangedHolders 添加一个 holder
    void addToOldChangeHolders(long key, RecyclerView.ViewHolder holder) {
        mOldChangedHolders.put(key, holder);
    }
    
    // 和 addToPreLayout 方法类似 flags 不同
    void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        // ...
        record.flags |= FLAG_APPEAR;
        record.preInfo = info;
    }
    
    // 和 addToPreLayout 方法类似 flags 不
    // 注意这里的方法名 是添加的 post-layout 真正布局阶段的信息 
    void addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        // ...
        record.postInfo = info; // 这里赋值的是 postInfo
        record.flags |= FLAG_POST;
    }
    
    // 这里直接拿到 InfoRecord 修改了 flag
    void addToDisappearedInLayout(RecyclerView.ViewHolder holder) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        // ...
        record.flags |= FLAG_DISAPPEARED;
    }
    // 这里直接拿到 InfoRecord 修改了 flag
    void removeFromDisappearedInLayout(RecyclerView.ViewHolder holder) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        // ...
        record.flags &= ~FLAG_DISAPPEARED;
    }
    
    // 移除 两个容器都移除
    void removeViewHolder(RecyclerView.ViewHolder holder) {
        //...
        mOldChangedHolders.removeAt(i);
        //...
        final InfoRecord info = mLayoutHolderMap.remove(holder);
    }
    
    // 这里其实是 动画开始的入口
    void process(ProcessCallback callback) {
        // 倒着遍历 mLayoutHolderMap
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
            final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
            final InfoRecord record = mLayoutHolderMap.removeAt(index);
            // 取出 InfoRecord 根据 flag 和 两个阶段位置信息 进行判断 触发对应的 callback 回调方法
            if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                callback.unused(viewHolder);
            } // 一大堆判断就先省略了,后面会提到 ...
            // ...
            // 最后回收
            InfoRecord.recycle(record);
        }
    }
    // ...
}

ViewInfoStore,字面翻译为 View信息商店,类名就体现出了他的功能,主要提供了对 ViewHolder 的 InfoRecord 存储以及修改,并且提供了动画触发的入口。

ProcessCallback

还有最后一个类需要了解,也就是上面 ViewInfoStore 最后一个方法 process 中用到的 callback,直接看源码:

ViewInfoStore.java

//前三个需要做动画的方法传入了 viewHolder 以及其预布局、真正布局两个阶段的位置信息
interface ProcessCallback {
    // 进行消失
    void processDisappeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo, RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
    // 进行出现
    void processAppeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo, RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
    // 持续 也就是 不变 或者 数据相同大小改变的情况
    void processPersistent(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo, RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
    // 未使用
    void unused(RecyclerView.ViewHolder holder);
}

ProcessCallback 在 RecyclerView 有默认实现,这个待会再详细分析,看 callback 的方法名也能略知一二,分别对应 ViewHolder 做动画的几种情况;

那么从前三个方法的参数中也能推断出,ViewHolder 做动画时,动画的数据也是从 preInfo 和 postInfo 两个参数中做计算得出。

动画处理

前置基础有点多😂😂😂,不过通过对上面几个类有一些了解,下面在分析动画触发、信息处理时就不用反复解释一些变量的意义了。

dispatchLayoutStep3

在之前分析布局阶段的博客中提到 dispatchLayoutStep1、2、3 三个核心方法,分别对应三种状态 STEP_START、STEP_LAYOUT、STEP_ANIMATIONS;

很显然,STEP_ANIMATIONS 是执行动画的阶段,再来看一下 dispatchLayoutStep3 方法中对 item 动画进行了哪些操作:

RecyclerView.java

private void dispatchLayoutStep3() {
    // ...
    if (mState.mRunSimpleAnimations) { // 需要做动画
        // 倒着循环 因为可能会发生移除
        for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
            // 获取到 holder
            ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            if (holder.shouldIgnore()) { // 如果被标记忽略则跳过
                continue;
            }
            // 获取 holder 的 key 一般情况获取的就是 position
            long key = getChangedHolderKey(holder);
            // 前置基础中 提到的 ItemHolderInfo
            // recordPostLayoutInformation 内部构建了一个 ItemHolderInfo 并且调用了 setFrom 设置了 位置信息
            final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
            // 从 ViewInfoStore 的 mOldChangedHolders 中获取 vh
            ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
            if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                // 是否正在执行消失动画
                final boolean oldDisappearing = mViewInfoStore.isDisappearing(oldChangeViewHolder);
                final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
                // 这个 if 判断 将要发生更新动画的 vh 已经在执行消失动画
                if (oldDisappearing && oldChangeViewHolder == holder) {
                    // 用消失动画代替
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
                } else {
                    // 获取 预布局阶段的 ItemHolderInfo
                    final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(oldChangeViewHolder);
                    // 设置 真正布局阶段的 ItemHolderInfo
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
                    // 然后在取出来
                    ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
                    if (preInfo == null) {
                        handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
                    } else { // 用上面取出来的 preInfo postInfo 做更新动画
                        animateChange(oldChangeViewHolder, holder, preInfo, postInfo,oldDisappearing, newDisappearing);
                    }
                }
            } else { // 没有获取到 直接设置 hodler 真正布局阶段的 位置信息 并且设置 flag
                mViewInfoStore.addToPostLayout(holder, animationInfo);
            }
        }

        // 开始执行动画
        mViewInfoStore.process(mViewInfoProcessCallback);
    }
    // ...
}

代码中的注释写的比较详细,主要注意一下 mViewInfoStore.addToPostLayout 会给 ViewHolder 生成 InfoRecord 对象,并且设置 postInfo,并且给 flags 添加 FLAG_POST,然后以 <ViewHolder, InfoRecord> 键值对形式添加到 ViewInfoStore 的 mLayoutHolderMap 中;

dispatchLayoutStep1

其实上一小节 dispatchLayoutStep3 方法中也包含对动画信息的处理,也就是针对真正布局后的位置信息设置的相关代码。那么删除、新增的动画在哪里实现呢?首先,回顾一下之前分析的布局流程,真正的布局发生在 dispatchLayoutStep2 中,预布局发生在 dispatchLayoutStep1 中,结合之前对预布局的简单解释,不难理解出预布局时肯定也对动画信息进行了处理,那么直接看一下 dispatchLayoutStep1 的相关源码,这部分需要分成两段来分析,先看第一段:

RecyclerView.java

private void dispatchLayoutStep1() {
    // ...
    if (mState.mRunSimpleAnimations) {
        // 遍历 child
        int count = mChildHelper.getChildCount();
        for (int i = 0; i < count; ++i) {
            // 获取 vh
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            // 忽略、无效的 跳过
            if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
                continue;
            }
            // 构造出 ItemHolderInfo
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPreLayoutInformation(mState, holder,
                            ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                            holder.getUnmodifiedPayloads());
            // 注意这里 时 addToPreLayout. 表示预布局阶段
            // 此时设置的是 InfoRecord 的 preInfo,flag 是 FLAG_PRE
            mViewInfoStore.addToPreLayout(holder, animationInfo);
            // 如果 holder 发生改变 添加到 ViewInfoStore 的 mOldChangedHolders 中
            if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                    && !holder.shouldIgnore() && !holder.isInvalid()) {
                long key = getChangedHolderKey(holder); // 获取 key 一般是 position
                mViewInfoStore.addToOldChangeHolders(key, holder);
            }
        }
    }
    // ...
}

这一段也不复杂,记录当前 holder 预布局阶段的位置信息(InfoRecord 的 preInfo)到 ViewInfoStore 的 mLayoutHolderMap 中,且添加了 FLAG_PRE 到 flags 中;

并且如果 holder 发生改变就添加到 ViewInfoStore 的 mOldChangedHolders 中;

再看下面的代码:

RecyclerView.java

private void dispatchLayoutStep1() {
    // ...
    if (mState.mRunPredictiveAnimations) {
        // ...
        // 这次是预布局 计算可用空间时忽略了要删除的项目 所以如果发生删除 会有新的 item 添加进去
        mLayout.onLayoutChildren(mRecycler, mState);
        // ...
        // 遍历 child
        for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
            final View child = mChildHelper.getChildAt(i);
            final ViewHolder viewHolder = getChildViewHolderInt(child);
            if (viewHolder.shouldIgnore()) {
                continue;
            }
            // 这个判断也就是没有经历过上一部分代码的 vh (onLayoutChildren 中新加入的 item)
            // InfoRecord 为 null 或者 flags 不包含 FLAG_PRE
            if (!mViewInfoStore.isInPreLayout(viewHolder)) {
                int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
                // 判断是否是隐藏的
                boolean wasHidden = viewHolder
                        .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                if (!wasHidden) { // 没有隐藏 则标记在预布局阶段出现
                    flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
                }
                // 构造出 ItemHolderInfo
                final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                        mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                if (wasHidden) { 
                    // 隐藏的 如果发生更新 并且没有被移除 就添加到 mOldChangedHolders
                    // 设置 preInfo 设置 flag为 FLAG_PRE
                    recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                } else { // 没有隐藏的 设置 flag FLAG_APPEAR, 并且设置 preInfo
                    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                }
            }
        }
        clearOldPositions();
    } else {
        clearOldPositions();
    }
    // ...
}

这里结合之前解释预布局时的图来理解下:

image.png

第一部分执行时,item1、2、3 都会执行 addToPreLayout,addToPreLayout 会生成 InfoRecord 并且设置其 preInfo 存储 vh 的位置信息,然后以 <ViewHolder, InfoRecord> 键值对形式添加到 ViewInfoStore 的 mLayoutHolderMap 中;

然后第二部分执行了 onLayoutChildren 进行了预布局,以 LinearLayoutManager 为例,在计算可用空间时会忽略要删除的 item3,从而 item4 被添加到 RecyclerView 中,再次对 child 进行遍历时进行 mViewInfoStore.isInPreLayout(viewHolder) 判断时显然 item4 对应的 ViewHolder 在 mLayoutHolderMap 中获取为 null,那么就能知道 item4 属于新增出来的,就在最后调用 mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo); 生成 InfoRecord 设置位置信息,并且添加 flag 为 FLAG_APPEAR 添加到 mLayoutHolderMap 中。

总结

这部分源码是倒着来分析的(先看 dispatchLayoutStep3 在看 1 ),可能有点不好理解,先从这三个布局核心方法的角度来稍稍总结一下(均假设需要执行动画):

dispatchLayoutStep1

  1. 首先将当前屏幕中的 items 信息保存;(生成 ItemHolderInfo 赋值给 InfoRecord 的 preInfo 并且对其 flags 添加 FLAG_PRE ,再将 InfoRecord 添加到 ViewInfoStore 的 mLayoutHolderMap 中)
  2. 进行预布局;(调用 LayoutManager 的 onLayoutChildren)
  3. 预布局完成后和第 1 步中保存的信息对比,将新出现的 item 信息保存;(和第 1 步中不同的是 flags 设置的是 FLAG_APPEAR)

dispatchLayoutStep2

  1. 将预布局 boolean 值改为 flase;
  2. 进行真正布局;(调用 LayoutManager 的 onLayoutChildren)

dispatchLayoutStep3

  1. 将真正布局后屏幕上的 items 信息保存;(与 dispatchLayoutStep1 不同的是赋值给 InfoRecord 的 postInfo 并且 flags 添加 FLAG_POST)
  2. 执行动画,调用 ViewInfoStore.process;
  3. 布局完成回调,onLayoutCompleted;

动画执行

经过上面两个 dispatchLayoutStep1 和 3 方法的执行,ViewInfoStore 中已经有预布局时 item 的信息、真正布局后的 item 信息、以及对应的 flags。最终调用了 ViewInfoStore 的 process 执行动画:

image.png

这里面的代码不难,就是根据 flags 进行判断执行对应的动画,调用的是 ProcessCallback 中的方法进行执行,那么看一下 ProcessCallback 的具体实现:

RecyclerView.java

private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
        new ViewInfoStore.ProcessCallback() {
            // ...
            // 这里就以执行新增动画为例 其他的也都差不多
            @Override
            public void processAppeared(ViewHolder viewHolder,
                    ItemHolderInfo preInfo, ItemHolderInfo info) {
                // 调用 animateAppearance
                animateAppearance(viewHolder, preInfo, info);
            }
            // ...
        };
        
void animateAppearance(@NonNull ViewHolder itemHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
    // 先标记 vh 不能被回收
    itemHolder.setIsRecyclable(false);
    // mItemAnimator 上面也提过了 又默认实现 待会再分析
    if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
        // 这里看方法名也知道是 post 一个 runnable
        postAnimationRunner();
    }
}

void postAnimationRunner() {
    if (!mPostedAnimatorRunner && mIsAttached) {
        // 核心就是 post 的 mItemAnimatorRunner
        ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
        mPostedAnimatorRunner = true;
    }

private Runnable mItemAnimatorRunner = new Runnable() {
    @Override
    public void run() {
        if (mItemAnimator != null) {
            // 有调用到了 mItemAnimator 中
            mItemAnimator.runPendingAnimations();
        }
        mPostedAnimatorRunner = false;
    }
};

整个调用下来核心就在于 ItemAnimator 的两个方法调用(animateAppearance、runPendingAnimations),那么下面我们就来进入 ItemAnimator 的分析;

ItemAnimator

在最开始的前置基础小节提到 mItemAnimator 实际上是 DefaultItemAnimator;而 DefaultItemAnimator 继承自 SimpleItemAnimator,SimpleItemAnimator 又继承自 ItemAnimator。ItemAnimator 是 RecyclerView 的内部类,其内部大部分是抽象方法需要子类实现,就简单说说其主要功能不贴代码了:

  1. ItemHolderInfo 是 ItemAnimator 的内部类,用于保存位置信息;
  2. ItemAnimatorListener 是其内部动画完成时的回调接口;
  3. 提供设置动画时间、动画执行、动画开始结束回调、动画状态的方法,大部分是需要子类实现的;

而上述提供的 animateAppearance 和 runPendingAnimations 都是抽象方法,这里并没有实现;

SimpleItemAnimator

SimpleItemAnimator 继承自 ItemAnimator,乍一看方法很多,大部分都是空实现或抽象方法:

image.png

这一堆 dispatchXXX 方法和 onXXX 方法是一一对应的,dispatchXXX 中调用 onXXX,而 onXXX 都是空方法交给子类去实现,这部分代码很简单就不贴了;

SimpleItemAnimator 实现了 animateAppearance:

public boolean animateAppearance(RecyclerView.ViewHolder viewHolder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
    if (preLayoutInfo != null && (preLayoutInfo.left != postLayoutInfo.left
            || preLayoutInfo.top != postLayoutInfo.top)) {
        return animateMove(viewHolder, preLayoutInfo.left, preLayoutInfo.top,
                postLayoutInfo.left, postLayoutInfo.top);
    } else {
        return animateAdd(viewHolder);
    }
}

逻辑很简单,如果 preLayoutInfo 不为空,并且 preLayoutInfo 和 postLayoutInfo 的 top、left 不同则调用 animateMove 否则调用 animateAdd;看名字也大致能理解是处理移除动画和添加动画;

对于 runPendingAnimations SimpleItemAnimator 还是没有实现;

DefaultItemAnimator

DefaultItemAnimator 继承自 SimpleItemAnimator,上述两个父类中都没有真正执行动画,那么执行动画一定在 DefaultItemAnimator 内部;在看其 runPendingAnimations 实现前先大概了解下类的结构;

// mPendingXXX 容器存放将要执行动画的 ViewHodler
private ArrayList<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<>();
private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<>();
// 这里的 MoveInfo,ChangeInfo 下面解释
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();

// mXXXAnimations 容器存放正在执行动画的 ViewHolder
ArrayList<RecyclerView.ViewHolder> mAddAnimations = new ArrayList<>();
ArrayList<RecyclerView.ViewHolder> mMoveAnimations = new ArrayList<>();
ArrayList<RecyclerView.ViewHolder> mRemoveAnimations = new ArrayList<>();
ArrayList<RecyclerView.ViewHolder> mChangeAnimations = new ArrayList<>();

// MoveInfo 额外存储了执行移除动画前后的坐标信息用于动画执行
private static class MoveInfo {
    public RecyclerView.ViewHolder holder;
    public int fromX, fromY, toX, toY;
    // ...
}

// ChangeInfo 想比于 MoveInfo 额外存储了 oldHolder
private static class ChangeInfo {
    public RecyclerView.ViewHolder oldHolder, newHolder;
    public int fromX, fromY, toX, toY;
    // ...
}

上面的代码逻辑很简单,注释都详细说明了,就不再解释了,最后来看看 animateRemoveImpl,animateMoveImpl 方法:

private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
    // 拿到 itemView
    final View view = holder.itemView;
    // 构建动画
    final ViewPropertyAnimator animation = view.animate();
    // 添加到正在执行动画的容器
    mRemoveAnimations.add(holder);
    // 执行动画
    animation.setDuration(getRemoveDuration()).alpha(0).setListener(
            new AnimatorListenerAdapter() {
                @Override
                public void onAnimationStart(Animator animator) {
                    // 开始执行动画回调
                    // SimpleItemAnimator 中默认空实现
                    dispatchRemoveStarting(holder);
                }

                @Override
                public void onAnimationEnd(Animator animator) {
                    // 动画结束后的一些操作
                    animation.setListener(null);
                    view.setAlpha(1);
                    // 当前 item 动画执行结束回调
                    dispatchRemoveFinished(holder);
                    mRemoveAnimations.remove(holder);
                    // 所有动画执行完成后的回调
                    // 内部通过判断上述各个容器是否为空触发回调
                    dispatchFinishedWhenDone();
                }
            }).start(); // 执行动画
}

void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
    final View view = holder.itemView;
    final int deltaX = toX - fromX;
    final int deltaY = toY - fromY;
    if (deltaX != 0) {
        view.animate().translationX(0);
    }
    if (deltaY != 0) {
        view.animate().translationY(0);
    }
    
    final ViewPropertyAnimator animation = view.animate();
    // 添加进容器
    mMoveAnimations.add(holder);
    animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animator) {
            // 动画开始回调
            dispatchMoveStarting(holder);
        }
        // ...
        @Override
        public void onAnimationEnd(Animator animator) {
            // 和 animateRemoveImpl 一样 就不重复说明了
            animation.setListener(null);
            dispatchMoveFinished(holder);
            mMoveAnimations.remove(holder);
            dispatchFinishedWhenDone();
        }
    }).start(); // 执行动画
}

DefaultItemAnimator 的代码也不难理解,这里仅仅贴出了重要部分代码进行解读,其余代码的阅读难度也不大,就不再细说了,对于自定义 ItemAnimator 仿照 DefaultItemAnimator 的逻辑实现即可。