RecyclerView系列二:RecyclerView.ItemDecoration的詳解使用

前言

在很早很早以前(long long ago),ListView鼎盛的時代有一個屬性叫做divider粱挡。但是在RecycleView上面就是找不到他殊橙,那怎么辦呢辐宾?妆偏?泻肯?直到后來有一天發(fā)現(xiàn)他變身了,變成了ItemDecoration嫌套。實在是扯不下去了敞葛,直接開始吧誉察!
這篇博客醞釀了好長時間,希望不會讓各位看官失望惹谐。

任務(wù)

了解ItemDecoration的原理持偏,自己可以添加分割線,每個 ItemView 上疊加一個角標(biāo)氨肌,自定義 RecyclerView 中的頭部或者是粘性頭部鸿秆。

分析和實戰(zhàn)

1.具體的使用

RecyclerView的簡單使用可以參考前一篇RecyclerView系列一:簡單使用
我們在頁面中放兩個RecyclerView,上面一個下面一個怎囚,用來對比卿叽。如下圖所示:

現(xiàn)在開始處理:寫TextItemDecoration類讓他繼承RecyclerView.ItemDecoration,主要的代碼如下所示:

    class TextItemDecoration extends RecyclerView.ItemDecoration {

        //設(shè)置ItemView的內(nèi)嵌偏移長度(inset)
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
        }

        // 在子視圖上設(shè)置繪制范圍恳守,并繪制內(nèi)容
        // 繪制圖層在ItemView以下考婴,所以如果繪制區(qū)域與ItemView區(qū)域相重疊,會被遮擋
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
        }

        //同樣是繪制內(nèi)容井誉,但與onDraw()的區(qū)別是:繪制在圖層的最上層
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
        }
    }

2.分析方法getItemOffsets()

分析這里的時候蕉扮,我們先來盜個圖,如下:


getItemOffsets()總的概括

我們所有的分析這個圖就可以概括了】攀ィ現(xiàn)在我們開始分析這個方法喳钟,在Android Studio中看super.getItemOffsets(outRect, view, parent, state);這個方法,最終我們在RecyclerView看到outRect.set(0, 0, 0, 0);這一行代碼在岂。
那么我們就拿outRect開刀奔则。TextItemDecoration中代碼如下:

        //設(shè)置ItemView的內(nèi)嵌偏移長度(inset)
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            //只是添加下面這一行代碼
            outRect.set(50, 50, 50, 50);
        }

把這個·ItemDecoration·放在下面的RecyclerView上面,代碼如下:

 recyclerView2.addItemDecoration(new TextItemDecoration());

運行效果蔽午,如下圖所示:


示意圖

再來一張單獨的圖片易茬,如下所示:


sc3.png
  • 如上圖所示,RecyclerView 中的 ItemView 外面會包裹著一個矩形(outRect)。
  • 內(nèi)嵌偏移長度:該矩形(outRect)與 ItemView的間隔.
  • 默認(rèn)的情況下抽莱,top范抓、left、right食铐、bottom都是0匕垫,所以矩形和ItemView就重疊了。
2.1源碼分析(直接上源碼):

下面的代碼都是在RecyclerView中虐呻,可以在RecyclerView里面找到源碼:

    //測量所有的子view的寬和高象泵,得到這個子view的Rect。然后就能得到這一塊真正的寬和高斟叼。
    //注意還有padding的值偶惠。
    public void measureChild(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() + widthUsed, lp.width,
                canScrollHorizontally());
        final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
                canScrollVertically());
        if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
            child.measure(widthSpec, heightSpec);
        }
    }

    //得到每個子view相應(yīng)的Rect,
    //mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
    //上面的代碼就是設(shè)置相應(yīng)的四個值朗涩,就和我們的TextItemDecoration類里面的代碼對應(yīng)起來了忽孽。
    Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }

        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        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;
    }


3.onDraw()

我們先來看看我們自定義的TextItemDecoration類里面的onDraw()代碼,如下所示:

        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
        }

很明顯上面?zhèn)鬟f了一個Canvas 參數(shù)對象馋缅,所以它擁有了繪制的能力扒腕。

注意: 1.getItemOffsets 是針對每一個 ItemView绢淀,而 onDraw 方法卻是針對 RecyclerView 本身萤悴,所以在 onDraw 方法中需要遍歷屏幕上可見的 ItemView,分別獲取它們的位置信息皆的,然后分別的繪制對應(yīng)的分割線覆履。
2.Itemdecoration的onDraw()繪制會先于ItemView的onDraw()繪制,

第二點會出現(xiàn)如下的情況:


示意圖

出現(xiàn)上面的問題解決方案是getItemOffsets()與onDraw()一塊使用费薄。說的我一愣一愣的硝全,最主要的問題onDrow()的時候,是怎樣得到相應(yīng)的點楞抡。


sc5.jpg

英雄莫怕伟众,請看上面的代碼onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)

看第二個參數(shù):RecyclerView parent 這就是我們的突破點。(這里有一個疑問點召廷?凳厢??我們第4節(jié)處理竞慢。)
  int childCount = parent.getChildCount();
  View child = parent.getChildAt(i);
  上面的代碼就能解決我們的問題先紫。

上代碼實戰(zhàn):要實現(xiàn)的效果如下:


sc6.png

代碼實現(xiàn)如下:

    class TextItemDecoration extends RecyclerView.ItemDecoration {
        private Paint mPaint;

        public TextItemDecoration() {
            this.mPaint = new Paint();
            mPaint.setColor(Color.YELLOW);
            // 畫筆顏色設(shè)置為黃色
        }

        //設(shè)置ItemView的內(nèi)嵌偏移長度(inset)
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            outRect.set(50, 50, 50, 50);
        }

        // 在子視圖上設(shè)置繪制范圍,并繪制內(nèi)容
        // 繪制圖層在ItemView以下筹煮,所以如果繪制區(qū)域與ItemView區(qū)域相重疊遮精,會被遮擋
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            // 獲取RecyclerView的Child view的個數(shù)
            int childCount = parent.getChildCount();
            // 遍歷每個Item,分別獲取它們的位置信息败潦,然后再繪制對應(yīng)的分割線
            for (int i = 0; i < childCount; i++) {
                // 獲取每個Item的位置
                final View child = parent.getChildAt(i);
                // 設(shè)置矩形(分割線)的寬度為10px
                final int mDivider = 10;
                // 矩形左上頂點 = (ItemView的左邊界,ItemView的下邊界)
                final int left = child.getLeft();
                final int top = child.getBottom();
                // 矩形右下頂點 = (ItemView的右邊界,矩形的下邊界)
                final int right = child.getRight();
                final int bottom = top + mDivider;
                // 通過Canvas繪制矩形(分割線)
                c.drawRect(left, top, right, bottom, mPaint);
            }
        }


        //同樣是繪制內(nèi)容本冲,但與onDraw()的區(qū)別是:繪制在圖層的最上層
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
        }
    }

4.onDrawOver()

我們先來看看我們自定義的TextItemDecoration類里面的onDrawOver()代碼准脂,如下所示:

        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
        }

很明顯onDrawOver里面的參數(shù)和onDraw里面的參數(shù)一模一樣,那還要onDrawOver有什么用呢檬洞?意狠??疮胖。請看下圖:


示意圖

最主要的就是紫色區(qū)域环戈,onDrawOver的使用方法和onDraw類似。現(xiàn)在我們在每一個條目的右上角加一個圖標(biāo)澎灸。效果如下:


sc8.png

代碼實現(xiàn)如下:

    class TextItemDecoration extends RecyclerView.ItemDecoration {
        private Paint mPaint;
        private Bitmap bitmap;

        public TextItemDecoration() {
            this.mPaint = new Paint();
            mPaint.setColor(Color.YELLOW);
            // 畫筆顏色設(shè)置為黃色
            bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.email);
        }

        //設(shè)置ItemView的內(nèi)嵌偏移長度(inset)
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            outRect.set(50, 50, 50, 50);
        }

        // 在子視圖上設(shè)置繪制范圍院塞,并繪制內(nèi)容
        // 繪制圖層在ItemView以下,所以如果繪制區(qū)域與ItemView區(qū)域相重疊性昭,會被遮擋
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            // 獲取RecyclerView的Child view的個數(shù)
            int childCount = parent.getChildCount();
            // 遍歷每個Item拦止,分別獲取它們的位置信息,然后再繪制對應(yīng)的分割線
            for (int i = 0; i < childCount; i++) {
                // 獲取每個Item的位置
                final View child = parent.getChildAt(i);
                // 設(shè)置矩形(分割線)的寬度為10px
                final int mDivider = 10;
                // 矩形左上頂點 = (ItemView的左邊界,ItemView的下邊界)
                final int left = child.getLeft();
                final int top = child.getBottom();
                // 矩形右下頂點 = (ItemView的右邊界,矩形的下邊界)
                final int right = child.getRight();
                final int bottom = top + mDivider;
                // 通過Canvas繪制矩形(分割線)
                c.drawRect(left, top, right, bottom, mPaint);
            }
        }


        //同樣是繪制內(nèi)容糜颠,但與onDraw()的區(qū)別是:繪制在圖層的最上層
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
            int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                final int left = child.getRight() - bitmap.getWidth();
                final int top = child.getTop();
                c.drawBitmap(bitmap, left, top, mPaint);
            }
        }
    }

5.自定義 RecyclerView 中的頭部或者是粘性頭部

具體的思路如下圖所示:


示意圖

我們的操作在OutRect里面處理汹族。下面我們用假數(shù)據(jù)處理,頁面中只保留一個RecyclerView其兴。我們只分析TextItemDecoration里面的代碼顶瞒。代碼如下:

    class TextItemDecoration extends RecyclerView.ItemDecoration {
        private Paint mPaint;

        public TextItemDecoration() {
            this.mPaint = new Paint();
            // 畫筆顏色設(shè)置為黃色
            mPaint.setColor(Color.YELLOW);
        }

        //設(shè)置ItemView的內(nèi)嵌偏移長度(inset)
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            int position = parent.getChildAdapterPosition(view);
            if (position % 5 == 0) {
                outRect.set(0, 50, 0, 0);
            }
        }

        // 在子視圖上設(shè)置繪制范圍,并繪制內(nèi)容
        // 繪制圖層在ItemView以下元旬,所以如果繪制區(qū)域與ItemView區(qū)域相重疊榴徐,會被遮擋
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            // 獲取RecyclerView的Child view的個數(shù)
            int childCount = parent.getChildCount();
            // 遍歷每個Item,分別獲取它們的位置信息匀归,然后再繪制對應(yīng)的分割線
            for (int i = 0; i < childCount; i++) {
                if (i % 5 == 0) {
                    View child = parent.getChildAt(i);
                    int left = 0;
                    int top = child.getTop() - 50;
                    int right = child.getRight();
                    int bottom = child.getTop();
                    c.drawRect(left, top, right, bottom, mPaint);
                }
            }
        }


        //同樣是繪制內(nèi)容坑资,但與onDraw()的區(qū)別是:繪制在圖層的最上層
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
        }
    }

效果圖如下:


示意圖

黃條才是我要的真愛,這怎么亂套了穆端?袱贮?問題就出在如下的代碼里面

        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            // 獲取RecyclerView的Child view的個數(shù)
            int childCount = parent.getChildCount();
            // 遍歷每個Item,分別獲取它們的位置信息体啰,然后再繪制對應(yīng)的分割線
            for (int i = 0; i < childCount; i++) {
                if (i % 5 == 0) {
                    View child = parent.getChildAt(i);
                    int left = 0;
                    int top = child.getTop() - 50;
                    int right = child.getRight();
                    int bottom = child.getTop();
                    c.drawRect(left, top, right, bottom, mPaint);
                }
            }
        }

上面就是有問題的代碼:

int childCount = parent.getChildCount();
得到的是這一屏幕有多少個孩子攒巍。不是說得到的總的孩子的數(shù)目。

我們修改一下onDraw里面的代碼狡赐,如下:

        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            // 獲取RecyclerView的Child view的個數(shù)
            int childCount = parent.getChildCount();
            // 遍歷每個Item窑业,分別獲取它們的位置信息,然后再繪制對應(yīng)的分割線
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                int index = parent.getChildAdapterPosition(child);
                if (index % 5 == 0) {
                    int left = 0;
                    int top = child.getTop() - 50;
                    int right = child.getRight();
                    int bottom = child.getTop();
                    c.drawRect(left, top, right, bottom, mPaint);
                }
            }
        }

效果圖如下:


示意圖

這距離我們想要的效果越來躍進了枕屉。能不能在黃條在最上面的時候停留呢常柄??還有就是推上去?西潘?不出所料所有的操作都是在這里面了卷玉。
下面我們直接上代碼,思考留給我親愛的讀者:

        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);

            // 獲取RecyclerView的Child view的個數(shù)
            int childCount = parent.getChildCount();
            // 遍歷每個Item喷市,分別獲取它們的位置信息相种,然后再繪制對應(yīng)的分割線
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                int index = parent.getChildAdapterPosition(child);
                if (index % 5 == 0) {
                    int item = (index) / 5;
                    if (i < 5) {
                        if (i == 1 && child.getTop() < 100) {
                            int left = 0;
                            int top = child.getTop() - 100;
                            int right = child.getRight();
                            int bottom = child.getTop() - 50;
                            c.drawRect(left, top, right, bottom, mPaint);
                            c.drawText("這是條目" + item + "視圖i是+" + i + "頂部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
                        } else {
                            int left = 0;
                            int top = 0;
                            int right = child.getRight();
                            int bottom = 50;
                            c.drawRect(left, top, right, bottom, mPaint);
                            if (i == 0) {
                                c.drawText("這是條目" + (item + 1) + "視圖i是+" + i + "頂部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
                            } else {
                                c.drawText("這是條目" + (item) + "視圖i是+" + i + "頂部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
                            }
                        }

                    }
                    if (i != 0) {
                        int left = 0;
                        int top = child.getTop() - 50;
                        int right = child.getRight();
                        int bottom = child.getTop();
                        c.drawRect(left, top, right, bottom, mPaint);
                        c.drawText("這是條目" + item + "視圖i是+" + i + "頂部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
                    }
                }
            }
        }

我們的效果圖如下:


示意圖

我把要打印的都給小伙伴們打印出來了。具體的優(yōu)化看自己的需求優(yōu)化就可以了品姓。

總結(jié)

本篇文章介紹了RecyclerView.ItemDecoration的使用寝并,還有它的原理。其實還是挺簡單的腹备。我相信簡單的自定義小伙伴應(yīng)該都會了衬潦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市植酥,隨后出現(xiàn)的幾起案子镀岛,更是在濱河造成了極大的恐慌,老刑警劉巖友驮,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漂羊,死亡現(xiàn)場離奇詭異,居然都是意外死亡卸留,警方通過查閱死者的電腦和手機走越,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來艾猜,“玉大人买喧,你說我怎么就攤上這事捻悯〈以撸” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵今缚,是天一觀的道長算柳。 經(jīng)常有香客問我,道長姓言,這世上最難降的妖魔是什么瞬项? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮何荚,結(jié)果婚禮上囱淋,老公的妹妹穿的比我還像新娘。我一直安慰自己餐塘,他們只是感情好妥衣,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般税手。 火紅的嫁衣襯著肌膚如雪蜂筹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天芦倒,我揣著相機與錄音艺挪,去河邊找鬼。 笑死兵扬,一個胖子當(dāng)著我的面吹牛麻裳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播器钟,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼掂器,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了俱箱?” 一聲冷哼從身側(cè)響起国瓮,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎狞谱,沒想到半個月后乃摹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡跟衅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年孵睬,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伶跷。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡掰读,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出叭莫,到底是詐尸還是另有隱情蹈集,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布雇初,位于F島的核電站拢肆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏靖诗。R本人自食惡果不足惜郭怪,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望刊橘。 院中可真熱鬧鄙才,春花似錦、人聲如沸促绵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至叙甸,卻和暖如春颖医,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背裆蒸。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工熔萧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人僚祷。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓佛致,卻偏偏與公主長得像,于是被迫代替她去往敵國和親辙谜。 傳聞我的和親對象是個殘疾皇子俺榆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

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