值得深入學(xué)習(xí)的控件-RecyclerView(源碼解析篇)

為什么要寫這篇源碼解析呢系忙?

我一直在說RecyclerView是一個值得深入學(xué)習(xí),甚至可以說是一門具有藝術(shù)性的控件九昧。那到底哪里值得我們花時間去深入學(xué)習(xí)呢尉咕。沒錯了叠蝇,就是源碼的設(shè)計。但是看源碼其實是一件不簡單的事情龙考,就拿RecyclerView的源碼來說蟆肆,打開源碼一看,往下拉啊拉啊晦款,我擦炎功,怎么還沒到頭,汗....居然有12k+行缓溅∩咚穑看到這里恐怕會嚇一跳,就這么一個看似簡單的控件就這么多行源碼坛怪,這讓我從何看起淤齐,一股畏懼感油然而生。

其實不需要害怕袜匿,我們不需要一開始就想完全弄懂它每一步怎么實現(xiàn)的更啄,這樣反而會造成只見森林不見樹木的感覺。我們就把源碼就當(dāng)成一片森林來說吧居灯。首先我們只需要先抓住一條路徑去看祭务,也就是帶著一個問題去看,這樣就能夠把這條路徑上的樹都看明白了怪嫌。就不會有只見森林不見樹义锥,一臉茫然了。當(dāng)然我們大多數(shù)情況肯定是不滿足于此一條路徑岩灭,想完全看明白它是怎么實現(xiàn)的拌倍,那就繼續(xù)另開路徑(再帶著另外一個問題),繼續(xù)看這條路上的樹。當(dāng)你把每條路都走差不多了柱恤,再回頭來看数初,就會發(fā)現(xiàn)你既見到了森林又見到了一顆顆清晰樹木,猶如醍醐灌頂膨更、豁然開朗妙真。

說著很簡單,但是不得不說看源碼的過程還是有點小痛苦的荚守。不過,不用慌练般,看完之后你所獲得那種充實感和滿足感會遠遠大于過程中的痛苦感矗漾。畢竟這是一個充滿藝術(shù)感的控件嘛,值得我們?nèi)バ蕾p和學(xué)習(xí)薄料。

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

一敞贡、開辟一條路徑

從使用RecyclerView的時候,它的一個功能就讓我感覺很這個控件不簡單摄职,不知道你和我想的是不是一樣誊役。那是什么功能呢?我們只需改變一行代碼就可以直接設(shè)置它的ItemView為水平布局谷市、垂直布局蛔垢、表格布局以及瀑布流布局。這是ListView所不能做到的迫悠。用起來簡單鹏漆,其背后肯定有故事啊。那我們就以這條路為核心來看這片森林了创泄。

二艺玲、開始尋路

從哪里開始看呢?
1.我們先從setAdapter()看起鞠抑,這個方法我們比較熟悉,在Activity中這是我們直接接觸的方法饭聚。


/**
*Replaces the current adapter with the new one and triggers listeners.
*/
 public void setAdapter(Adapter adapter){
        
        .....
        
        //用一個新的設(shè)配器和觸發(fā)器來替代目前正在使的
        setAdapterInternal(adapter,false,true);
        //請求布局,直接調(diào)用View類的請求布局方法
        requestLayout();
    }

setAdapter里面主要做了兩件事:

首先調(diào)用setAdapterInternal方法搁拙,目的是用一個新的設(shè)配器和觸發(fā)器來替代目前正在使用的秒梳。
我們深入進去看看它做了什么?

對于熟悉了觀察者設(shè)計模式的感混,可以從下面的代碼看出來端幼,其實里面有個操作是:

注銷觀察者(之前的設(shè)配器)和注冊觀察者(新的設(shè)配器)操作。簡單的理解一下就是設(shè)配器觀察者會監(jiān)測一些對象的狀態(tài)弧满,當(dāng)這些對象狀態(tài)改變婆跑,它可以通過這種設(shè)計模式低耦合的做出相應(yīng)的改變。最后調(diào)用markKnownViewsInvalid方法刷新一下視圖庭呜。

如果你想深入了解觀察者設(shè)計模式的可以看一下這篇文章

傳送門:觀察者設(shè)計模式

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

之后調(diào)用了 requestLayout方法請求重新布局滑进。這個方法很關(guān)鍵犀忱,和我們的這次選的路是相通的。

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

這么關(guān)鍵的方法代碼卻這么少扶关?而且好像只做了一個操作阴汇?沒錯,表面上只調(diào)用了父類View的requestLayout方法节槐。其實通過父類的這個方法之后會調(diào)用它的onLayout方法搀庶,這個名字熟悉自定義View的童鞋都知道了。但我們看父類View的onLayout方法其實是個空方法铜异。也就是說最終需要由它的子類來重寫哥倔,也即RecyclerVie調(diào)用自身的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又調(diào)用了dispatchLayout方法,來分發(fā)layout

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

它把這個分發(fā)的過程分為了三步走
step1:做一下準(zhǔn)備工作:決定哪一個動畫被執(zhí)行揍庄,保存一些目前view的相關(guān)信息

 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和最終的狀態(tài)后運行l(wèi)ayout咆蒿。

    private void dispatchLayoutStep2() {
        eatRequestLayout();
        onEnterLayoutOrScroll();
       ......
       
        mState.mInPreLayout = false;
         // Step 2: 運行l(wèi)ayout
        mLayout.onLayoutChildren(mRecycler, mState);

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

這里面有個方法很關(guān)鍵了,就是下面這個onLayoutChildren蚂子,這個為什么關(guān)鍵呢沃测,先提一下這個,待會要詳細說的食茎。

mLayout.onLayoutChildren(mRecycler, mState);

step3:做一些分發(fā)的收尾工作了蒂破,保存動畫和一些其他的信息。和我們不同路董瞻,就不看它了寞蚌。

看了這么多先喝一杯92年的肥宅快樂水壓壓驚吧~~,順便看張圖小結(jié)一下上面的過程
圖片發(fā)自簡書App
first.jpg

三钠糊、尋得果樹

之前說過RecyclerView和ListView最大的不同就是在它們的布局實現(xiàn)上挟秤。在ListView中布局是通過自身的layoutChildren方法實現(xiàn)的,但對于RecyclerView來說就不是了抄伍,那是誰來實現(xiàn)了呢艘刚?

這就要從剛才結(jié)束的onLayoutChildren方法說起了,它不是RecyclerView的類直接方法截珍,它是RecyclerView的內(nèi)部類LayoutManager的方法攀甚,顧名思義,就是布局管理者了岗喉。我們的RecyclerView布局就通過這個布局管理者來做了秋度,把這樣一個很重要的職責(zé)就交給它了。從而實現(xiàn)某種程度上的低耦合钱床。

那我們繼續(xù)走荚斯,它是怎么執(zhí)行這一職責(zé)的。
但是點進去看onLayoutChildren方法,發(fā)現(xiàn)只有一行代碼事期,而且還是打印的日志:必須重寫這個方法滥壕。

 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 {
           ......
        }

        ......
    }

這個方法主要就是通過一個布局算法,實現(xiàn)itemView從頂部到底部或者底部到頂部的填充唠倦,并創(chuàng)建一個布局的狀態(tài)称鳞。接下來看一下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步實現(xiàn)了itemVIew的填充:

(1)計算RecyclerView可用的布局寬或高

(2)迭代布局item View

(3)布局itemview

(4)計算布局偏移量

(5)計算剩余的可用空間

fill方法又會循環(huán)的調(diào)用layoutChunk來進行itemView的布局稠鼻,下面先看看layoutChunk的實現(xiàn)

 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        //1.獲取itemview
        View view = layoutState.next(recycler);
        ......
        //2.獲取itemview的布局參數(shù)
        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的上下左右坐標(biāo)
        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準(zhǔn)確的位實現(xiàn)最終的布局
        layoutDecoratedWithMargins(view, left, top, right, bottom);
       
        }
        result.mFocusable = view.hasFocusable();
    }

在layoutChunk中首先從layoutState獲取此時的itemview,然后根據(jù)獲得的這個itemview獲取它的布局參數(shù)和尺寸信息枷餐,并且判斷布局方式(橫向或者縱向),以此計算出itemview的上下左右坐標(biāo)苫亦。最后調(diào)用layoutDecoratedWithMargins方法完成布局毛肋。

這樣一看就對整個過程有了個清晰的認識了吧,有沒有感覺設(shè)計的很優(yōu)雅屋剑。

四润匙、貫穿布局的一條線

到這里已經(jīng)算走完我們之前準(zhǔn)備走的一條路了。但從開始到這里始終忽略了一個東西沒有說唉匾,那就在布局過程的大多方法中的參數(shù)都有一個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的。

具體是怎么實現(xiàn)的這里就不做詳細的解釋了磷支。因為這里一說又會牽涉到其他的點谒撼,子子孫孫無窮盡也,畢竟這是一個有藝術(shù)感的控件雾狈,不能指望一篇文章把它說透哈廓潜。

到這里我們就結(jié)束了我們對RecyclerView的的源碼分析了。相信你看完會有所收獲。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末茉帅,一起剝皮案震驚了整個濱河市叨叙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌堪澎,老刑警劉巖擂错,帶你破解...
    沈念sama閱讀 222,946評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異樱蛤,居然都是意外死亡钮呀,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評論 3 399
  • 文/潘曉璐 我一進店門昨凡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爽醋,“玉大人,你說我怎么就攤上這事便脊÷焖模” “怎么了?”我有些...
    開封第一講書人閱讀 169,716評論 0 364
  • 文/不壞的土叔 我叫張陵哪痰,是天一觀的道長遂赠。 經(jīng)常有香客問我,道長晌杰,這世上最難降的妖魔是什么跷睦? 我笑而不...
    開封第一講書人閱讀 60,222評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮肋演,結(jié)果婚禮上抑诸,老公的妹妹穿的比我還像新娘。我一直安慰自己爹殊,他們只是感情好蜕乡,可當(dāng)我...
    茶點故事閱讀 69,223評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著边灭,像睡著了一般异希。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绒瘦,一...
    開封第一講書人閱讀 52,807評論 1 314
  • 那天称簿,我揣著相機與錄音,去河邊找鬼惰帽。 笑死憨降,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的该酗。 我是一名探鬼主播授药,決...
    沈念sama閱讀 41,235評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼士嚎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了悔叽?” 一聲冷哼從身側(cè)響起莱衩,我...
    開封第一講書人閱讀 40,189評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎娇澎,沒想到半個月后笨蚁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,712評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡趟庄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,775評論 3 343
  • 正文 我和宋清朗相戀三年括细,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戚啥。...
    茶點故事閱讀 40,926評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡奋单,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出猫十,到底是詐尸還是另有隱情览濒,我是刑警寧澤,帶...
    沈念sama閱讀 36,580評論 5 351
  • 正文 年R本政府宣布拖云,位于F島的核電站匾七,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏江兢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,259評論 3 336
  • 文/蒙蒙 一丁频、第九天 我趴在偏房一處隱蔽的房頂上張望杉允。 院中可真熱鬧,春花似錦席里、人聲如沸叔磷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽改基。三九已至,卻和暖如春咖为,著一層夾襖步出監(jiān)牢的瞬間秕狰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評論 1 274
  • 我被黑心中介騙來泰國打工躁染, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸣哀,地道東北人。 一個月前我還...
    沈念sama閱讀 49,368評論 3 379
  • 正文 我出身青樓吞彤,卻偏偏與公主長得像我衬,于是被迫代替她去往敵國和親叹放。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,930評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 【Android 控件 RecyclerView】 概述 RecyclerView是什么 從Android 5.0...
    Rtia閱讀 307,566評論 27 439
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,336評論 25 707
  • "心有多大舞臺就有多大"曾經(jīng)這是寫在書桌旁的座右銘挠羔,如今長大了井仰,心開闊了,想的也多了破加【愣瘢可是想太多,終歸處于一種疲勞拌喉。
    哈羅休庚閱讀 269評論 0 1
  • 對錢的看法決定了一個人的成長力如何尿背,男人對女人的評價標(biāo)準(zhǔn)是以其對金錢價值觀為基準(zhǔn)端仰,追求表面浮華的女人是一個不能‘脫...
    慶哥說閱讀 832評論 0 2
  • 關(guān)于成長荔烧,我們都會被物欲橫流的社會所左右過,我們從哭到笑汽久,從故鄉(xiāng)到異鄉(xiāng)鹤竭,從流浪到安寧…… 屋外,是偶爾的雨驚擾了春...
    孤獨小區(qū)閱讀 187評論 0 1