阅读 112

值得深入学习的控件-RecyclerView(源码解析篇)

作者:錦小白 www.jianshu.com/p/10298503c…

为什么要写这篇源码解析呢?

我一直在说RecyclerView是一个值得深入学习,甚至可以说是一门具有艺术性的控件。那到底哪里值得我们花时间去深入学习呢。没错了,就是源码的设计。但是看源码其实是一件不简单的事情,就拿RecyclerView的源码来说,打开源码一看,往下拉啊拉啊,我擦,怎么还没到头,汗....居然有12k+行。看到这里恐怕会吓一跳,就这么一个看似简单的控件就这么多行源码,这让我从何看起,一股畏惧感油然而生。

其实不需要害怕,我们不需要一开始就想完全弄懂它每一步怎么实现的,这样反而会造成只见森林不见树木的感觉。我们就把源码就当成一片森林来说吧。首先我们只需要先抓住一条路径去看,也就是带着一个问题去看,这样就能够把这条路径上的树都看明白了。就不会有只见森林不见树,一脸茫然了。当然我们大多数情况肯定是不满足于此一条路径,想完全看明白它是怎么实现的,那就继续另开路径(再带着另外一个问题),继续看这条路上的树。当你把每条路都走差不多了,再回头来看,就会发现你既见到了森林又见到了一颗颗清晰树木,犹如醍醐灌顶、豁然开朗。

说着很简单,但是不得不说看源码的过程还是有点小痛苦的。不过,不用慌,看完之后你所获得那种充实感和满足感会远远大于过程中的痛苦感。毕竟这是一个充满艺术感的控件嘛,值得我们去欣赏和学习。

那么开始放正片了......

一、开辟一条路径

从使用RecyclerView的时候,它的一个功能就让我感觉很这个控件不简单,不知道你和我想的是不是一样。那是什么功能呢?我们只需改变一行代码就可以直接设置它的ItemView为水平布局、垂直布局、表格布局以及瀑布流布局。这是ListView所不能做到的。用起来简单,其背后肯定有故事啊。那我们就以这条路为核心来看这片森林了。

二、开始寻路

从哪里开始看呢? 1.我们先从setAdapter()看起,这个方法我们比较熟悉,在Activity中这是我们直接接触的方法。


/**
*Replaces the current adapter with the new one and triggers listeners.
*/
 public void setAdapter(Adapter adapter){

        .....

        //用一个新的设配器和触发器来替代目前正在使的
        setAdapterInternal(adapter,false,true);
        //请求布局,直接调用View类的请求布局方法
        requestLayout();
    }

复制代码

setAdapter里面主要做了两件事:

首先调用setAdapterInternal方法,目的是用一个新的设配器和触发器来替代目前正在使用的。 我们深入进去看看它做了什么?

对于熟悉了观察者设计模式的,可以从下面的代码看出来,其实里面有个操作是:

注销观察者(之前的设配器)和注册观察者(新的设配器)操作。简单的理解一下就是设配器观察者会监测一些对象的状态,当这些对象状态改变,它可以通过这种设计模式低耦合的做出相应的改变。最后调用markKnownViewsInvalid方法刷新一下视图。

如果你想深入了解观察者设计模式的可以看一下这篇文章

传送门:观察者设计模式

{
 Adapter mAdapter;
......

 private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
                                    boolean removeAndRecycleViews) {
        if (mAdapter != null) {
            mAdapter.unregisterAdapterDataObserver(mObserver);  //注销观察者
            mAdapter.onDetachedFromRecyclerView(this);          //Called by RecyclerView when it stops observing this Adapter.
        }
        ......
        mAdapterHelper.reset();
        final Adapter oldAdapter = mAdapter;
        mAdapter = adapter;
        if (adapter != null) {
            adapter.registerAdapterDataObserver(mObserver);  //注册观察者
            adapter.onAttachedToRecyclerView(this);
        }
      ......
        //刷新视图
        markKnownViewsInvalid();
    }

复制代码

之后调用了 requestLayout方法请求重新布局。这个方法很关键,和我们的这次选的路是相通的。

 @Override
    public void requestLayout() {
        if (mEatRequestLayout == 0 && !mLayoutFrozen) {
            super.requestLayout();
        } else {
            mLayoutRequestEaten = true;
        }
    }

复制代码

这么关键的方法代码却这么少?而且好像只做了一个操作?没错,表面上只调用了父类View的requestLayout方法。其实通过父类的这个方法之后会调用它的onLayout方法,这个名字熟悉自定义View的童鞋都知道了。但我们看父类View的onLayout方法其实是个空方法。也就是说最终需要由它的子类来重写,也即RecyclerVie调用自身的onLayout方法。

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

复制代码

onLayout又调用了dispatchLayout方法,来分发layout

void dispatchLayout() {
       ......
        if (mState.mLayoutStep == State.STEP_START) {
          //分发第一步
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
              //分发第二步
            dispatchLayoutStep2();
        } 
        ......
         //分发第三步
        dispatchLayoutStep3();
        ......
    }

复制代码

它把这个分发的过程分为了三步走 step1:做一下准备工作:决定哪一个动画被执行,保存一些目前view的相关信息

 private void dispatchLayoutStep1() {
       ......
        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));
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPreLayoutInformation(mState, holder,
                 ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                holder.getUnmodifiedPayloads());
                mViewInfoStore.addToPreLayout(holder, animationInfo);

            }
            ......
        }

复制代码

step2:找到实际的view和最终的状态后运行layout。

    private void dispatchLayoutStep2() {
        eatRequestLayout();
        onEnterLayoutOrScroll();
       ......

        mState.mInPreLayout = false;
         // Step 2: 运行layout
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        ....
        resumeRequestLayout(false);
    }

复制代码

这里面有个方法很关键了,就是下面这个onLayoutChildren,这个为什么关键呢,先提一下这个,待会要详细说的。

mLayout.onLayoutChildren(mRecycler, mState);

复制代码

step3:做一些分发的收尾工作了,保存动画和一些其他的信息。和我们不同路,就不看它了。

看了这么多先喝一杯92年的肥宅快乐水压压惊吧~~,顺便看张图小结一下上面的过程

三、寻得果树

之前说过RecyclerView和ListView最大的不同就是在它们的布局实现上。在ListView中布局是通过自身的layoutChildren方法实现的,但对于RecyclerView来说就不是了,那是谁来实现了呢?

这就要从刚才结束的onLayoutChildren方法说起了,它不是RecyclerView的类直接方法,它是RecyclerView的内部类LayoutManager的方法,顾名思义,就是布局管理者了。我们的RecyclerView布局就通过这个布局管理者来做了,把这样一个很重要的职责就交给它了。从而实现某种程度上的低耦合。

那我们继续走,它是怎么执行这一职责的。 但是点进去看onLayoutChildren方法,发现只有一行代码,而且还是打印的日志:必须重写这个方法。

 public void onLayoutChildren(Recycler recycler, State state) {
            Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
        }

复制代码

那么既然要重写必须要寻找一个子类,所以这里我就找了一个子类LinearLayoutManager类,也是我们最常用的一种线性布局来看。

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

       ......
        int startOffset;
        int endOffset;
        final int firstLayoutDirection;
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
        ......
        if (mAnchorInfo.mLayoutFromEnd) {
            // 底部向顶部的填充
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;

            //填充
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // 顶部向底部的填充
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
              //填充
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;

            ......
            }
        } else {
           ......
        }

        ......
    }

复制代码

这个方法主要就是通过一个布局算法,实现itemView从顶部到底部或者底部到顶部的填充,并创建一个布局的状态。接下来看一下fill方法是怎么进行填充的。

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        ......
        //1.计算RecyclerView可用的布局宽或高
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        //2.迭代布局item View
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            //3.布局item view
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            //4.计算布局偏移量
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;

            if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                //5.计算剩余的可用空间
                remainingSpace -= layoutChunkResult.mConsumed;
            }
            ......
        }

        return start - layoutState.mAvailable;
    }

复制代码

fill方法总的来说用了5步实现了itemVIew的填充:

(1)计算RecyclerView可用的布局宽或高

(2)迭代布局item View

(3)布局itemview

(4)计算布局偏移量

(5)计算剩余的可用空间

fill方法又会循环的调用layoutChunk来进行itemView的布局,下面先看看layoutChunk的实现

 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        //1.获取itemview
        View view = layoutState.next(recycler);
        ......
        //2.获取itemview的布局参数
        LayoutParams params = (LayoutParams) view.getLayoutParams();

        //3.测量Item View
        measureChildWithMargins(view, 0, 0);
        //4.计算该itemview消耗的宽和高
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);

        int left, top, right, bottom;
        //5.按照水平或竖直方向布局来计算itemview的上下左右坐标
        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;
            }
        } else {
            ......
        }
        6.计算itemview的边界比如下划线和margin,从而确定itemview准确的位实现最终的布局
        layoutDecoratedWithMargins(view, left, top, right, bottom);

        }
        result.mFocusable = view.hasFocusable();
    }

复制代码

在layoutChunk中首先从layoutState获取此时的itemview,然后根据获得的这个itemview获取它的布局参数和尺寸信息,并且判断布局方式(横向或者纵向),以此计算出itemview的上下左右坐标。最后调用layoutDecoratedWithMargins方法完成布局。

这样一看就对整个过程有了个清晰的认识了吧,有没有感觉设计的很优雅。

四、贯穿布局的一条线

到这里已经算走完我们之前准备走的一条路了。但从开始到这里始终忽略了一个东西没有说,那就在布局过程的大多方法中的参数都有一个Recycler对象。这个Recycler是什么呢?

在使用RecyclerView的过程中,我们都知道Adapter被缓存的单位不再是普通的itemview了,而是一个ViewHolder。这是和listview的一个很大的不同。

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

     public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }

        View getViewForPosition(int position, boolean dryRun) {
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
        }

         ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {

                ...
                }  

        ......

复制代码

在Recycler类的开始就看到mAttachedScrap、mChangedScrap、mCachedViews、 mUnmodifiableAttachedScrap这几个ViewHolder的列表对象,它们就是用来缓存ViewHolder的。

具体是怎么实现的这里就不做详细的解释了。因为这里一说又会牵涉到其他的点,子子孙孙无穷尽也,毕竟这是一个有艺术感的控件,不能指望一篇文章把它说透哈。

到这里我们就结束了我们对RecyclerView的的源码分析了。相信你看完会有所收获。

阅读更多

有关Android插件化思考

Java并发面试,幸亏有点道行,不然又被忽悠了

Android酷炫实用的开源框架(UI框架)

(Android)面试题级答案(精选版)

相信自己,没有做不到的,只有想不到的

关注下面的标签,发现更多相似文章
评论