ItemDecoration深刻解析與實戰(一)——源碼分析

一 概述

ItemDecorationRecyclerView 中的一個抽象靜態內部類。java

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.canvas

這是官網對 ItemDecoration 的描述,簡單來講就是能夠爲 RecyclerView的每個 ItemView 進行一些特殊的繪製或者特殊的佈局。從而咱們能夠爲 RecyclerView 添加一些實用好玩的效果,好比分割線,邊框,飾品,粘性頭部等。bash

此文會分析ItemDecoration 的使用及原理,而後進行一些Demo的實現,包括分割線,網格佈局的邊框,以及粘性頭部。app

二 方法

1. 方法概述

ItemDecoration中的實際方法只有6個,其中有3個是重載方法,都被標註爲 @deprecated,即棄用了,這些方法以下ide

修飾符 返回值類型 方法名 標註
void public onDraw(Canvas c, RecyclerView parent, State state)
void public onDraw(Canvas c, RecyclerView parent) @deprecated
void pulbic onDrawOver(Canvas c, RecyclerView parent, State state)
void public onDrawOver(Canvas c, RecyclerView parent) @deprecated
void public getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
void public getItemOffsets(Rect outRect, View view, RecyclerView parent) @deprecated

2. getItemOffsets

除了 getItemOffsets 方法,其餘方法的默認實現都爲空,而 getItemOffsets 的默認實現方法也很簡單:佈局

@Deprecated
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }
複製代碼

兩個getItemOffsets方法最終都是調用了上面實現,就一行代碼,若是咱們自定義過 ItemDecoration 的話,就會知道,咱們能夠爲 outRect 設置四邊的大小來爲 itemView 設置一個偏移量. 這個偏移量有點相似於 View 的margin,看下面的圖1:ui

RecyclerView&Child.png

圖片很清晰的表示了 ItemView 的結構(該圖不是特別精確,後面會說到),這是隻有一個 Child 的狀況,咱們從外往裏看:this

  1. 最外的邊界即 RecyclerView 的邊界
  2. 紅色部分是 RecyclerView 的 Padding,這個咱們應該能理解
  3. 橙色部分是咱們爲 ItemView 設置的 Margin,這個相信寫過佈局都能理解
  4. 藍色部分就是咱們在 getItemOffsets方法中給 outRect對象設置的值
  5. 最後的的黃色部分就是咱們的 ItemView 了

整體就是說,getItemOffsets中設置的值就至關於 margin 的一個存在。"圖說無憑",接下來就結合源碼講解一下這個圖的"依據"。首先看一下 getItemOffsets在哪裏被調用了:spa

Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        ...
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);  //被調用
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
 }
複製代碼

RecyclerView源碼中,這是 getItemOffsets惟一被調用的地方,代碼也很簡單,就是將 RecyclerView中全部的(即經過addDecoration()方法添加的) ItemDecoration 遍歷一遍,而後將咱們設在 getItemOffsets 中設置的四個方向的值分別累加並存儲在insets這個Rect當中。那麼這個 insets又在哪裏被調用了呢,順着方法繼續跟蹤下去:rest

public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;

    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);
    }
}
複製代碼

咱們看到,在 measureChildWithMargins方法中,將剛剛獲得的 insets 的值與 Recyclerview 的 Padding 以及當前 ItemView 的 Margin 相加,而後做爲 getChildMeasureSpec的第三個參數傳進去:

public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
    int childDimension, boolean canScroll) {
    int size = Math.max(0, parentSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    //...省略部分代碼
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼

getChildMeasureSpec方法的第三個參數標註爲 padding ,在方法體這個 padding 的做用就是計算出 size 這個值,這個 size是就是後面測量中 Child(ItemView) 能達到的最大值。

也就是說咱們設置的 ItemView 的 Margin 以及ItemDecoration.getItemOffsets中設置的值到頭來也是跟 Parent 的 Padding 一塊兒來計算 ItemView 的可用空間,也就印證了上面的圖片,在上面說了該圖不精確就是由於

  • parent-padding
  • layout_margin
  • insets(all outRect)

他們是一體的,並無劃分紅一段一段這樣,圖中的outRect也應該改成insets,可是圖中的形式能夠更方便咱們理解。

3. onDraw

public void onDraw(Canvas c, RecyclerView parent, State state) {
        onDraw(c, parent);
    }

    /**
     * @deprecated Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
     */
    @Deprecated
    public void onDraw(Canvas c, RecyclerView parent) {
    }
複製代碼

onDraw方法有兩個重載,一個被標註爲 @deprecated,即棄用了,咱們知道,若是重寫了 onDraw,就能夠在咱們上面的 getItemOffsets中設置的範圍內繪製,知其然還要知其因此然,咱們看下源碼裏面是怎樣實現的 #RecyclerView.java

@Override
    public void onDraw(Canvas c) {
        super.onDraw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }

複製代碼

ReyclerViewonDraw方法中,將會把全部 DecorationonDraw方法調用一遍,並且會把Recyclerview#onDraw(Canvas)方法中的Canvas傳遞給Decoration#onDraw,也就是說咱們在Decoration中拿到了整個 RecyclerView 的 Canvas,那麼咱們基本就能夠隨意繪製了,可是咱們使用中會發現,咱們繪製的區域若是在 ItemView 的範圍內就會被蓋住,這是爲何呢?

因爲View的繪製是先執行 draw(Canvas)再到onDraw(Canvas)的,咱們複習一波自定義View的知識,看下View的繪製流程: #View.java

public void draw(Canvas canvas) {
      
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)

        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);   //註釋1

            // Step 4, draw the children
            dispatchDraw(canvas);        //註釋2
            ...
            // we're done... return; } } 複製代碼

咱們直接看註釋1與註釋2那段,能夠看到,View的繪製是先繪製自身(onDraw調用),而後再繪製child,因此咱們在 Decoration#onDraw中繪製的界面會被 ItemView 遮擋也是理所固然了。

因此咱們在繪製中就要計算好繪製的範圍,使繪製範圍在上面彩圖中藍色區域內,即getItemOffsets設置的範圍內,避免沒有顯示或者過度繪製的狀況。

4.onDrawOver

public void onDrawOver(Canvas c, RecyclerView parent, State state) {
        onDrawOver(c, parent);
    }

    /**
     * @deprecated
     * Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
     */
    @Deprecated
    public void onDrawOver(Canvas c, RecyclerView parent) {
    }
複製代碼

onDrawOveronDraw很是相似,也是兩個重載,一個被棄用了,看名稱咱們就基本能知道這個方法的用途,它是用於補充 onDraw 的一個方法,因爲onDraw會被 ItemView 覆蓋,因此咱們想要繪製一些漂浮在RecyclerView頂層的裝飾就沒法實現,因此就有了這個方法,他是在 ItemView 繪製完畢後纔會被調用的,看下源碼的實現: #RecyclerView.java

@Override
    public void draw(Canvas c) {
        super.draw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
        ...
    }
複製代碼

super.draw(c) 就是咱們在上面分析的View#draw(Canvas)方法,會調用一系列的繪製流程,包括onDraw(ItemDecoration的onDraw)以及dispatchDraw(ItemView的繪製),走完這些流程後纔會調用Decoration#onDrawOver方法.

到此,咱們就能夠得出 onDraw>dispatchDraw(ItemView的繪製)>onDrawOver的執行流程。

5. 總結

  1. getItemOffsets用於提供一些空間(相似Margin)給 onDraw繪製
  2. onDraw方法繪製的內容若是在 ItemView 的區域則可能被覆蓋(沒效果)
  3. onDraw>dispatchDraw(ItemView的繪製)>onDrawOver從左到右執行

三 實戰

實戰將會從易到難進行幾個小的Demo練習。 因爲這篇文章內容已經比較充實了,就把實戰部分放到下篇講解。

感謝你的閱讀,因爲水平有限,若有錯誤懇請提醒。

相關文章
相關標籤/搜索