美文网首页源码分析Android技术View
RecyclerView 全面的源码分析

RecyclerView 全面的源码分析

作者: rexyren | 来源:发表于2017-07-14 00:49 被阅读2037次

    RecyclerView 概要

    RecyclerView是Android 5.0开始提供一个可回收容器,它比 ListView更先进灵活更具扩展性,可高效重用和滚动,能方便定制各种布局和交互效果。它继承自 ViewGroup,那么它展示和交互也离不开 measure , layout , draw , touch ,adapter 五步。后面将以这五步来抽丝剥茧分析其工作原理,其中 LayoutManager 以最简单的 LinearLayoutManager为例。更多经典自定义 ViewGroup集合

    RecyclerView 的辅助类非常多,相联紧密的类都作为其内部类存在,如下图:

    recycler_class

    在这个类之外还有比较重要的三个类:AdapterHelper,ChildHelper,ViewInfoStore 。AdapterHelper负责处理 Adapter 里的数据集发生变化时的预处理;ChildHelper负责管理和访问 RecyclerView 的子视图;ViewInfoStore 记录 pre-layout 和 post-layout 阶段的 ViewHolder 状态信息,方便分析差异做相应的动画。

    RecyclerView 的使用

    首先需要引入 recyclerview-v7 的包,在 build.gradle的 dependencies 块中添加

    compile 'com.android.support:recyclerview-v7:25.2.0@aar'
    或
    compile(name: 'recyclerview-v7-25.2.0', ext: 'aar') //直接引用 aar 文件,需要flatDir中添加 aar 目录dirs。
    

    使用时自已的 YourAdapter 需要继承自 RecyclerView.Adapter,YourViewHolder 要继承RecyclerView.ViewHolder.
    除此对 RecyclerView 还需要设置 LayoutManager,SDK提供了常用的三种布局管理器LinearLayoutManager,GridLayoutManager,StaggeredGridLayoutManager。要设置divider或其它修饰可通过RecyclerView的addItemDecoration来定制。而事件可以在 Adapter 的onBindViewHolder里去绑定。总的来说 RecyclerView 的定制性是相当高的,用法就不多讲,下面重点讲解其工作原理。

    RecyclerView(25.2.0) 原理分析(五个维度深入)。

    <a name="part_measure" id="part_measure" /> 1.measure 过程,因为RecyclerView 的默认三大布局管理器的 mAutoMeasure 默认都是true,所以measure过程实际被LayoutManager接管。关键源码如下:

    protected void onMeasure(int widthSpec, int heightSpec) {
        ......
        if (mLayout.mAutoMeasure) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            if (skipMeasure || mAdapter == null) {
                return;
            }
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }
            ......
            dispatchLayoutStep2();                      
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            ......
        }else{
            ......
        }
    

    首先调用了LayoutManager的onMeasure,内部只是调用了RecyclerView的defaultOnMeasure,这一步实质是在忽略子视图情况下测绘容器大小。当RecyclerView本身的宽和高都是精确值即没有wrap_content时skipMeasure为true 此时子视图的测绘将会延迟到 onLayout 过程,后续会讲到。skipMeasure 为 true 或 adapter 为空时都是没有必要测绘子视图来决定自身大小的。不确定自身大小(存在wrap_content)才会走后续的 dispatchLayoutStep1()和 dispatchLayoutStep2()方法。而测绘和布局子视图就是在dispatchLayoutStep2这一步完成的。当调用mLayout.setMeasuredDimensionFromChildren,内部会调用 RecyclerView.setMeasuredDimension方法从而定格了 RecyclerView 自身容器的大小。

    注:dispatchLayoutStep2在onLayout 中也有调用稍后会讲到其具体实现

    <a name="part_layout" id="part_layout" /> 2.layout 过程调用了dispatchLayout,它会根据mState.mLayoutStep的值调用不同的dispatchLayout步骤,源码概要如下:

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

    可以发现 layout 过程也像 onMeasure 里有类似调用 即dispatchLayoutStep1和dispatchLayoutStep2. 这是因为RecyclerView对子视图的 measure 和 layout 都是通过dispatchLayoutStep2这个方法一起完成的。当然我们不必担心它会重复调用了,因为还有一个mState.mLayoutStep来控制测绘步骤呢。当值为STEP_START时会调用dispatchLayoutStep1之后赋值mLayoutStep = State.STEP_LAYOUT,当值为STEP_LAYOUT时才会调用dispatchLayoutStep2然后再赋值mLayoutStep = State.STEP_ANIMATIONS,而当值为STEP_ANIMATIONS时才会调用dispatchLayoutStep3然后重新赋值为STEP_START。确保 onMeasure 到 onLayout 中各步骤只会有一次。

    <a name="part_measure_layout" id="part_measure_layout" /> 3.重点分析dispatchLayoutStep三个方法和 Recycler 类。

    每一次数据集变化或是调用了 requestLayout 都会走dispatchLayoutStep这几个方法。这个方法也是 RecyclerView 对子视图填充布局和做动画的核心所在。上面在 onMeasure 和 onLayout 时也多次出现,以下就讲综合讲述它们各阶段的作用 。
    ��回顾 measure回顾 layout

    其中最重要的是实现了子视图测量和填充的方法是dispatchLayoutStep2,它是,下面我们先讲
    dispatchLayoutStep1和dispatchLayoutStep3,这两个方法都是与执行不同操作的动画紧密相关且相对简单些(动作如 Adapter 的 add、remove、insert 等)。

    (1).dispatchLayoutStep1 布局预处理,step3 做动画的基础。

    private void dispatchLayoutStep1() {
          ......
          mViewInfoStore.clear();
          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;
                  }
                  ......
                  mViewInfoStore.addToPreLayout(holder, animationInfo);
                  if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                          && !holder.shouldIgnore() && !holder.isInvalid()) {
                      long key = getChangedHolderKey(holder);
                      ......
                      mViewInfoStore.addToOldChangeHolders(key, holder);
                  }
              }
          }
          if (mState.mRunPredictiveAnimations) {
            ......
          } 
          ......
          mState.mLayoutStep = State.STEP_LAYOUT;
    }
    

    这部分主要是对已经存在容器里的 View 的 ViewHolder预处理信息分类暂时存放到mViewInfoStore中,为后面做动画有个差异分析提供基础。

    (2). dispatchLayoutStep3 布局 post 处理,执行相应动作的动画。

    
    private void dispatchLayoutStep3() {
        ......
        mState.mLayoutStep = State.STEP_START;
        if (mState.mRunSimpleAnimations) { 
                for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                    ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                    ......
                    long key = getChangedHolderKey(holder);
                    final ItemHolderInfo animationInfo = mItemAnimator
                            .recordPostLayoutInformation(mState, holder);
                    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) {
                            mViewInfoStore.addToPostLayout(holder, animationInfo);
                        } else {
                            final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
                                    oldChangeViewHolder);
                            mViewInfoStore.addToPostLayout(holder, animationInfo);
                            ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
                            animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
                                                            oldDisappearing, newDisappearing);
                        }
                    } else {
                        mViewInfoStore.addToPostLayout(holder, animationInfo);
                    }
                }
                mViewInfoStore.process(mViewInfoProcessCallback);
        }
        .....
    }
    

    经过dispatchLayoutStep2重新填充后视图会更新或多或少,通过对比 dispatchLayoutStep1 阶段暂存在mViewInfoStore中的 pre-layout 信息,做一些处理后将当前ViewHolder和动画信息添加到mViewInfoStore的 post-layout 中。这两个阶段 addToPreLayout 和 addToPostLayout 都会添加相应的标记,最后调用mViewInfoStore.process(mViewInfoProcessCallback)在内部进行处理后并回调才执行相应操作的动画。见下面动画预处理后的回调处源码:

    private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
                new ViewInfoStore.ProcessCallback() {
            @Override
            public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
                    @Nullable ItemHolderInfo postInfo) {
                mRecycler.unscrapView(viewHolder);
                animateDisappearance(viewHolder, info, postInfo);
            }
            @Override
            public void processAppeared(ViewHolder viewHolder,
                    ItemHolderInfo preInfo, ItemHolderInfo info) {
                animateAppearance(viewHolder, preInfo, info);
            }
            ......
    }        
    

    <a name="part_dispatchlayout" id="part_dispatchlayout" /> (3). dispatchLayoutStep2 正真的测量和填充子视图。

    RecyclerView 的子视图测量和布局,最终还是委托给了 LayoutManager 来处理见源码:

    private void dispatchLayoutStep2() {
        ......
        mLayout.onLayoutChildren(mRecycler, mState);
        ......
    }
    
    //LinearLayoutManager 的onLayoutChildren方法
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        ......
        updateAnchorInfoForLayoutExpose(state, mAnchorInfo);
        ......
        detachAndScrapAttachedViews(recycler);
        if (mAnchorInfo.mLayoutFromEnd) {
            // fill towards start 和下面的逻辑分支是反过来的,仅看下面的逻辑为讲解。
            ......
        } else {
            // fill towards end
            updateLayoutStateToFillEndExpose(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            // fill towards start
            updateLayoutStateToFillStartExpose(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
        }
        ......
    }
    
    • 第一步找锚点和位置确定相对坐标和 item 位置;
    • 对容器上的 View 做detach 和回收处理见调用detachAndScrapAttachedViews,回收过程讲解
    • 填充 View 到可用空间,以锚点为界,在布局方向以相反的两个方向扩张来填充可用空间,单向填充算法.

    以描点为界向两个方向填充的算法示意图如下,其中红色点为描点位置:

    anchor_fill.jpg

    <a name="part_fillchild" id="part_fillchild" /> 初始化时无描点的 itemView 此时按 (1)或(2)进行,当长视图滑动时填充就会出现(3)示意的填充。LinearLayoutManager单一方向的填充算法源码如下:

    protected int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
                           RecyclerView.State state, boolean stopOnFocusable) {
        ......
        if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
            ......
            recycleByLayoutStateExpose(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        while (remainingSpace > 0 && layoutState.hasMore(state)) {
            layoutChunkResultCache.resetInternal();
            layoutChunk(recycler, state, layoutState, layoutChunkResultCache);
            if (layoutChunkResultCache.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResultCache.mConsumed * layoutState.mLayoutDirection;
            if (!layoutChunkResultCache.mIgnoreConsumed || mLayoutState.mScrapList != null
               || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResultCache.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResultCache.mConsumed;
            }         ......
        }
        if (DEBUG) {
            validateChildOrderExpose();
        }
        return start - layoutState.mAvailable;
    }
    

    首先会通过recycleByLayoutStateExpose方法移除已经滑出屏幕以外的子视图,然后通过一个循环的朝着一个布局方向每次调用layoutChunk添加 View,并适时调节剩余空间和下次 layout 的 offset,直到剩下的空间不足循环结束。而子视图逐个的测量和布局就是在layoutChunk这个方法里进行的,看下面部分源码:

    protected void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                                   LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        ......
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            // can not find in scrapList
            if (mShouldReverseLayoutExpose == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        }
        ......
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (getOrientation() == 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;
            }
        } else {
        ......
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
                right - params.rightMargin, bottom - params.bottomMargin);
        ......
        result.mFocusable = view.isFocusable();
    }
    

    此方法通过layoutState.next(recycler)复用机制获取下一个需要布局的 View, 将它通过 ViewGroup 的 addView 方法添加到 RecyclerView 里面来,然后通过measureChildWithMargins(view, 0, 0)测量它的大小,最后通过layoutDecorated方法实际上调用了 child.layout(left,top,right,bottom)来把子视图布局到相应的位置。

    <a name="part_recycler" id="part_recycler" /> (4).复用回收过程是Recycler负责的,涉及 ViewHolder 的创建和回收,它是 RecyclerView的内部类,具有三级的缓存结构外加一个扩展缓存接口:

    public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;
    
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
    
        private final List<ViewHolder>
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
    
        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;
    
        RecycledViewPool mRecyclerPool;
    
        private ViewCacheExtension mViewCacheExtension;
        ......
    }
    

    先讲View和ViewHolder 的创建。接上文,layoutChunk 添加View时调用了layoutState.next(recycler)来获取下个 View,实际调用了 Recycler的getViewForPosition方法。

    public View next(RecyclerView.Recycler recycler) {
        if (mScrapList != null) {
            return nextFromLimitedList();
        }
        final View view = recycler.getViewForPosition(mCurrentPosition);
        mCurrentPosition += mItemDirection;
        return view;
    }
    //以下 next 会调用下面方法。
    View getViewForPosition(int position, boolean dryRun) {
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
    

    最终是调用Recycler的tryGetViewHolderForPositionByDeadline 这个方法分以下几步:

    (1)检查mChangedScrap,若匹配到则返回相应holder
    (2)检查mAttachedScrap,若匹配到且holder有效则返回相应holder
    (3)检查mViewCacheExtension,若匹配到则返回相应holder
    (4)检查mRecyclerPool,若匹配到则返回相应holder
    (5)否则执行Adapter.createViewHolder(),新建holder实例
    (6)返回holder再通过 holder.itemView 得到了要添加的 View.

    此方法比较长,看下面关键源码:

    ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                    boolean dryRun, long deadlineNs) {
        ......
        ViewHolder holder = null;
        // 0) If there is a changed scrap, try to find from there
        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 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 && 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
                holder = getRecycledViewPool().getRecycledView(type);
                ......
            }
            if (holder == null) {
                ......
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
                ......
            }
        }
        ......
        boolean bound = false;
        if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
            if (DEBUG && holder.isRemoved()) {
                throw new IllegalStateException("Removed holder should be bound and it should"
                        + " come here only in pre-layout. Holder: " + holder);
            }
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
        }
        ......
        return holder;
    }
    

    基本上就是通过各级缓存获取 ViewHolder,获取不到就调用mAdapter.createViewHolder 创建 。最终的 ViewHolder 如果需要绑定就会调用tryBindViewHolderByDeadline来绑定数据,其内部调用了mAdapter.bindViewHolder。

    下面我们再看看 Recycler 的回收过程,

    • 在重新设置 Adapter 或通知某个item 变法时,相应的 ViewHolder 会加上标志位如UPDATE,REMOVE 等,待onLayoutChildren时就会先调用detachAndScrapAttachedViews其最终也是调用了Recycler 的的recycleViewHolderInternal方法。

    • 滑动过程中不断回收不可见的 ViewHolder,见fill 方法中的recycleByLayoutStateExpose,最终是调用了LayoutManager 的 removeAndRecycleViewAt。

        //先移除再回收,
        public void removeAndRecycleViewAt(int index, Recycler recycler) {
            final View view = getChildAt(index);
            removeViewAt(index);
            recycler.recycleView(view);
        }
    

    而 removeViewAt 是通过帮助类ChildHelper来移除的,最终回收仍是调用了Recycler 的recycleViewHolderInternal

    回收关键方法recycleViewHolderInternal很长,关键源码如下:

    void recycleViewHolderInternal(ViewHolder holder) {
        ......
        if (forceRecycle || holder.isRecyclable()) {
            if (mViewCacheMax > 0
                    && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                            | ViewHolder.FLAG_REMOVED
                            | ViewHolder.FLAG_UPDATE
                            | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                ......
                mCachedViews.add(targetCacheIndex, holder);
                cached = true;
            }
            if (!cached) {
                addViewHolderToRecycledViewPool(holder, true);
                recycled = true;
            }
        }
        ......
        mViewInfoStore.removeViewHolder(holder);
     }
    

    对于移除的 View 如果是有效的无update 或remove 动作就添加到mCachedViews中,否则失效的 ViewHolder 会添加到RecycledViewPool中。

    在 Recycler的各级缓存中,只有RecycledViewPool是按照 viewType 去存储和获取缓存的 ViewHolder 来达到复用,其它缓存需要匹配 layoutPosition 或 getItemId再加上 viewType 。缓存级别最高的是mChangedScrap它是预处理layout时添加的,对应方法 dispatchLayoutStep1;mAttachedScrap是其次,它是attach到容器未移除的缓存复用很适用频繁抖动滑动;mCachedViews再次,它是 detach 的 ViewHolder 缓存,mViewCacheExtension是用户扩展的缓存池一般用户是没有实现的,最后才是 RecyclerViewPool它只需要按viewType 返回。

    4.draw 过程比较简单,只是简单循环调用了ItemDecoration去绘制背景和前景视图,

    (1).以下是ItemDecoration接口的设置和内部使用的代码。

    public void addItemDecoration(ItemDecoration decor) {
        addItemDecoration(decor, -1);
    }
    
    public void onDraw(Canvas c) {
        super.onDraw(c);
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }
        @Override
    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);
        }
        ......
     }
    

    (2). ItemDecoration 的灵活性比较高,主要需要用户实现 onDraw,onDrawOver 和 getItemOffset三个方法,且都不是必须的。

     public static abstract class ItemDecoration {
        public void onDraw(Canvas c, RecyclerView parent, State state){ }
        public void onDrawOver(Canvas c, RecyclerView parent, State state){}
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {}
     }
    

    在对 itemView 画装饰时候,为了实现 Item 之间的间隔或是避免画的内容覆盖到 itemView 上,就需要通过getItemOffsets返回,itemView 的需要预留出的四边内间距,如下视图。

    item_decoration

    整个 item 所占的空间也就是最外面虚线的边界,在 measure 过程中这部分其实也是考虑进去了。

    5.手势滑动,RecyclerView 的手势滑动和大多数滑动控件类似处理了onInterceptTouchEvent 和 onTouchEvent事件。

    fling平滑滑动过程是借助ViewFlinger不断postOnAnimation 内部调用ScrollerCompat工具进行计算,每次滑动 dx,dy,这就和ACTION_MOVE 时的滑动一致了。
    最终手势滑动或 fling 滑动都会回调给 LayoutManager的scrollHorizontallyBy(dx...)或scrollVerticallyBy(dy...)内部又会调用 scrollBy 如下

    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        ......
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        ......
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }
    

    可见滑动过程也会调用 LinearLayoutManager 的 fill 方法,上面讲过,内部会回收移除屏幕的 View和 ViewHolder,并填充新的View 和 ViewHolder.

    6.进阶 Adapter和数据集的更新过程.

    每次重新设置不同的 Adapter 或是更改 Adapter 里的数据集合并调用 notifyItemXXX 时,最终都会触发当前存在的 ViewHolder里的标记位发生变化。加上FLAG_UPDATE, FLAG_REMOVE等不同操作的标记位。然后再 requestLayout 后会重新走 上面讲到的 dispatchLayoutStep 的三个流程。界面的变化更新也主要是在第dispatchLayoutStep2 后完成。

    (1).Adapter 发生变化时

    Adapter 改变后比较简单,直接重置所有无效。清除所有的缓存 ViewHolder 调用了removeAndRecycleViews();并且调用markKnownViewsInvalid标记了容器中的所有 View 无效(FLAG_UPDATE)。

    (2).Adapter 数据合集操作并调用相应的notifyItemXXX 方法。

    在设置 Adapter 给 RecyclerView 时就会绑定一个数据变化时的观察者DataObserver。当更改数据集后用户需要调用
    notifyItemChanged,notifyItemRemoved,notifyItemInsert 等方法,然后DataObserver就会接收到相应的 onItemChaged,onItemRemoved,onItemInsert 等回调并由AdapterHelper对应的onItemXXX来接管。在AdapterHelper内每个操作都对应一个UpdateOp

        static class UpdateOp {
            static final int ADD = 1;
            static final int REMOVE = 1 << 1;
            static final int UPDATE = 1 << 2;
            static final int MOVE = 1 << 3;
            static final int POOL_SIZE = 30;
    
            int cmd;
            int positionStart;
            ......
        }
    

    AdapterHelper的onItemXXX 方法是有有bool型的返回值,为true表示需要处理触发triggerUpdateProcessor调用,实际post 执行以下 Runnable:

        final Runnable mUpdateChildViewsRunnable = new Runnable() {
            @Override
            public void run() {
                if (!mFirstLayoutComplete || isLayoutRequested()) {
                    // a layout request will happen, we should not do layout here.
                    return;
                }
                if (!mIsAttached) {
                    requestLayout();
                    // if we are not attached yet, mark us as requiring layout and skip
                    return;
                }
                if (mLayoutFrozen) {
                    mLayoutRequestEaten = true;
                    return; //we'll process updates when ice age ends.
                }
                consumePendingUpdateOperations();
            }
        };
    

    没有首次layout 过,或是layout 后续即将触发时直接return ,这里就防止了频繁操作会多次执行操作。consumePendingUpdateOperations方法,调用AdapterHelper的preProcess进行预处理内部预处理暂存的一系列 UpdateOp 对象后再通过callback 回调给RecyclerView,而RecyclerView 再传给了 Recycler 处理,即对 ViewHolder 添加不同的标记。
    最后调用了dispatchLayout 进行重新执行 dispatchLayoutStep 三步来更新视图。在dispatchLayoutStep1 中也会调用processAdapterUpdatesAndSetAnimationFlags来处理mAdapterHelper 中的UpdateOp.

    只要 ViewHolder 有了标记后在下一个requestLayout 期间会调用到onMeasure,onLayout最终会触发dispatchLayoutStep三步,在dispatchLayoutStep2 里会更新视图,而在dispatchLayoutStep 3 里会播放相应操作的动画 。

    总结

    至此 RecyclerView 工作的源码分析基本完成,其相关辅助类比较多,源码很长,不对或不详尽之处请大家多提意见,后续再分享 VLayout,欢迎拍砖.更多经典自定义 ViewGroup集合

    相关文章

      网友评论

      • boboyuwu:为什么onLayout那里要调用二次fill()呢
        boboyuwu:@rexyren 大兄弟我可以加你qq探讨下吗
        rexyren: @boboyuwu 它的layout算法就是从锚点位置从布局方向和布局的反方向填充 view 所以是两次。
      • 蓅哖伊人为谁笑:华而不实,几乎就时代码流程的赋值粘贴,三级缓存的ArrayList<ViewHolder> mChangedScrap = null;

        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

        private final List<ViewHolder> mUnmodifiableAttachedScrap =Collections.unmodifiableList(mAttachedScrap);
        什么时候调用,这么设计目的是什么,都没有讲
        看完,懂得人自然懂,不懂得依旧不懂。
        rexyren: @蓅哖伊人为谁笑 既然你开口了 我有时间专写一篇回收机制😃
        蓅哖伊人为谁笑:@rexyren 期待你的下一篇讲解哈
        rexyren: @蓅哖伊人为谁笑 确实细节都没有讲,就是讲的大体流程。缓存回收复用这块和ViewHolder的各种标记位相关,需要大篇幅讲解。
      • 开发者头条_程序员必装的App:感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/m5h7tr 欢迎点赞支持!
        欢迎订阅《rexy的独家号》https://toutiao.io/subjects/12288

      本文标题:RecyclerView 全面的源码分析

      本文链接:https://www.haomeiwen.com/subject/fdtnhxtx.html