ItemDecoration深入解析與實戰(zhàn)(一)——源碼分析

一 概述

ItemDecorationRecyclerView 中的一個抽象靜態(tài)內(nèi)部類基括。

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.

這是官網(wǎng)對 ItemDecoration 的描述财岔,簡單來說就是可以為 RecyclerView的每一個 ItemView 進(jìn)行一些特殊的繪制或者特殊的布局风皿。從而我們可以為 RecyclerView 添加一些實用好玩的效果,比如分割線匠璧,邊框揪阶,飾品,粘性頭部等患朱。

此文會分析ItemDecoration 的使用及原理,然后進(jìn)行一些Demo的實現(xiàn)炊苫,包括分割線裁厅,網(wǎng)格布局的邊框侨艾,以及粘性頭部执虹。

二 方法

1. 方法概述

ItemDecoration中的實際方法只有6個,其中有3個是重載方法唠梨,都被標(biāo)注為 @deprecated袋励,即棄用了,這些方法如下

修飾符 返回值類型 方法名 標(biāo)注
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 方法,其他方法的默認(rèn)實現(xiàn)都為空,而 getItemOffsets 的默認(rèn)實現(xiàn)方法也很簡單:

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

兩個getItemOffsets方法最終都是調(diào)用了上面實現(xiàn),就一行代碼,如果我們自定義過 ItemDecoration 的話,就會知道,我們可以為 outRect 設(shè)置四邊的大小來為 itemView 設(shè)置一個偏移量.
這個偏移量有點類似于 View 的margin,看下面的圖1:

RecyclerView&Child.png

圖片很清晰的表示了 ItemView 的結(jié)構(gòu)(該圖不是特別精確茬故,后面會說到),這是只有一個 Child 的情況盖灸,我們從外往里看:

  1. 最外的邊界即 RecyclerView 的邊界
  2. 紅色部分是 RecyclerView 的 Padding,這個我們應(yīng)該能理解
  3. 橙色部分是我們?yōu)?ItemView 設(shè)置的 Margin磺芭,這個相信寫過布局都能理解
  4. 藍(lán)色部分就是我們在 getItemOffsets方法中給 outRect對象設(shè)置的值
  5. 最后的的黃色部分就是我們的 ItemView 了

總體就是說赁炎,getItemOffsets中設(shè)置的值就相當(dāng)于 margin 的一個存在。"圖說無憑",接下來就結(jié)合源碼講解一下這個圖的"依據(jù)"钾腺。首先看一下 getItemOffsets在哪里被調(diào)用了:

 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);  //被調(diào)用
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
 }

RecyclerView源碼中徙垫,這是 getItemOffsets唯一被調(diào)用的地方,代碼也很簡單放棒,就是將 RecyclerView中所有的(即通過addDecoration()方法添加的) ItemDecoration 遍歷一遍姻报,然后將我們設(shè)在 getItemOffsets 中設(shè)置的四個方向的值分別累加并存儲在insets這個Rect當(dāng)中。那么這個 insets又在哪里被調(diào)用了呢间螟,順著方法繼續(xù)跟蹤下去:

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 以及當(dāng)前 ItemView 的 Margin 相加寒亥,然后作為 getChildMeasureSpec的第三個參數(shù)傳進(jìn)去:

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方法的第三個參數(shù)標(biāo)注為 padding 邮府,在方法體這個 padding 的作用就是計算出 size 這個值,這個 size是就是后面測量中 Child(ItemView) 能達(dá)到的最大值溉奕。

也就是說我們設(shè)置的 ItemView 的 Margin 以及ItemDecoration.getItemOffsets中設(shè)置的值到頭來也是跟 Parent 的 Padding 一起來計算 ItemView 的可用空間褂傀,也就印證了上面的圖片,在上面說了該圖不精確就是因為

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

他們是一體的加勤,并沒有劃分成一段一段這樣仙辟,圖中的outRect也應(yīng)該改為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方法有兩個重載鳄梅,一個被標(biāo)注為 @deprecated,即棄用了叠国,我們知道,如果重寫了 onDraw戴尸,就可以在我們上面的 getItemOffsets中設(shè)置的范圍內(nèi)繪制粟焊,知其然還要知其所以然,我們看下源碼里面是怎樣實現(xiàn)的
#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方法調(diào)用一遍项棠,而且會把Recyclerview#onDraw(Canvas)方法中的Canvas傳遞給Decoration#onDraw,也就是說我們在Decoration中拿到了整個 RecyclerView 的 Canvas,那么我們基本就可以隨意繪制了挎峦,但是我們使用中會發(fā)現(xiàn)香追,我們繪制的區(qū)域如果在 ItemView 的范圍內(nèi)就會被蓋住,這是為什么呢坦胶?

由于View的繪制是先執(zhí)行 draw(Canvas)再到onDraw(Canvas)的透典,我們復(fù)習(xí)一波自定義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調(diào)用)税弃,然后再繪制child,所以我們在 Decoration#onDraw中繪制的界面會被 ItemView 遮擋也是理所當(dāng)然了讹语。

所以我們在繪制中就要計算好繪制的范圍钙皮,使繪制范圍在上面彩圖中藍(lán)色區(qū)域內(nèi),即getItemOffsets設(shè)置的范圍內(nèi)顽决,避免沒有顯示或者過分繪制的情況短条。

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頂層的裝飾就無法實現(xiàn),所以就有了這個方法蚓耽,他是在 ItemView 繪制完畢后才會被調(diào)用的渠牲,看下源碼的實現(xiàn):
#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)方法,會調(diào)用一系列的繪制流程步悠,包括onDraw(ItemDecoration的onDraw)以及dispatchDraw(ItemView的繪制)签杈,走完這些流程后才會調(diào)用Decoration#onDrawOver方法.

到此,我們就可以得出 onDraw>dispatchDraw(ItemView的繪制)>onDrawOver的執(zhí)行流程鼎兽。

5. 總結(jié)

  1. getItemOffsets用于提供一些空間(類似Margin)給 onDraw繪制
  2. onDraw方法繪制的內(nèi)容如果在 ItemView 的區(qū)域則可能被覆蓋(沒效果)
  3. onDraw>dispatchDraw(ItemView的繪制)>onDrawOver從左到右執(zhí)行

三 實戰(zhàn)

實戰(zhàn)將會從易到難進(jìn)行幾個小的Demo練習(xí)答姥。
由于這篇文章內(nèi)容已經(jīng)比較充實了,就把實戰(zhàn)部分放到下篇講解谚咬。

感謝你的閱讀鹦付,由于水平有限,如有錯誤懇請?zhí)嵝选?/p>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末择卦,一起剝皮案震驚了整個濱河市敲长,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌秉继,老刑警劉巖潘明,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異秕噪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滋戳,“玉大人喜命,你說我怎么就攤上這事疫稿“斡ィ” “怎么了凿掂?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵是牢,是天一觀的道長灯荧。 經(jīng)常有香客問我礁击,道長,這世上最難降的妖魔是什么逗载? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任哆窿,我火速辦了婚禮,結(jié)果婚禮上厉斟,老公的妹妹穿的比我還像新娘挚躯。我一直安慰自己,他們只是感情好擦秽,可當(dāng)我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布码荔。 她就那樣靜靜地躺著,像睡著了一般感挥。 火紅的嫁衣襯著肌膚如雪缩搅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天触幼,我揣著相機(jī)與錄音硼瓣,去河邊找鬼。 笑死域蜗,一個胖子當(dāng)著我的面吹牛巨双,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播霉祸,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼筑累,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了丝蹭?” 一聲冷哼從身側(cè)響起慢宗,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奔穿,沒想到半個月后镜沽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡贱田,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年缅茉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片男摧。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡蔬墩,死狀恐怖译打,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拇颅,我是刑警寧澤奏司,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站樟插,受9級特大地震影響韵洋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜黄锤,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一搪缨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧猜扮,春花似錦勉吻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至煮盼,卻和暖如春短纵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背僵控。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工香到, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人报破。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓悠就,卻偏偏與公主長得像,于是被迫代替她去往敵國和親充易。 傳聞我的和親對象是個殘疾皇子梗脾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,500評論 2 359

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