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

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

我一直在说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年的肥宅快乐水压压惊吧~~,顺便看张图小结一下上面的过程
图片发自简书App
first.jpg

三、寻得果树

之前说过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的的源码分析了。相信你看完会有所收获。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 【Android 控件 RecyclerView】 概述 RecyclerView是什么 从Android 5.0...
    Rtia阅读 307,365评论 27 439
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,457评论 25 707
  • "心有多大舞台就有多大"曾经这是写在书桌旁的座右铭,如今长大了,心开阔了,想的也多了。可是想太多,终归处于一种疲劳。
    哈罗休庚阅读 260评论 0 1
  • 对钱的看法决定了一个人的成长力如何,男人对女人的评价标准是以其对金钱价值观为基准,追求表面浮华的女人是一个不能‘脱...
    庆哥说阅读 797评论 0 2
  • 关于成长,我们都会被物欲横流的社会所左右过,我们从哭到笑,从故乡到异乡,从流浪到安宁…… 屋外,是偶尔的雨惊扰了春...
    孤独小区阅读 178评论 0 1