理解RecyclerView(五)—RecyclerView的繪製流程

前言:做人如果沒有夢想,那和鹹魚有什麼區別。        ——《少林足球》

一、概述

  上一篇文章對RecyclerView中實現瞭如何高度自定義點擊事件、萬能ViewHolder、萬能適配器的封裝和使用。最開始就提到,RecyclerView支持各種各樣的佈局效果,其核心關鍵在於RecyclerView.LayoutManager中,使用時我們是需要setLayoutManager()設置佈局管理器的。RecyclerView已經將一部分功能抽離出來,在佈局管理器中另外處理,也方便開發者自行拓展。LayoutManager就是負責RecyclerView的測量和佈局以及itemView的回收和複用。今天這裏主要結合LinearLayoutManager來分析RecyclerView的繪製流程。

RecyclerView提供了三中佈局管理器:

  • LinearLayoutManager      以列表的方式展示item,有水平方向RecyclerView.HORIZONTAL和垂直方向RecyclerView.VERTICAL;
  • GridLayoutManager       以網格的方式展示item,有水平方向和垂直方向;
  • StaggeredGridLayoutManager   以瀑布流的方式展示item,有水平方向和垂直方向。

這裏就不一一分析了,前面的文章已經做了詳細的介紹,不瞭解的同學可以回頭看一下。這裏以LinearLayoutManager爲例來分析RecyclerView的繪製流程。

二、RecyclerView的繪製三個步驟

  RecyclerView設置佈局管理器,這一步是必要的,用什麼樣的LayoutManager來繪製RecyclerView,不然RecyclerView也不知道怎麼繪製。

 recyclerView.setLayoutManager(manager);

從設置佈局管理器方法入手,setLayoutManager()設置佈局管理器給RecyclerView使用:

   public void setLayoutManager(@Nullable LayoutManager layout) {
        if (layout == mLayout) {//和之前的管理器一樣則直接return
            return;
        }
        stopScroll();//停止滾動
        if (mLayout != null) {//每次設置layoutManager都重新設置recyclerView的初始參數,動畫回收view等
        	if (mItemAnimator != null) {
                mItemAnimator.endAnimations();//結束動畫
            }
            mLayout.removeAndRecycleAllViews(mRecycler);//移除回收所有itemView
            mLayout.removeAndRecycleScrapInt(mRecycler);//移除回收所有已經廢棄的itemView
            mRecycler.clear();//清除所有緩存
            
            mLayout.setRecyclerView(null);//重置RecyclerView
            mLayout = null;
        } else {
            mRecycler.clear();
        }
     	·······
     	mLayout.setRecyclerView(this);//LayoutManager與RecyclerView關聯
        mRecycler.updateViewCacheSize();//更新緩存大小
        requestLayout();//請求重繪
    }

這裏首先做了重置回收工作,然後LayoutManager與RecyclerView關聯起來,最後請求重繪。這裏調用了請求重繪requestLayout()方法,那麼說明每次設置layoutManager都會執行View樹的繪製,那麼就會重走RecyclerView的onMeasure()onLayout()onDraw()繪製三部曲。

    public void requestLayout() {
        if (mRecyclerView != null) {
            mRecyclerView.requestLayout();//請求重繪
        }
    }

2.1 onMeasure()

我們來看看RecyclerView的onMeasure()方法:

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {//如果mLayout爲空則採用默認測量,然後結束
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.mAutoMeasure) {//如果爲自動測量,默認爲true
        	final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
        	mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);//測量RecyclerView的寬高
        	 //當前RecyclerView的寬高是否爲精確值
        	final boolean measureSpecModeIsExactly =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            if (measureSpecModeIsExactly || mAdapter == null) {//如果RecyclerView的寬高爲精確值或者mAdapter爲空,則結束
                return;
            }
            //RecyclerView的寬高爲wrap_content時,即measureSpecModeIsExactly = false則進行測量
            //因爲RecyclerView的寬高爲wrap_content時,需要先測量itemView的寬高才能知道RecyclerView的寬高
            if (mState.mLayoutStep == State.STEP_START) {//還沒測量過
                dispatchLayoutStep1();//1.適配器更新、動畫運行、保存當前視圖的信息、運行預測佈局
            }
            dispatchLayoutStep2();//2.最終實際的佈局視圖,如果有必要會多次運行
            //根據itemView得到RecyclerView的寬高
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        }
    }

onMeasure()主要是RecyclerView寬高測量工作,主要有兩種情況:

  • (1)當RecyclerView的寬高爲match_parent或者精確值時,即measureSpecModeIsExactly = true,此時只需要測量自身的寬高就知道RecyclerView的寬高,測量方法結束;
  • (2)當RecyclerView的寬高爲wrap_content時,即measureSpecModeIsExactly = false,會往下執行dispatchLayoutStep1()dispatchLayoutStep2(),就是遍歷測量ItemView的大小從而確定RecyclerView的寬高,這種情況真正的測量操作都是在dispatchLayoutStep2()中完成。

dispatchLayoutStep1()dispatchLayoutStep2()下面會講解到。

2.2 onLayout()

onLayout()方法中, 直接調用dispatchLayout()方法佈局:

   @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
        dispatchLayout(); //直接調用dispatchLayout()方法佈局
        TraceCompat.endSection();
        mFirstLayoutComplete = true;
    }

dispatchLayout()layoutChildren()的包裝器,它處理由佈局引起的動態變化:

  void dispatchLayout() {
  		······
        mState.mIsMeasuring = false;//設置RecyclerView佈局完成狀態,前面已經設置預佈局完成了。
        if (mState.mLayoutStep == State.STEP_START) {//如果沒在OnMeasure階段提前測量子ItemView
            dispatchLayoutStep1();//佈局第一步:適配器更新、動畫運行、保存當前視圖的信息、運行預測佈局
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {//前兩步完成測量,但是因爲大小改變不得不再次運行下面的代碼
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();//佈局第二步:最終實際的佈局視圖,如果有必要會多次運行
        } else {
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();//佈局第三步:最後一步的佈局,保存視圖動畫、觸發動畫和不必要的清理。
    }

可以看到dispatchLayout()onMeasure()階段中一樣選擇性地進行測量佈局的三個步驟:

  • 1、如果沒在onMeasure階段提前測量子ItemView,即RecyclerView寬高爲match_parent或者精確值時,調用dispatchLayoutStep1()dispatchLayoutStep2()測量itemView寬高;
  • 2、如果在onMeasure階段提前測量子ItemView,但是子視圖發生了改變或者期望寬高和實際寬高不一致,則會調用dispatchLayoutStep2()重新測量;
  • 3、最後都會執行dispatchLayoutStep3()方法。

(1)我們來看看dispatchLayoutStep1、2、3分發佈局的三個步驟:dispatchLayoutStep1()主要是進行預佈局,適配器更新、動畫運行、保存當前視圖的信息等工作;

  private void dispatchLayoutStep1() {
        mState.assertLayoutStep(State.STEP_START);
        fillRemainingScrollValues(mState);
        mState.mIsMeasuring = false;
        startInterceptRequestLayout();//攔截佈局請求
        mViewInfoStore.clear();//itemView信息清除
        onEnterLayoutOrScroll();
        //測量和分派佈局時,更新適配器和計算那種類型要運行的動畫
        processAdapterUpdatesAndSetAnimationFlags();
        saveFocusInfo();//保存焦點信息
        mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
        mItemsAddedOrRemoved = mItemsChanged = false;
        mState.mInPreLayout = mState.mRunPredictiveAnimations;
        mState.mItemCount = mAdapter.getItemCount();
        findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);//找到可繪製itemView最小最大position

        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());
                //保存holder和動畫信息到預佈局中
                mViewInfoStore.addToPreLayout(holder, animationInfo);
            }
        }
        //運行與佈局,將會使用舊的item的position,佈局管理器佈局所有
        if (mState.mRunPredictiveAnimations) {
            //保存舊的管理器可以運行的邏輯
            saveOldPositions();
            final boolean didStructureChange = mState.mStructureChanged;
            mState.mStructureChanged = false;
            //佈局itemView
            mLayout.onLayoutChildren(mRecycler, mState);
            mState.mStructureChanged = didStructureChange;
        }
        stopInterceptRequestLayout(false);//回覆繪製鎖定
        mState.mLayoutStep = State.STEP_LAYOUT;
    }

(2)dispatchLayoutStep2()表示對最終狀態的視圖進行實際佈局:

  private void dispatchLayoutStep2() {
        startInterceptRequestLayout();//攔截請求佈局
        onEnterLayoutOrScroll();
        //設置佈局狀態和動畫狀態
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        //預佈局完成,開始佈局itemView
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);
		······
        stopInterceptRequestLayout(false);//停止攔截佈局請求
    }

(3)dispatchLayoutStep3()是佈局的最後一步,保存view的動畫信息,執行動畫,和一些必要的清理工作:

   private void dispatchLayoutStep3() {
        mState.assertLayoutStep(State.STEP_ANIMATIONS);
        startInterceptRequestLayout();//開始攔截佈局請求

        mState.mLayoutStep = State.STEP_START;//佈局開始狀態
        if (mState.mRunSimpleAnimations) {
            //步驟3:找出事情現在的位置,並處理更改動畫。
            //反向遍歷列表,因爲我們可能會在循環中調用animateChange,這可能會刪除目標視圖持有者。
            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
                ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
                //運行一個變更動畫。如果一個項目被更改,但是更新後的版本正在消失,則會產生衝突的情況。
                //由於標記爲正在消失的視圖可能會超出界限,所以我們運行一個change動畫。兩個視圖都將在動畫完成後自動清除。
                //另一方面,如果是相同的視圖持有者實例,我們將運行一個正在消失的動畫,因爲我們不會重新綁定更新的VH,除非它是由佈局管理器強制執行的。

                //運行消失動畫而不是改變
                mViewInfoStore.addToPostLayout(holder, animationInfo);
                final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(oldChangeViewHolder);
                //我們添加和刪除,這樣任何的佈置信息都是合併的
                mViewInfoStore.addToPostLayout(holder, animationInfo);

                ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
                mViewInfoStore.addToPostLayout(holder, animationInfo);
            }

            //處理視圖信息列表和觸發動畫
            mViewInfoStore.process(mViewInfoProcessCallback);
        }
        //回收廢棄的視圖
        mLayout.removeAndRecycleScrapInt(mRecycler);
        //重置狀態
        mState.mPreviousLayoutItemCount = mState.mItemCount;
        mDataSetHasChangedAfterLayout = false;

        //清除mChangedScrap中的數據
        mRecycler.mChangedScrap.clear();
        mRecycler.updateViewCacheSize();//更新緩存大小

        mLayout.onLayoutCompleted(mState);//佈局完成狀態
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);//停止攔截佈局請求
        mViewInfoStore.clear();//itemView信息清除

        recoverFocusFromState();//回覆焦點
        resetFocusInfo();//重置焦點信息
    }

總結一下這分發佈局的三個步驟:

  • dispatchLayoutStep1()  表示進行預佈局,適配器更新、動畫運行、保存當前視圖的信息等工作;
  • dispatchLayoutStep2()  表示對最終狀態的視圖進行實際佈局,有必要時會多次執行;
  • dispatchLayoutStep3()  表示佈局最後一步,保存和觸發有關動畫的信息,相關清理等工作。

2.3 onDraw()

來到最後一步的繪製onDraw()方法中,如果不需要一些特殊的效果,在TextView、ImageView控件中已經繪製完了。

    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);//所有itemView先繪製
		//分別繪製ItemDecoration
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }

2.4 RecyclerView的繪製三個步驟總結:

1、RecyclerView的itemView可能會被測量多次,如果RecyclerView的寬高是固定值或者match_parent,那麼在onMeasure()階段是不會提前測量ItemView佈局,如果RecyclerView的寬高是wrap_content,由於還沒有知道RecyclerView的實際寬高,那麼會提前在onMeasure()階段遍歷測量itemView佈局確定內容顯示區域的寬高值來確定RecyclerView的實際寬高;

2、dispatchLayoutStep1()dispatchLayoutStep2()dispatchLayoutStep3()這三個方法一定會執行,在RecyclerView的實際寬高不確定時,會提前多次執行dispatchLayoutStep1()dispatchLayoutStep2()方法,最後在onLayout()階段執行 dispatchLayoutStep3(),如果有itemView發生改變會再次執行dispatchLayoutStep2()

3、正在的測量和佈局itemView實際在dispatchLayoutStep2()方法中。

RecyclerView的繪製三個步驟流程圖:
在這裏插入圖片描述

三、LinearLayoutManager填充、測量、佈局過程

  RecyclerView的繪製經過measure、layout、draw三個步驟,但是itemView的真正佈局時委託給各個的LayoutManager中處理,上面LinearLayoutManager可以知道dispatchLayoutStep2()是實際佈局視圖步驟,通過LayoutManager調用onLayoutChildren()方法進行佈局itemView,它是繪製itemView的核心方法,表示從給定的適配器中列出所有相關的子視圖。

3.1 onLayoutChildren()佈局itemView

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 1) 檢查子類和其他變量找到描點座標和描點位置
        // 2) 從開始填補,從底部堆積
        // 3) 從底部填補,從頂部堆積
        // 4) 從底部堆積來滿足需求
        // 創建佈局狀態
        if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);//移除所有子View
                return;
            }
        }
        ensureLayoutState();
        mLayoutState.mRecycle = false;//禁止回收
        //顛倒繪製佈局
        resolveShouldLayoutReverse();

        final View focused = getFocusedChild();//獲取目前持有焦點的child
        if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
                || mPendingSavedState != null) {
            mAnchorInfo.reset();//重置錨點信息
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            //1. 計算更新描點位置和座標
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        }
		·······
        //計算第一佈局的方向
        int startOffset;
        int endOffset;
        final int firstLayoutDirection;

        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
        detachAndScrapAttachedViews(recycler);//暫時分離已經附加的view,即將所有child detach並通過Scrap回收
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mIsPreLayout = state.isPreLayout();
      
        mLayoutState.mNoRecycleSpace = 0;
        //2.開始填充,從底部開始堆疊;
        if (mAnchorInfo.mLayoutFromEnd) {
            //描點位置從start位置開始填充ItemView佈局
            updateLayoutStateToFillStart(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);//填充所有itemView
           
            //描點位置從end位置開始填充ItemView佈局
            updateLayoutStateToFillEnd(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);//填充所有itemView
            endOffset = mLayoutState.mOffset;
        }else { //3.向底填充,從上往下堆放;
            //描點位置從end位置開始填充ItemView佈局
            updateLayoutStateToFillEnd(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);
 
            //描點位置從start位置開始填充ItemView佈局
            updateLayoutStateToFillStart(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
        }
        //4.計算滾動偏移量,如果有必要會在調用fill方法去填充新的ItemView
         layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
    }

首先是狀態判斷和一些準備工作,對描點信息選擇和更新, detachAndScrapAttachedViews(recycler)暫時將已經附加的view分離,緩存Scrap中,下次重新填充時直接拿出來複用。然後計算是從哪個方向開始佈局。佈局算法如下:

  • 1.通過檢查子元素和其他變量,找到一個錨點座標和一個錨點項的位置;
  • 2.開始填充,從底部開始堆疊;
  • 3.向底填充,從上往下堆放;
  • 4.滾動以滿足要求,如堆棧從底部。

3.2 fill()開始填充itemView

填充佈局交給了fill()方法,表示填充由layoutState定義的給定佈局。爲什麼要fill兩次呢,我們來看看fill()方法:

  //填充方法,返回的是填充itemView的像素,方便後續滾動時使用
  int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
       recycleByLayoutState(recycler, layoutState);//回收滑出屏幕的view
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        //核心  == while()循環 ==
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {//一直循環,知道沒有數據
            layoutChunkResult.resetInternal();
            //填充itemView的核心方法
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ······
            if (layoutChunkResult.mFinished) {//佈局結束,退出循環
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;//根據添加的child高度偏移計算   
        }
     	······
        return start - layoutState.mAvailable;//返回這次填充的區域大小
    }

fill()核心就是一個while()循環,循環執行layoutChunk()填充一個itemView到屏幕,同時返回這次填充的區域大小。首先根據屏幕還有多少剩餘空間remainingSpace,根據這個數值減去子View所佔的空間大小,小於0時佈局子View結束,如果當前所有子View還沒有超過remainingSpace時,調用layoutChunk()安排View的位置。

3.3 layoutChunk()對itemView創建、填充、測量、佈局

layoutChunk()作爲最終填充佈局itemView的方法,對itemView創建、填充、測量、佈局,主要有以下幾個步驟:

  • 1.layoutState.next(recycler)從緩存中獲取itemView,如果沒有則創建itemView;
  • 2.根據實際情況來添加itemView到RecyclerView中,最終調用的還是ViewGroup的addView()方法;
  • 3.measureChildWithMargins()測量itemView大小包括父視圖的填充、項目裝飾和子視圖的邊距;
  • 4.根據計算好的left, top, right, bottom通過layoutDecoratedWithMargins()使用座標在RecyclerView中佈局給定的itemView。
   void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        //1.從緩存中獲取或者創建itemView
        View view = layoutState.next(recycler);//獲取當前postion需要展示的View
       	······
       	//2.根據實際情況來添加itemView到RecyclerView中,最終調用的還是ViewGroup的addView()方法
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } 
        
        //3.測量子View大小包括父視圖的填充、項目裝飾和子視圖的邊距
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        //計算一個ItemView的left, top, right, bottom座標值
        int left, top, right, bottom;
       	······
        //4.使用座標在RecyclerView中佈局給定的itemView
        //計算正確的佈局位置,減去margin,計算所有視圖的邊界框(包括margin和裝飾)
        layoutDecoratedWithMargins(view, left, top, right, bottom);//調用child.layout進行佈局
    }

通過layoutState.next()從緩存中獲取itemView如果沒有就創建一個新的itemView,然後addView()根據實際情況來添加itemView到RecyclerView中,最終調用的還是ViewGroupaddView()方法,接着通過 measureChildWithMargins()測量子View大小包括父視圖的填充、項目裝飾和子視圖的邊距;最後getDecoratedMeasuredWidth()通過計算好的left, top, right, bottom值在RecyclerView座標中佈局給定的itemView,注意這裏的寬度是item+decoration的總寬度。

   View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

獲取itemView,並且如果mScrapList中有緩存的View 則使用緩存的view,如果沒有mScrapList 就創建view,並添加到mScrapList 中。接下來getViewForPosition()方法主要是RecyclerView的緩存機制,後續的文章會講解到。

3.4 LinearLayoutManager填充、測量、佈局過程總結:

onLayoutChildren()表示從給定的適配器中列出所有相關的子視圖,填充佈局交給了fill()方法,填充由layoutState定義的給定佈局,while()循環執行layoutChunk()填充一個itemView到屏幕,作爲最終填充佈局itemView的方法,layoutState.next(recycler)從緩存中獲取或者創建itemView,通過addView()添加itemView到RecyclerView中,其實最終調用的還是ViewGroup的addView()方法,measureChildWithMargins()測量itemView大小包括父視圖的填充、項目裝飾和子視圖的邊距,最後layoutDecoratedWithMargins()根據計算好的left, top, right, bottom通過使用座標在RecyclerView中佈局給定的itemView。

流程圖如下:
在這裏插入圖片描述
至此!本文結束。


請尊重原創者版權,轉載請標明出處:https://blog.csdn.net/m0_37796683/article/details/104864318 謝謝!


相關文章:

理解RecyclerView(五)

 ● RecyclerView的繪製流程

理解RecyclerView(六)

 ● RecyclerView的滑動原理

理解RecyclerView(七)

 ● RecyclerView的嵌套滑動機制

理解RecyclerView(八)

 ● RecyclerView的回收複用緩存機制詳解

理解RecyclerView(九)

 ● RecyclerView的自定義LayoutManager

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章