RecyclerView(3)-LayoutMagager源碼解析,LinearLayoutManager

上一節(jié)RecyclerView(2)- 自定義Decoration打造時(shí)光軸效果也已經(jīng)寫(xiě)完了,希望有看到我文章的同學(xué)能有一些收獲。layoutManager可以說(shuō)是一個(gè)重中之重,代碼量非常多,且涉及到復(fù)用機(jī)制的調(diào)用等等。等源碼分析過(guò)后,同學(xué)們應(yīng)該可以通過(guò)自定義LayoutManager打造奇形怪狀的奇葩的UI需求了。

· RecyclerView(1)- Decoration源碼解析
· RecyclerView(2)- 自定義Decoration打造時(shí)光軸效果
· RecyclerView(3)- LayoutMagager源碼解析,LinearLayoutManager
· RecyclerView(4)- 核心、Recycler復(fù)用機(jī)制_1
· RecyclerView(4)- 核心、Recycler復(fù)用機(jī)制_2
· RecyclerView(5)- 自定義LayoutManager(布局、復(fù)用)
· RecyclerView(6)- 自定義ItemAnimator
· RecyclerView(7)- ItemTouchHelper
· RecyclerView(8)- MultiTypeAdapter文章MultiTypeAdapter Github地址
文章視頻地址:鏈接:http://pan.baidu.com/s/1hssvXC4 密碼:18v1

分析LinearLayoutManager,先羅列幾個(gè)想弄明白的問(wèn)題

· 1、如何擺位置;
· 2、按需加載布局,位置擺放的規(guī)則
· 3、滑動(dòng)時(shí)itemview的位置改變遵循的規(guī)則

LayoutManager如何擺位置;

先從 測(cè)量 onMeasure開(kāi)始

RecyclerView.class
    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout.mAutoMeasure) { // 自動(dòng)測(cè)量
             mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);// 調(diào)用一次recyclerview的測(cè)量
             //....
            dispatchLayoutStep2();
        }
        else{//自定義測(cè)量規(guī)則
        
        }
    }

    private void dispatchLayoutStep2() {
        //....
         mState.mItemCount = mAdapter.getItemCount();
        //.... 自定義擺放位置
        mLayout.onLayoutChildren(mRecycler, mState);
        //....
    }

可以看到在recyclerview的 onMeasure 調(diào)用了 mLayout的onLayoutChildren方法 并將Recycler與 包含了 適配器一些信息的包裝成一個(gè) State參數(shù) 傳入mLayout的onLayoutChildren

接下來(lái)看一下LinearLayoutManager

LinearLayoutManager.class
    //初始化
    public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        setOrientation(orientation);
        setReverseLayout(reverseLayout);
        setAutoMeasureEnabled(true);
    }
    public void setAutoMeasureEnabled(boolean enabled) {
        mAutoMeasure = enabled;
    }
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:
        // 1) by checking children and other variables, find an anchor coordinate and an anchor
        //  item position.
        // 2) fill towards start, stacking from bottom
        // 3) fill towards end, stacking from top
        // 4) scroll to fulfill requirements like stack from bottom.
        // create layout state
        if (DEBUG) {
            Log.d(TAG, "is pre layout:" + state.isPreLayout());
        }
        if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
            if (state.getItemCount() == 0) {
            // 如果沒(méi)有項(xiàng)目 移除布局
                removeAndRecycleAllViews(recycler);
                return;
            }
        }
        
        // ...
        this.updateLayoutStateToFillStart(this.mAnchorInfo);
        this.mLayoutState.mExtra = extraForStart;
        fill(recycler, mLayoutState, state, false);
        //...
        this.updateLayoutStateToFillEnd(this.mAnchorInfo);
        this.mLayoutState.mExtra = extraForEnd;
        this.mLayoutState.mCurrentPosition += this.mLayoutState.mItemDirection;
        this.fill(recycler, this.mLayoutState, state, false);
    }
    // 填充view
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
            //.. 擺位置
             layoutChunk(recycler, state, layoutState, layoutChunkResult);
            //...  布局回收
              this.recycleByLayoutState(recycler, layoutState);
    }
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        //...
        addView(view);  // -> addView(View child, int index) ->  addViewInt(View child, int index, boolean disappearing)
        //計(jì)算位置
         this.measureChildWithMargins(view, 0, 0);
            result.mConsumed = this.mOrientationHelper.getDecoratedMeasurement(view);
            int left;
            int top;
            int right;
            int bottom;
            if(this.mOrientation == 1) {
                if(this.isLayoutRTL()) {
                    right = this.getWidth() - this.getPaddingRight();
                    left = right - this.mOrientationHelper.getDecoratedMeasurementInOther(view);
                } else {
                    left = this.getPaddingLeft();
                    right = left + this.mOrientationHelper.getDecoratedMeasurementInOther(view);
                }

                if(layoutState.mLayoutDirection == -1) {
                    bottom = layoutState.mOffset;
                    top = layoutState.mOffset - result.mConsumed;
                } else {
                    top = layoutState.mOffset;
                    bottom = layoutState.mOffset + result.mConsumed;
                }
            } else {
                top = this.getPaddingTop();
                bottom = top + this.mOrientationHelper.getDecoratedMeasurementInOther(view);
                if(layoutState.mLayoutDirection == -1) {
                    right = layoutState.mOffset;
                    left = layoutState.mOffset - result.mConsumed;
                } else {
                    left = layoutState.mOffset;
                    right = layoutState.mOffset + result.mConsumed;
                }
            }
        
        //布局
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        //...
    }
    private void addViewInt(View child, int index, boolean disappearing) {
          final ViewHolder holder = getChildViewHolderInt(child);
          mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);//依附在parent  這邊是 parent 是 recyclerView
          
          // 對(duì)holder 做一些變量改變
    }

RecyclerView.class   
    static class LayoutState {
        View next(RecyclerView.Recycler recycler) {
            //..
            //從 recyler 中取出view
            final View view = recycler.getViewForPosition(mCurrentPosition);
            //...
            return view;
        }
    }

一開(kāi)始的注釋說(shuō)了整個(gè)大概的流程:
1、先檢查children和其它變量,找到一個(gè)錨點(diǎn)坐標(biāo)與錨點(diǎn)(如果實(shí)在線性布局中,相當(dāng)于找到當(dāng)前界面內(nèi)第一個(gè)VIew,與第一個(gè)view的坐標(biāo)點(diǎn))
2、填充從底部開(kāi)始堆疊
3、從頂部填充到端部
4、計(jì)算是否還有滾動(dòng),添加各種變量,創(chuàng)建布局。

其實(shí)原理還是挺簡(jiǎn)單的,循環(huán)判斷是否超出邊界,測(cè)量view,添加view 布局view,判定值改變 在跳到上面循環(huán)。好多人看不懂是因?yàn)間oogle工程師將這些判斷數(shù)據(jù)抽取封裝了起來(lái),而其中字段非常多,讓人眼花繚亂。
一些總結(jié):
開(kāi)始調(diào)用 onLayoutChildren() 調(diào)用fill() 擺放位置
fill():判斷相應(yīng)規(guī)則 回收一部分view, 調(diào)用layoutChunk() 獲取 view、填充、擺放view;
layoutChunk(): 獲取view,計(jì)算 view的位置
layoutDecoratedWithMargins():拿到裝飾器設(shè)置的偏移量,擺放view;

2、按需加載布局,位置擺放的規(guī)則

上面也可以知道 在fill()方法內(nèi)我們先去判斷位置擺放的規(guī)則 在決定是否加載下一個(gè)/上一個(gè)View(也就是調(diào)用 layoutChunk),那么我們就來(lái)看一下判斷規(guī)則是怎么樣的吧

   while((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
           int start = layoutState.mAvailable;
        if(layoutState.mScrollingOffset != -2147483648) {
            if(layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }

            this.recycleByLayoutState(recycler, layoutState);
        }

        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LinearLayoutManager.LayoutChunkResult layoutChunkResult = this.mLayoutChunkResult;
            layoutChunkResult.resetInternal();
            this.layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if(layoutChunkResult.mFinished) {
                break;
            }

            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            if(!layoutChunkResult.mIgnoreConsumed || this.mLayoutState.mScrapList != null || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if(layoutState.mScrollingOffset != -2147483648) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if(layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }

                this.recycleByLayoutState(recycler, layoutState);
            }

            if(stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }

可以看到while關(guān)鍵字內(nèi),有l(wèi)auoutState.mInfinite remainingspace layoutState.haseMore(state)變量,就是這三個(gè)變量控制著我們的擺放view的數(shù)量。
字面意思可以猜出來(lái):無(wú)窮、可用空間>0; 是否有更多

那我們來(lái)一個(gè)一個(gè)看一下,這幾個(gè)變量都經(jīng)歷了什么。

2.1、layoutState.mInfinite

字面意思 mInfinite是無(wú)窮的意思...

    this.mLayoutState.mInfinite = this.resolveIsInfinite();
    boolean resolveIsInfinite() {
    return this.mOrientationHelper.getMode() == 0 && this.mOrientationHelper.getEnd() == 0;
}
new OrientationHelper(layoutManager) {
         public int getEnd() {
                return this.mLayoutManager.getHeight();
            }
         public int getMode() {
                return this.mLayoutManager.getHeightMode();
          }    
        }

可以看到 mInfinite 的值與 recyclerview的高度 與規(guī)格有關(guān),判斷==0 ? 這是什么鬼! 看起來(lái)一點(diǎn)卵用都沒(méi)有 ?_?(是在下輸了)

2.1、remainingspace

  int remainingSpace = layoutState.mAvailable + layoutState.mExtra;

第一次 layoutState.mAvailable的取值

    private void updateAnchorInfoForLayout(Recycler recycler, State state, LinearLayoutManager.AnchorInfo anchorInfo) {
        if(!this.updateAnchorFromPendingData(state, anchorInfo)) {
        }
        
    private boolean updateAnchorFromPendingData(State state, LinearLayoutManager.AnchorInfo anchorInfo) {
    //.....
               if(anchorInfo.mLayoutFromEnd) {
            anchorInfo.mCoordinate = this.mOrientationHelper.getEndAfterPadding() - this.mPendingSavedState.mAnchorOffset;
        } else {
            anchorInfo.mCoordinate = this.mOrientationHelper.getStartAfterPadding() + this.mPendingSavedState.mAnchorOffset;
        }
        //.........
    }
    
    private void updateLayoutStateToFillStart(LinearLayoutManager.AnchorInfo anchorInfo) {
        this.updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate);
    }

    private void updateLayoutStateToFillStart(int itemPosition, int offset) {
        this.mLayoutState.mAvailable = offset - this.mOrientationHelper.getStartAfterPadding();
    }
    

可以看到 第一次 layoutState.mAvailable的值是通過(guò) 錨點(diǎn) AnchorInfo來(lái)計(jì)算的。
其中還判斷了mLayoutFromEnd ...

看到這里我是暈的,變量太多了。。。

第一次 layoutState.mExtra的值
extra意思是額外...

2.3、layoutstate.hasMore(state)

        boolean hasMore(State state) {
            return this.mCurrentPosition >= 0 && this.mCurrentPosition < state.getItemCount();
        }

就是判斷 position;

2.4、擺完一個(gè)布局以后,干了些什么

布局是一個(gè)一個(gè)添加的,那么在加載完一個(gè)children view后在加載了什么呢?

    int fill(Recycler recycler, LinearLayoutManager.LayoutState layoutState, State state, boolean stopOnFocusable) {
    
        while((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            this.layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if(layoutChunkResult.mFinished) {
                break;
            }

    // 加上偏移量
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            if(!layoutChunkResult.mIgnoreConsumed || this.mLayoutState.mScrapList != null || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                //  減去剩余空間
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if(layoutState.mScrollingOffset != -2147483648) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if(layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }

                this.recycleByLayoutState(recycler, layoutState);
            }

            if(stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
            //....
        }
    }
void layoutChunk(Recycler recycler, State state, LinearLayoutManager.LayoutState layoutState, LinearLayoutManager.LayoutChunkResult result){
  //......
    //得到 消耗的高度/寬度
    result.mConsumed = this.mOrientationHelper.getDecoratedMeasurement(view);
    // 布局
    addView(view); 
    layout
    // 判斷是否被隱藏忽略
     if(params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
     }
     // 焦點(diǎn)
     result.mFocusable = view.isFocusable();
}

mOrientationHelper.calss
        public int getDecoratedMeasurement(View view) {
            LayoutParams params = (LayoutParams)view.getLayoutParams();
            return this.mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin;
        }

可以看到 layoutChunk()中賦值layoutChunkResult,在之后偏移量添加layoutState.mOffset+=...,剩余空間減去了view消耗的數(shù)量 remainingSpace-= layoutChunkResult.mConsumed。

還是回到了剛開(kāi)始的注釋?zhuān)?先找到錨點(diǎn),計(jì)算第一個(gè)坐標(biāo), 在循環(huán)添加view、計(jì)算偏移量確定擺放位置。

2.5 布局的一些總結(jié)

來(lái)一點(diǎn)總結(jié)吧:

涉及到的主要類(lèi)
1、 LinearLayoutManager.LayoutState:布局狀態(tài)類(lèi)(有布局方向(向上向下)、結(jié)束布局、偏移量...)
2、Recycler 布局支持類(lèi)、可回收view、創(chuàng)建view...
3、AnchorInfo 錨點(diǎn)信息類(lèi)

布局流程:
1、以開(kāi)始方向先更新錨點(diǎn)信息
2、在通過(guò)錨點(diǎn)信息 賦值layoutState 得到可用空間,偏移量等信息, while(剩余空間) 擺放childrenView 改變偏移量;
3、以底部更新錨點(diǎn)信息
4、重復(fù)第2步
5、結(jié)束

原理其實(shí)是很簡(jiǎn)單的,只不過(guò)其中變量太多了,代碼也是很多所以看得云里霧里的,有時(shí)候還會(huì)跑偏找不到南北。
畫(huà)一個(gè)流程圖吧。

onChildren填充機(jī)制

其中填充規(guī)則 fill()方法流程如下

fill填充規(guī)則

3、 滑動(dòng)時(shí)干了哪些事情

第一次布局看起來(lái)比較簡(jiǎn)單,我們就可以去猜測(cè) 滑動(dòng)時(shí)應(yīng)該加載布局也是一樣的。 去看看源碼吧。

    public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
        return this.mOrientation == 1?0:this.scrollBy(dx, recycler, state);
    }

    public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
        return this.mOrientation == 0?0:this.scrollBy(dy, recycler, state);
    }

有兩個(gè), 分析scrollVerticallyBy

    int scrollBy(int dy, Recycler recycler, State state) {
        if(this.getChildCount() != 0 && dy != 0) {
            this.mLayoutState.mRecycle = true;
            this.ensureLayoutState();
            int layoutDirection = dy > 0?1:-1;
            int absDy = Math.abs(dy);
            this.updateLayoutState(layoutDirection, absDy, true, state);
            // 滑動(dòng)且 返回消耗的距離
            int consumed = this.mLayoutState.mScrollingOffset + this.fill(recycler, this.mLayoutState, state, false);
            if(consumed < 0) {
                return 0;
            } else {
            // 計(jì)算滑動(dòng)的量 
                int scrolled = absDy > consumed?layoutDirection * consumed:dy;
                this.mOrientationHelper.offsetChildren(-scrolled);
                this.mLayoutState.mLastScrollDelta = scrolled;
                return scrolled;
            }
        } else {
            return 0;
        }
    }
    
      private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, State state) {
        this.mLayoutState.mInfinite = this.resolveIsInfinite();
        this.mLayoutState.mExtra = this.getExtraLayoutSpace(state);
        this.mLayoutState.mLayoutDirection = layoutDirection;
        int scrollingOffset;
        View child;
        if(layoutDirection == 1) {
            this.mLayoutState.mExtra += this.mOrientationHelper.getEndPadding();
            child = this.getChildClosestToEnd();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?-1:1;
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedEnd(child);
            scrollingOffset = this.mOrientationHelper.getDecoratedEnd(child) - this.mOrientationHelper.getEndAfterPadding();
        } else {
            child = this.getChildClosestToStart();
            this.mLayoutState.mExtra += this.mOrientationHelper.getStartAfterPadding();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?1:-1;
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedStart(child);
            scrollingOffset = -this.mOrientationHelper.getDecoratedStart(child) + this.mOrientationHelper.getStartAfterPadding();
        }
        // 可用距離  偏移距離 
        this.mLayoutState.mAvailable = requiredSpace;
        if(canUseExistingSpace) {
            this.mLayoutState.mAvailable -= scrollingOffset;
        }

        this.mLayoutState.mScrollingOffset = scrollingOffset;
    }

scrollVerticallyBy 的作用是 ,改變layoutState,調(diào)用fill,布局view,返回實(shí)際消耗的距離。

fill的流程我們?cè)谏厦嬉惨呀?jīng)做了說(shuō)明了。
補(bǔ)充一點(diǎn)的是,當(dāng)我們滑動(dòng)的距離在一定范圍內(nèi)(沒(méi)有超出頁(yè)面上 第一個(gè)或最后一個(gè)child在屏幕上的范圍)是不會(huì)重新

4、 錨點(diǎn)的位置是如何確定的

從以上流程中我們了解到了layoutManager的一些布局的規(guī)則,我們的布局都是通過(guò)一個(gè)基準(zhǔn)點(diǎn)來(lái)進(jìn)行上下布局,那么最重要的肯定就是這個(gè)基準(zhǔn)點(diǎn)即錨點(diǎn)的位置。
我們知道錨點(diǎn)的信息 保存在 LinearLayoutManager.AnchorInfo中,進(jìn)而layouState控制布局

1、第一次錨點(diǎn)得位置

void onLayoutChildren(Recycler recycler, State state) {
}
    private void updateAnchorInfoForLayout(Recycler recycler, State state, LinearLayoutManager.AnchorInfo anchorInfo) {
        if(!this.updateAnchorFromPendingData(state, anchorInfo)) {
            if(!this.updateAnchorFromChildren(recycler, state, anchorInfo)) {
            //分配坐標(biāo)   
                anchorInfo.assignCoordinateFromPadding();
            //得到數(shù)據(jù)集位置
                anchorInfo.mPosition = this.mStackFromEnd?state.getItemCount() - 1:0;
            }
        }
    }
        class AnchorInfo {
            void assignCoordinateFromPadding() {
            this.mCoordinate = this.mLayoutFromEnd?LinearLayoutManager.this.mOrientationHelper.getEndAfterPadding():LinearLayoutManager.this.mOrientationHelper.getStartAfterPadding();
            }
        }

第一次特別簡(jiǎn)單,就判斷布局方向獲取mCoordinate 開(kāi)始或結(jié)束的padding與itemView的數(shù)據(jù)集位置position。

2、 滑動(dòng)位置的確定

    int scrollBy(int dy, Recycler recycler, State state) {
            this.updateLayoutState(layoutDirection, absDy, true, state);
    }
    private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, State state) {
        this.mLayoutState.mInfinite = this.resolveIsInfinite();
        this.mLayoutState.mExtra = this.getExtraLayoutSpace(state);
        this.mLayoutState.mLayoutDirection = layoutDirection;
        int scrollingOffset;
        View child;
        if(layoutDirection == 1) {
            this.mLayoutState.mExtra += this.mOrientationHelper.getEndPadding();
            child = this.getChildClosestToEnd();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?-1:1;
            
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection; 
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedEnd(child);
            scrollingOffset = this.mOrientationHelper.getDecoratedEnd(child) - this.mOrientationHelper.getEndAfterPadding();
        } else {
            child = this.getChildClosestToStart();
            this.mLayoutState.mExtra += this.mOrientationHelper.getStartAfterPadding();
            this.mLayoutState.mItemDirection = this.mShouldReverseLayout?1:-1;
            this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
            this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedStart(child);
            scrollingOffset = -this.mOrientationHelper.getDecoratedStart(child) + this.mOrientationHelper.getStartAfterPadding();
        }

        this.mLayoutState.mAvailable = requiredSpace;
        if(canUseExistingSpace) {
            this.mLayoutState.mAvailable -= scrollingOffset;
        }

        this.mLayoutState.mScrollingOffset = scrollingOffset;
    }

給張圖吧

文章視頻地址:鏈接:http://pan.baidu.com/s/1o7Ai48E 密碼:98pc

recyclerView滾動(dòng)時(shí)布局流程

· RecyclerView(1)- Decoration源碼解析
· RecyclerView(2)- 自定義Decoration打造時(shí)光軸效果
· RecyclerView(3)- LayoutMagager源碼解析,LinearLayoutManager
· RecyclerView(4)- 核心、Recycler復(fù)用機(jī)制_1
· RecyclerView(4)- 核心、Recycler復(fù)用機(jī)制_2
· RecyclerView(5)- 自定義LayoutManager(布局、復(fù)用)
· RecyclerView(6)- 自定義ItemAnimator
· RecyclerView(7)- ItemTouchHelper
· RecyclerView(8)- MultiTypeAdapter文章MultiTypeAdapter Github地址
文章視頻地址:鏈接:http://pan.baidu.com/s/1hssvXC4 密碼:18v1


希望我的文章不會(huì)誤導(dǎo)在觀看的你,如果有異議的地方歡迎討論和指正。
如果能給觀看的你帶來(lái)收獲,那就是最好不過(guò)了。

人生得意須盡歡, 桃花塢里桃花庵
點(diǎn)個(gè)關(guān)注唄,對(duì),不信你點(diǎn)試試?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • RecyclerView 源碼分析 本文原創(chuàng),轉(zhuǎn)載請(qǐng)注明出處。歡迎關(guān)注我的 簡(jiǎn)書(shū) ,關(guān)注我的專(zhuān)題 Android ...
    MeloDev閱讀 10,146評(píng)論 6 49
  • RecyclerView 概要 RecyclerView是Android 5.0開(kāi)始提供一個(gè)可回收容器,它比 Li...
    rexyren閱讀 5,651評(píng)論 10 27
  • RecyclerView包含以下幾個(gè)重要的組件:1.LayoutManager: 測(cè)量和布局子View2.Recy...
    烏龜愛(ài)吃肉閱讀 3,564評(píng)論 4 7
  • 假如說(shuō)“我愛(ài)你” 我害怕 天際會(huì)劃落星辰 黑夜吞噬白晝 假如說(shuō)“我愛(ài)你” 我害怕 河流會(huì)失去海洋 洪水浸沒(méi)家園...
    花香蝶往閱讀 271評(píng)論 0 0
  • 唧唧蟲(chóng)聲響,汪汪狗吠長(zhǎng)。 江南種紅豆,相思在北方。
    何典閱讀 287評(píng)論 2 2