美文网首页
RecyclerView源码解析

RecyclerView源码解析

作者: 莫库施勒 | 来源:发表于2019-06-04 20:09 被阅读0次

    首先是RecyclerView的一般用法

    recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
    LinearLayoutManager layoutManager = new LinearLayoutManager(this); 
    
    //设置布局管理器 
    recyclerView.setLayoutManager(layoutManager); 
    
    //设置为垂直布局,这也是默认的 
    layoutManager.setOrientation(OrientationHelper. VERTICAL); 
    
    //设置Adapter 
    recyclerView.setAdapter( recycleAdapter); 
    
    //设置分隔线 
    recyclerView.addItemDecoration( new DividerGridItemDecoration(this )); 
    
    //设置增加或删除条目的动画 
    recyclerView.setItemAnimator( new DefaultItemAnimator());
    
    1. constructor 构造函数根据设置属性进行了初始化工作,如果设置了 layoutManager 属性,也会通过反射实例化 layoutManager。
    2. setLayoutManager 会先清空之前缓存的View,通过 requestLayout 通知执行测量与绘制 onMeasure、onLayout、onDraw。
      RecyclerView 会将测量与布局交给 LayoutManager 来做,并且 LayoutManager 有一个 mAutoMeasure 的属性来控制自动测量,否则 LayoutManager 就要重写 onMeasure 来处理测量工作
        public void setLayoutManager(LayoutManager layout) {
            if (layout == mLayout) {
                return;
            }
            stopScroll();
            // TODO We should do this switch a dispatchLayout pass and animate children. There is a good
            // chance that LayoutManagers will re-use views.
            if (mLayout != null) {
                // end all running animations
                if (mItemAnimator != null) {
                    mItemAnimator.endAnimations();
                }
                mLayout.removeAndRecycleAllViews(mRecycler);
                mLayout.removeAndRecycleScrapInt(mRecycler);
                mRecycler.clear();
    
                if (mIsAttached) {
                    mLayout.dispatchDetachedFromWindow(this, mRecycler);
                }
                mLayout.setRecyclerView(null);
                mLayout = null;
            } else {
                mRecycler.clear();
            }
            // this is just a defensive measure for faulty item animators.
            mChildHelper.removeAllViewsUnfiltered();
            mLayout = layout;
            if (layout != null) {
                if (layout.mRecyclerView != null) {
                    throw new IllegalArgumentException("LayoutManager " + layout
                            + " is already attached to a RecyclerView: " + layout.mRecyclerView);
                }
                mLayout.setRecyclerView(this);
                if (mIsAttached) {
                    mLayout.dispatchAttachedToWindow(this);
                }
            }
            mRecycler.updateViewCacheSize();
            requestLayout();
        }
    
    
    1. onMeasure 如果宽高都为 EXACTLY是,可以直接设置对应的宽高,然后return,也就是 skipMeasure。
        @Override
        protected void onMeasure(int widthSpec, int heightSpec) {
            if (mLayout == null) {
                defaultOnMeasure(widthSpec, heightSpec);
                return;
            }
            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();
                }
                // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
                // consistency
                mLayout.setMeasureSpecs(widthSpec, heightSpec);
                mState.mIsMeasuring = true;
                dispatchLayoutStep2();
    
                // now we can get the width and height from the children.
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
    
                // if RecyclerView has non-exact width and height and if there is at least one child
                // which also has non-exact width & height, we have to re-measure.
                if (mLayout.shouldMeasureTwice()) {
                    mLayout.setMeasureSpecs(
                            MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                            MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
                    mState.mIsMeasuring = true;
                    dispatchLayoutStep2();
                    // now we can get the width and height from the children.
                    mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
                }
            } else {
                if (mHasFixedSize) {
                    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                    return;
                }
                ...
           }
    }
    
    1. 如果不是 EXACTLY, onMeasure 就会根据 MeasureSpec 处理布局。dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3来布局,其中布局状态保存在 RecyclerView.State
      RecyclerView.State
      这个类封装了当前RecyclerView的有用信息。
    public static class State {
           @IntDef(flag = true, value = {
                    STEP_START, STEP_LAYOUT, STEP_ANIMATIONS
            })
            @Retention(RetentionPolicy.SOURCE)
            @interface LayoutState {}
    
            @LayoutState
            int mLayoutStep = STEP_START; // RecyclerView 的布局状态
    }
    

    step1负责记录状态,step2负责布局,step3则与step1进行比较,根据变化来触发动画。
    RecyclerView是支持 WRAP_CONTENT 属性的,比如我们可以很容易的在 RecyclerView 的下面放置其它的View,RecyclerView 会根据子 View 所占大小动态调整自己的大小,这时候,RecyclerView 就会将子控件的 measure 与 layout 提前到 Recycler 的 onMeasure 中,因为它需要确定子空间的大小与位置后,再来设置自己的大小。所以这时候就会在 onMeasure 中完成 step1 与 step2。否则,就需要在 onLayout 中去完成整个布局过程

    1. onLayout
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            Trace.beginSection(TRACE_ON_LAYOUT_TAG);
            dispatchLayout();
            Trace.endSection();
            mFirstLayoutComplete = true;
        }
    

    这里我们看到如果在onMeasure中已经完成了step1与step2,则只会执行step3,否则三步会依次触发。
    现在我们来分析一下这三步

    • step1
      它的目的就是记录View的状态,首先遍历当前所有的View依次进行处理,mItemAnimator会根据每个View的信息封装成一个ItemHolderInfo,这个ItemHolderInfo中主要包含的就是当前View的位置状态等。然后ItemHolderInfo 就被存入mViewInfoStore中
    private void dispatchLayoutStep1(){   
        ...
        if (mState.mRunSimpleAnimations) {
              int count = mChildHelper.getChildCount();
              for (int i = 0; i < count; ++i) {
                  final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                  final ItemHolderInfo animationInfo = mItemAnimator
                          .recordPreLayoutInformation(mState, holder,
                                  ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                  holder.getUnmodifiedPayloads());
                  mViewInfoStore.addToPreLayout(holder, animationInfo);
                  ...
              }
        }
        ...
        mState.mLayoutStep = State.STEP_LAYOUT;
    }
    

    数据被存储到 ViewInfoStore 中的,可以查看ArrayMap介绍

    class ViewInfoStore {
        @VisibleForTesting
        final ArrayMap<ViewHolder, InfoRecord> mLayoutHolderMap = new ArrayMap<>();
    
        @VisibleForTesting
        final LongSparseArray<ViewHolder> mOldChangedHolders = new LongSparseArray<>();
        // ArrayMap和SparseArray一样,也会对key使用二分法进行从小到大排序,
        // 在添加、删除、查找数据的时候都是先使用二分查找法得到相应的index,
        // 然后通过index来进行添加、查找、删除等操作,
        // ArrayMap 的 key 可以是任意值, SparseArray 的 key 只能为int,
    }
    

    其中 addToPreLayout 的功能就是利用 ArrayMap 的二分查找先快速根据holder来查询InfoRecord信息,如果没有,则生成,然后将info信息赋值给InfoRecord的preInfo变量。最后标记FLAG_PRE信息。

    void addToPreLayout(ViewHolder holder, ItemHolderInfo info) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        if (record == null) {
            record = InfoRecord.obtain();
            mLayoutHolderMap.put(holder, record);
        }
        record.preInfo = info;
        record.flags |= FLAG_PRE;
    }
    
    // 主要包含的就是当前View的位置状态等
    public static class ItemHolderInfo {
        public int left;
        public int top;
        public int right;
        public int bottom;
        @AdapterChanges
        public int changeFlags;
        public ItemHolderInfo() {
        }
    }
    
    • step2
    private void dispatchLayoutStep2() {
        ...
        mLayout.onLayoutChildren(mRecycler, mState);
         ...
        mState.mLayoutStep = State.STEP_ANIMATIONS;
    }
    

    onLayoutChildren

    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {    
        ...
        if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||
                mPendingSavedState != null) {
            // 找到 anchor 点
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        }
        ...
        // 确定布局方向
        if (mAnchorInfo.mLayoutFromEnd) {
            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :
                    LayoutState.ITEM_DIRECTION_HEAD;
        } else {
            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
                    LayoutState.ITEM_DIRECTION_TAIL;
        }
        ...
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
        ...
        if (mAnchorInfo.mLayoutFromEnd) {
           ...
        } else {
            // 根据anchor一直向 End 方向布局,直至填充满anchor点前面的所有区域
            updateLayoutStateToFillEnd(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);
              ...     
            // 根据anchor一直向 Start 方向布局,直至填充满anchor点后面的所有区域
            updateLayoutStateToFillStart(mAnchorInfo);
              ...
            fill(recycler, mLayoutState, state, false);
            ...
        }
        ...
    }
    

    确定 anchor 是通过 updateAnchorFromChildren,首先寻找被focus的child,找到的话以此child作为anchor,否则根据布局的方向寻找最合适的child来作为anchor,如果找到则将child的信息赋值给anchorInfo,其实anchorInfo主要记录的信息就是view的物理位置与在adapter中的位置。找到后返回true,否则返回false则交由上一步的函数做处理。
    还可以通过重写onAnchorReady方法,来实现定位。
    确定布局方向后就调用 fill 进行填充

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // 根据当前信息对不需要的 View 进行回收
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ...
        }
        return start - layoutState.mAvailable;
     }
    

    回收 View,回收的是逃离边界的 View,recycleViewsFromStart -> recycleChildren -> removeAndRecycleViewAt

    public void removeAndRecycleViewAt(int index, Recycler recycler) {
        final View view = getChildAt(index);
        removeViewAt(index);
        recycler.recycleView(view);
    }
    

    接下来是 layoutChunk


    layoutChunk获取缓存
    • step3
    private void dispatchLayoutStep3() {
        mState.mLayoutStep = State.STEP_START;
        if (mState.mRunSimpleAnimations) {
            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                ...
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPostLayoutInformation(mState, holder);
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
            }
            mViewInfoStore.process(mViewInfoProcessCallback);
        }
        ...
    }
    

    这个方法在 onLayout 中执行,子View 已经布局完成,通过 recordPostLayoutInformation 和 addToPostLayout ,来记录布局之后的状态。最后调用 process 方法,这个方法就是根据mViewInfoStore中的View信息,来执行动画逻辑。
    接下来就是draw

    @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);
        }
        ...
    }
    @Override
    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);
        }
    }
    
    1. Adapter
      adapter 根据 data 返回绑定的 ViewHolder,同时在 setAdapter 时还注册了 RecyclerViewDataObserver ,可以在 notify 的时候,就会通知更新 View,在 setAdapter 还作了:
    • 如果之前存在 Adapter,先移除原来的,注销观察者,和从RecyclerView Detached。
    • 然后根据参数,决定是否清除原来的 ViewHolder
    • 然后重置 AdapterHelper,并更新 Adapter,注册观察者。
      现在来分析 RecyclerViewDataObserver
    private class RecyclerViewDataObserver extends AdapterDataObserver {
        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            // 1) 断言不在布局或者滚动过程中
            assertNotInLayoutOrScroll(null);
            // 2) 这里小心,不要小看if括号中的内容,这是关键。我们去看看这个方法的实现。
            // 见下面注释 3),在 3) 返回true之后执行triggerUpdateProcessor方法,
            // triggerUpdateProcessor方法分析请看注释 4)。
            if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
                triggerUpdateProcessor();
            }
        }
        // 4) 触发更新处理操作,分为两种情况,在 版本大于16 且 已经Attach 并且 设置了大小固定 的情况下,
        // 进行mUpdateChildViewsRunnable中的操作。否则请求布局。
            void triggerUpdateProcessor() {
                if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
                    RecyclerView.this.postOnAnimation(mUpdateChildViewsRunnable);
                } else {
                    mAdapterUpdateDuringMeasure = true;
                    requestLayout();
                }
            }
    }
        // 5) 其中核心代码是consumePendingUpdateOperations()
        final Runnable mUpdateChildViewsRunnable = new Runnable() {
            @Override
            public void run() {
                ...
                consumePendingUpdateOperations();
            }
        };
    
    private void consumePendingUpdateOperations() {
        ...
        if (mAdapterHelper.hasAnyUpdateTypes(UpdateOp.UPDATE) && !mAdapterHelper
                .hasAnyUpdateTypes(UpdateOp.ADD | UpdateOp.REMOVE | UpdateOp.MOVE)) {
            // 6) 如果只有更新类型的操作(这里指内容的更新,不影响View位置的改变)的情况下,
            // 先进行预处理,然后在没有View更新的情况下消耗延迟的更新操作,否则调用
            // dispatchLayout方法对RecyclerView中的View重新布局。那么接下来分析
            // preProcess()方法。
            mAdapterHelper.preProcess();
            if (!mLayoutRequestEaten) {
                if (hasUpdatedView()) {
                    dispatchLayout();
                } else {
                    mAdapterHelper.consumePostponedUpdates();
                }
            }
            resumeRequestLayout(true);
        } else if (mAdapterHelper.hasPendingUpdates()) {
            // 7) 在既有更新操作又有添加或者删除或者移动中任意一个的情况下,调用
            // dispatchLayout方法对RecyclerView中的View重新布局
            dispatchLayout();
        }
    }
    
    // 8) 预处理做了以下几件事情,<1> 先将待处理操作重排。<2> 应用所有操作 <3> 清空待处理操作列表,
    // 以ADD为例分析流程。
    void preProcess() {
        mOpReorderer.reorderOps(mPendingUpdates);
        final int count = mPendingUpdates.size();
        for (int i = 0; i < count; i++) {
            UpdateOp op = mPendingUpdates.get(i);
            switch (op.cmd) {
                case UpdateOp.ADD:
                    applyAdd(op);
                    break;
                case UpdateOp.REMOVE:
                    applyRemove(op);
                    break;
                case UpdateOp.UPDATE:
                    applyUpdate(op);
                    break;
                case UpdateOp.MOVE:
                    applyMove(op);
                    break;
            }
            if (mOnItemProcessedCallback != null) {
                mOnItemProcessedCallback.run();
            }
        }
        mPendingUpdates.clear();
    }
    // 9) 直接看 postponeAndUpdateViewHolders
    private void applyAdd(UpdateOp op) {
        postponeAndUpdateViewHolders(op);
    }
    // 10) 先将操作添加到推迟的操作列表中。然后将操作的内容交给回调处理。
    private void postponeAndUpdateViewHolders(UpdateOp op) {
        mPostponedList.add(op);
        switch (op.cmd) {
            case UpdateOp.ADD:
                mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
                break;
            case UpdateOp.MOVE:
                mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
                break;
            case UpdateOp.REMOVE:
                mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
                        op.itemCount);
                break;
            case UpdateOp.UPDATE:
                mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
                break;
            default:
                throw new IllegalArgumentException("Unknown update op type for " + op);
        }
    }
    // 11) 直接看offsetPositionRecordsForInsert
    @Override
    public void offsetPositionsForAdd(int positionStart, int itemCount) {
        offsetPositionRecordsForInsert(positionStart, itemCount);
        mItemsAddedOrRemoved = true;
    }
    // 12) 该方法主要是遍历所有的ViewHolder,然后把在插入位置之后的ViewHolder的位置
    // 向后移动插入的个数,最后在对Recycler中缓存的ViewHolder做同样的操作,最后申请重新布局。
    void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
        final int childCount = mChildHelper.getUnfilteredChildCount();
        for (int i = 0; i < childCount; i++) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
            if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) {
                holder.offsetPosition(itemCount, false);
                mState.mStructureChanged = true;
            }
        }
        mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount);
        requestLayout();
    }
    
    class AdapterHelper implements OpReorderer.Callback {
        // 3) 该方法将插入操作的信息存储到一个UpdateOp中,并添加到待处理更新操作列表 mPendingUpdates 中,
        // 如果操作列表中的值是1,就返回真表示需要处理操作,等于1的判断避免重复触发处理操作。
        // obtainUpdateOp内部是通过池来得到一个UpdateOp对象。那么下面回去看我们注释 4)。
        boolean onItemRangeInserted(int positionStart, int itemCount) {
            if (itemCount < 1) {
                return false;
            }
            mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount, null));
            mExistingUpdateTypes |= UpdateOp.ADD;
            return mPendingUpdates.size() == 1;
        }
    }
    

    注意,在上面 4) 步的时候有个优化 POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached ,就是可以通过 setHasFixedSize(true),来减少一定程度的计算,绕过requestlayout,只走自身的布局流程,提高性能。

    1. 滑动
      RecyclerView的滑动过程可以分为2个阶段:手指在屏幕上移动,使RecyclerView滑动的过程,可以称为scroll;手指离开屏幕,RecyclerView继续滑动一段距离的过程,可以称为fling。
      我们可以参考下图
      RecyclerView滑动过程
      1. 当RecyclerView接收到ACTION_MOVE事件后,会先计算出手指移动距离(dy),并与滑动阀值(mTouchSlop)比较,当大于此阀值时将滑动状态设置为SCROLL_STATE_DRAGGING,而后调用scrollByInternal()方法,使RecyclerView滑动,这样RecyclerView的滑动的第一阶段scroll就完成了。其中 scrollByInternal调用的是 LinearLayoutManager.scrollBy方法
      2. 当接收到ACTION_UP事件时,会根据之前的滑动距离与时间计算出一个初速度yvel,这步计算是由 VelocityTracker 实现的,然后再以此初速度,调用方法fling(),完成RecyclerView滑动的第二阶段fling。fling 方法调用的是 mViewFlinger.fling,其实借助的是 Scroller 实现的
        postOnAnimation()保证了RecyclerView的滑动是流畅,这里涉及到著名的“android 16ms”机制,简单来说理想状态下,上段代码会以16毫秒一次的速度执行,也就是每16毫秒平移一次。
        我们再看看 scrollBy 方法
    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        final int absDy = Math.abs(dy);
        updateLayoutState(layoutDirection, absDy, true, state);
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        ...
        final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
        // mOrientationHelper.offsetChildren()作用就是平移ItemView
        mOrientationHelper.offsetChildren(-scrolled);
        ...
    }
    public void fling(int velocityX, int velocityY) {
         setScrollState(SCROLL_STATE_SETTLING);
         mLastFlingX = mLastFlingY = 0;
         mScroller.fling(0, 0, velocityX, velocityY,
                   Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
         postOnAnimation();
    }                                   
    

    在ViewFling 的 run 方法中,我们看到,如果没有结束,会继续滑动下去

    ...
     if (scroller.isFinished() || !fullyConsumedAny) {
         setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
         if (ALLOW_THREAD_GAP_WORK) {
               mPrefetchRegistry.clearPrefetchPositions();
          }
    } else {
         postOnAnimation();
         if (mGapWorker != null) {
               mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
          }
    }
    ...
    
    1. 缓存
      根据列表位置获取ItemView,先后从scrapped、cached、exCached、recycled集合中查找相应的ItemView,如果没有找到,就创建(Adapter.createViewHolder()),最后与数据集绑定。其中scrapped、cached和exCached集合定义在RecyclerView.Recycler中,分别表示将要在RecyclerView中删除的ItemView、一级缓存ItemView和二级缓存ItemView,cached集合的大小默认为2,exCached是需要我们通过RecyclerView.ViewCacheExtension自己实现的,默认没有;recycled集合其实是一个Map,定义在RecyclerView.RecycledViewPool中,将ItemView以ItemType分类保存了下来,通过RecyclerView.RecycledViewPool可以实现在不同的RecyclerView之间共享ItemView,只要为这些不同RecyclerView设置同一个RecyclerView.RecycledViewPool就可以了。
    2. 回收
      View的回收并不像View的创建那么复杂,这里只涉及了两层缓存mCachedViews与mRecyclerPool,mCachedViews相当于一个先进先出的数据结构,每当有新的View需要缓存时都会将新的View存入mCachedViews,而mCachedViews则会移除头元素,并将头元素放入mRecyclerPool,所以mCachedViews相当于一级缓存,mRecyclerPool则相当于二级缓存,并且mRecyclerPool是可以多个RecyclerView共享的

    最后与 AdapterView 比较

    区别 AdapterView RecyclerView
    点击事件 提供click、LongClick事件 使用 onTouchListener(复杂)
    分割线 divider ItemDecoration(复杂,定制型强)
    布局 listView(流式布局)和GridView(网格时) LayoutManager(简单)
    缓存 RecyclerBin Recycler
    局部刷新 复杂 notifyItemChanged(position,payload)
    动画 复杂 简单
    嵌套布局 支持头部和尾部 通过 Type实现
    多选 自身优点 复杂

    参考:
    https://juejin.im/entry/586a12c5128fe10057037fba

    相关文章

      网友评论

          本文标题:RecyclerView源码解析

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