RecyclerView源碼剖析: 基本顯示

RecyclerView處發佈以來,就受到開發者的青睞,它良好的功能解耦,使咱們在定製它的功能方面變得遊刃有餘。自從在項目中使用這個控件以來,我對它是不勝歡喜,以致於我想用一系列的文章來剖析它。本文就從最基本的顯示入手來分析,爲後面的分析打下堅實的基礎。java

基本使用

本文先分析RecyclerView從建立到顯示的過程,這將爲後面的系列文章打下堅實的基礎,咱們先看下它的基本使用函數

mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(new RvAdapter());
複製代碼

LayoutManagerAdapterRecyclerView必不可少的部分,本文就來分析這段代碼。源碼分析

爲了方便,在後面的分析中,我將使用RV表示RecyclerView,用LM表示LayoutManager,用LLM表示LinearLayoutManager佈局

構造函數

View的構造函數一般就是用來解析屬性和初始化變量,RV的構造函數也不例外,而與本文相關代碼以下this

public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        // ...

        mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
            // ...
        });
        
        mChildHelper = new ChildHelper(new ChildHelper.Callback() {
            // ...
        });

        // ...
    }
複製代碼

從全名就大體能夠猜想出這兩個類的做用。AdapterHelperAdapter的輔助類,用來處理Adapter的更新操做。ChildHelper是RV的輔助類,用來管理它的子View。spa

設置LayoutManager

public void setLayoutManager(@Nullable LayoutManager layout) {
        // ...
        
        // 保存LM
        mLayout = layout;
        if (layout != null) {
            // LM保存RV引用
            mLayout.setRecyclerView(this);
            // 若是RV添加到Window中,那麼就通知LM
            if (mIsAttached) {
                mLayout.dispatchAttachedToWindow(this);
            }
        }
        
        // ...
        
        // 請求從新佈局
        requestLayout();
    }
複製代碼

setLayoutManager()方法最主要的操做就是RV和LM互相保存引用,因爲RV的LM改變了,所以須要從新請求佈局。code

設置Adapter

setAdapter()方法是由setAdapterInternal()實現server

private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews) {
        // ...
        
        // RV保存Adapter引用
        mAdapter = adapter;
        if (adapter != null) {
            // 給新Adapter註冊監聽者
            adapter.registerAdapterDataObserver(mObserver);
            // 通知新Adapter已經添加到RV中
            adapter.onAttachedToRecyclerView(this);
        }
        // 若是LM存在,就通知LM,Adapter改變了
        if (mLayout != null) {
            mLayout.onAdapterChanged(oldAdapter, mAdapter);
        }
        // 通知RV,Adapter改變了
        mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
        // 表示Adapter改變了
        mState.mStructureChanged = true;
    }
複製代碼

RV保存Adapter引用並給新Adapter註冊監聽者,而後通知每個關心Adapter的監聽者,例如RV, LM。繼承

測量

當一切準備就緒後,如今來分析測量的部分開發

protected void onMeasure(int widthSpec, int heightSpec) {
        // ...
        // 若是LM使用自動測量機制
        if (mLayout.isAutoMeasureEnabled()) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);

            // 爲了兼容處理,實際調用了RV的defaultOnMeasure()方法測量
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

            final boolean measureSpecModeIsExactly =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            // 若是寬和高的測量模式都是EXACTLY,那麼就使用默認測量值,並直接返回
            if (measureSpecModeIsExactly || mAdapter == null) {
                return;
            }

            // ... 省略剩餘的測量代碼
        } else {
            // ...
        }
    }
複製代碼

首先根據LM是否支持RV的自動測量機制來決定測量邏輯,LLM是支持自動測量機制的,所以只分析這種狀況的測量。

究竟什麼是自動測量機制,你們能夠仔細研讀源碼的註釋以及測量邏輯,我這裏只作簡單分析。

使用自動測量機制,首先會調用LM的onMeasure()進行測量。這裏你可能會有疑問,既然叫作自動測量機制,爲什麼還會用LM來測量呢。其實這是爲了兼容處理,它實際是調用了RV的defaultOnMeasure()方法

void defaultOnMeasure(int widthSpec, int heightSpec) {
        final int width = LayoutManager.chooseSize(widthSpec,
                getPaddingLeft() + getPaddingRight(),
                ViewCompat.getMinimumWidth(this));
        final int height = LayoutManager.chooseSize(heightSpec,
                getPaddingTop() + getPaddingBottom(),
                ViewCompat.getMinimumHeight(this));

        setMeasuredDimension(width, height);
    }
複製代碼

咱們能夠發現,RV做爲一個ViewGroup,這裏竟然沒有考慮子View就保存了測量的結果。很顯然,這是一個粗糙的測量。

可是這個粗糙的測量實際上是爲了知足一種特殊狀況,那就是父View給出的寬高限制模式都是MeasureSpec.EXACTLY。從代碼中能夠發現,在經歷這一步粗糙測量後,就處理了這種特殊狀況。

爲了簡化分析,目前只考慮這種特殊狀況。 被省略的代碼其實就是考慮子View測量的代碼,而這段代碼在onLayout()中也有,由於放到後面講解。

佈局

onLayout是由dispatchLayout()實現的

void dispatchLayout() {
        // ...
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            // RV已經測量完畢,所以LM保存RV的測量結果
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // ...
        } else {
            // ...
        }
        dispatchLayoutStep3();
    }
複製代碼

佈局的過程,不管如何,都是通過dispatchLayoutStep1()dispatchLayoutStep2()dispatchLayoutStep3()完成。而與本文相關的只有dispatchLayoutStep2(),它是完成子View的實際佈局操做,它是由LM的onLayoutChildren()實現。

LM實現子View的佈局

從ww前面的分析可知,RV對子View的佈局是交給LM來處理的。例子中使用的是LLM,所以這裏分析它的onLayoutChildren()方法。因爲這個方法代碼量比較大,所以將分步解析。

初始化信息

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 確保mLayoutState被建立
        ensureLayoutState();
        mLayoutState.mRecycle = false;
        // 解析是否使用反向佈局
        if (mOrientation == VERTICAL || !isLayoutRTL()) {
            mShouldReverseLayout = mReverseLayout;
        } else {
            mShouldReverseLayout = !mReverseLayout;
        }
    }
複製代碼

首先確保了LayoutState mLayoutState的建立,LayoutState用來保存佈局的狀態。

而後解析是否使用反向佈局,例子中的LLM使用的是垂直佈局,而且佈局使用默認的不支持RTL,所以mShouldReverseLayout值爲false,表示不是反向佈局。

你們須要知道LLM的返回佈局的狀況。

更新錨點信息

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 1. 初始化信息
        // 2. 更新錨點信息
        if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
                || mPendingSavedState != null) {
            mAnchorInfo.reset();
            // 錨點信息保存是不是反向佈局
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // 計算錨點的位置和座標
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            // 表示錨點信息有效
            mAnchorInfo.mValid = true;
        }   
        
        // ...
        
        final int firstLayoutDirection;
        if (mAnchorInfo.mLayoutFromEnd) {
            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                    : LayoutState.ITEM_DIRECTION_HEAD;
        } else {
            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                    : LayoutState.ITEM_DIRECTION_TAIL;
        }
        // 通知錨點信息準備就緒
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    }
複製代碼

AnchorInfo mAnchorInfo是用來保存錨點信息,錨點位置和座標來表示佈局從哪裏開始,這個將會在後面看到。

錨點信息保存了是否反向佈局的信息,這裏又冒出來一個mStackFromEnd,這個是爲了兼容支持AbsListView#setStackFromBottom(boolean)特性,說白了就是爲了提供給開發者一致的操做方法。我的以爲這真是一個垃圾操做。

以後用updateAnchorInfoForLayout()方法計算出了錨點的位置和座標

private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
        // ...
        // 根據padding值決定錨點座標
        anchorInfo.assignCoordinateFromPadding();
        // 若是不是反向佈局,錨點位置爲0
        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    }
    
    // AnchorInfo#assignCoordinateFromPadding()
    void assignCoordinateFromPadding() {
        // 若是不是反向佈局,座標就是RV的paddingTop值
        mCoordinate = mLayoutFromEnd
                ? mOrientationHelper.getEndAfterPadding()
                : mOrientationHelper.getStartAfterPadding();
    }    
複製代碼

根據例子中的狀況,錨點座標是RV的paddingTop,位置是0。

計算佈局的額外空間

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 1. 初始化信息
        // 2. 更新錨點信息
        // 3. 計算佈局的額外空間
        // 保存佈局方向,無滾動的狀況下,值爲LayoutState.LAYOUT_END
        mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
                ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        // 計算佈局須要的額外空間,結果保存到mReusableIntPair
        calculateExtraLayoutSpace(state, mReusableIntPair);
        // 額外究竟還要考慮padding
        int extraForStart = Math.max(0, mReusableIntPair[0])
                + mOrientationHelper.getStartAfterPadding();
        int extraForEnd = Math.max(0, mReusableIntPair[1])
                + mOrientationHelper.getEndPadding();
        if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
                && mPendingScrollPositionOffset != INVALID_OFFSET) {
            // ...
        }
    }
複製代碼

在RV滑動的時候,calculateExtraLayoutSpace()會分配一個頁面的額外空間,其它的狀況下是不會分配額外空間的。

對於例子中的狀況,calculateExtraLayoutSpace()分配的額外空間就是0。可是對於佈局,額外空間還須要考慮RV的padding值。

若是自定義一個繼承自LLM的LM,能夠複寫calculateExtraLayoutSpace()定義額外空間的分配策略。

爲子View佈局

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 1. 初始化信息
        // 2. 更新錨點信息
        // 3. 計算佈局的額外空間
        // 4. 爲子View佈局
        // 首先分離而且回收子View
        detachAndScrapAttachedViews(recycler);
        // RV的高度爲0,而且模式爲UNSPECIFIED
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mIsPreLayout = state.isPreLayout();
        mLayoutState.mNoRecycleSpace = 0;
        if (mAnchorInfo.mLayoutFromEnd) {
            // ...
        } else {
            // 從錨點位置向後填充
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForEnd;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
            final int lastElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            // 從錨點位置向前填充
            // ...

            // 若是還有額外空間,就向後填充更多的子View
            if (mLayoutState.mAvailable > 0) {
                // ...
            }
        }        
複製代碼

爲子View佈局以前,首先從RV分離子View,並回收。而後,經過fill()分別從錨點位置,向後以及向前填充子View,最後若是還有剩餘空間,就嘗試嘗試繼續向後填充子View(若是還有子View的話)。

根據例子計算出來的錨點位置是0,座標是paddongTop,所以這裏只分析從錨點位置向後填充的過程。

首先調用updateLayoutStateToFillEnd()方法,根據錨點信息來更新mLayoutState

private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
        // 參數傳入的是錨點的位置和座標
        updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
    }

    private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
        // 可用空間就是去掉padding後的可用大小
        mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
        // 表示Adapter的數據遍歷的方向
        mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
                LayoutState.ITEM_DIRECTION_TAIL;
        // 保存錨點當前位置
        mLayoutState.mCurrentPosition = itemPosition;
        // 保存佈局的方向
        mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
        // 保存錨點座標,也就是佈局的偏移量
        mLayoutState.mOffset = offset;
        // 滾動偏移量
        mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
    }
複製代碼

更新完mLayoutState信息後,就調用fill()填充子View

int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
        final int start = layoutState.mAvailable;
        // ...
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        // 有能夠空間,而且還有子View沒有填充
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            // 這裏表明全部子View已經layout完畢
            if (layoutChunkResult.mFinished) {
                break;
            }
            // 更新佈局偏移量
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
    
            // 從新計算可用空間
            if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            // ...
        }
        // 返回這次佈局使用了多少空間
        return start - layoutState.mAvailable;
    }
複製代碼

根據例子來分析,只有還存在可用空間,而且還有子View沒有填充,那麼就會一直調用layoutChunk()方法進行填充子View,直到可用空間消耗完,或者沒有了子View。

LLM#layoutChunk()分析

獲取子View

layoutChunk()是LLM爲子View佈局的核心方法,咱們須要重點關注這個方法的實現。因爲這個方法也比較長,所以我也打算分段講解

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
        // 1. 獲取一個子View,並更新mLayoutState.mCurrentPosition
        View view = layoutState.next(recycler);    
    }
複製代碼

根據例子的狀況,這裏是從RecyclerView.Recycler中建立一個子View,下面的代碼爲建立新View的代碼

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
            
            // ...
            
            if (holder == null) {
                if (holder == null) {
                    // 1. 回調Adapter.onCreateViewHoler()建立ViewHolder,並設置ViewHolder類型
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    // ...
                }
            }

            // ...
            
            boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // ...
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                // 2. 回調Adapter.bindViewHolder()綁定ViewHolder,並更新ViewHolder的一些信息
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
            
            // 3. 確保建立View的佈局參數的正確性並更新信息
            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            final LayoutParams rvLayoutParams;
            if (lp == null) {
                rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else if (!checkLayoutParams(lp)) {
                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else {
                rvLayoutParams = (LayoutParams) lp;
            }
            // 佈局參數保存ViewHolder
            rvLayoutParams.mViewHolder = holder;
            // 若是View不是新建立,而且已經綁定,那麼mPendingInvalidate爲true,表示須要刷新
            rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
            return holder;
        }
複製代碼

Adapter中建立ViewHolder,就是利用Adapter的幾個回調,而後更新ViewHolder的相關信息,最後更新建立View的佈局參數。

你們須要知道基本的Adapter是如何寫。

把子View添加到RV中

獲取子View後,就須要把這個子View添加到RV中

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
        // 1. 獲取子View 
        // 2. 把子View添加到RV中
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                // 把獲取的子View添加到RV末尾
                addView(view);
            } else {
                
            }
        }         
    }
複製代碼

addView()方法是利用基類LM的addViewInt()實現,而最終是經過mChildHelper.addView()實現。

// ChildHelper#addView()
    void addView(View child, int index, boolean hidden) {
        final int offset;
        if (index < 0) {
            // index爲-1,就獲取RV已經有了多少個子View
            offset = mCallback.getChildCount();
        } else {
            offset = getOffset(index);
        }
        
        // ...
        
        // 根據offset,把子View添加到RV中
        mCallback.addView(child, offset);
    }        
複製代碼

因爲index值爲-1,所以是把這個View添加到RV的子View末尾。

測量子View

把子View添加到RV後,就須要測量這個子View

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
    // 1. 獲取子View 
    // 2. 把子View添加到RV中
    // 3. 測量子View
    measureChildWithMargins(view, 0, 0);
    // 保存LLM相應方向上消耗的大小
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
}    
    
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    // 獲取ItemDecoration的Rect
    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    // 已經使用的寬和高要把ItemDecoration的Rect計算在內
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;
    // 這是一個考慮了padding, margin, ItemDecoration Rect的測量
    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
            getPaddingLeft() + getPaddingRight()
                    + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
            canScrollHorizontally());
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
            getPaddingTop() + getPaddingBottom()
                    + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
            canScrollVertically());
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}    
複製代碼

對子View的測量,考慮了padding, margin, 以及ItemDecorationRect。 具體如何測量就不在本文的分析範圍內。

給子View進行佈局

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
        // 1. 獲取子View 
        // 2. 把子View添加到RV中
        // 3. 測量子View
        // 4. 子View佈局
        // 計算佈局須要的座標值
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
               
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            // ...
        }
        // 進行佈局
        layoutDecoratedWithMargins(view, left, top, right, bottom);        
    }
複製代碼

這個佈局過程很簡單,這裏是一筆代過了。

你們必定要了解自定義View如何測量和佈局,這是你分析本文的基礎。

繪製

測量和佈局過程都已經分析完畢,剩下的就是繪製過程

public void draw(Canvas c) {
        // 調用onDraw()方法
        super.draw(c);
        // 用ItemDecoration.onDrawOver()進行繪製
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
        // 繪製邊界效果
        // ...
    }
    
    public void onDraw(Canvas c) {
        super.onDraw(c);
        
        // 用ItemDecoration.onDraw()進行繪製
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }    
複製代碼

對於RV來講,繪製過程主要就是繪製ItemDecoration,首先是用ItemDecoration.onDraw()方法進行繪製,而後再用ItemDecoration.onDrawOver()進行繪製。

本系列的文章中,我會用單獨一篇文章講解ItemDecoration的原理,以及如何使用。

感想

RV的源碼分析真不能一蹴而就,須要極大的耐心和毅力。本文以極簡的方式(文章仍然很長)分析了RV從建立到顯示界面的過程,初始窺探了RV的原理,可是這還遠遠不能知足個人好奇心,我將以這篇文章爲基石繼續前行。

相關文章
相關標籤/搜索