ItemDecoration解析(一) getItemOffsets

2019年7月1日17:56:30 更新

最新介紹:請移步這里
SuperDecoration工具類:求star

介紹

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.

All ItemDecorations are drawn in the order they were added, before the item views (in onDraw() and after the items (in onDrawOver(Canvas, RecyclerView, RecyclerView.State).

上面這段話是官方文檔對ItemDecoration的定義,貼出來不是為了裝逼,而是google的定義非常的精確,基本上介紹了ItemDecoration的用途。

根據(jù)自己的理解,簡單的翻譯下:

ItemDecoration 允許應(yīng)用給具體的View添加具體的圖畫或者layout的偏移,對于繪制View之間的分割線,視覺分組邊界等等是非常有用的。

所有的ItemDecorations按照被添加的順序在itemview之前(如果通過重寫`onDraw()`)或者itemview之后(如果通過重寫 `onDrawOver(Canvas, RecyclerView, RecyclerView.State)`)繪制。

先看看ItemDecoration中的方法

ItemDecoration方法

除去被標(biāo)記為過時的外,只剩如下三個方法:

public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)
  1. getItemOffests可以通過outRect.set(l,t,r,b)設(shè)置指定itemview的paddingLeftpaddingToppaddingRightpaddingBottom
  2. onDraw可以通過一系列c.drawXXX()方法在繪制itemView之前繪制我們需要的內(nèi)容。
  3. onDrawOveronDraw類似,只不過是在繪制itemView之后繪制,具體表現(xiàn)形式,就是繪制的內(nèi)容在itemview上層。

調(diào)用RecyclerViewaddItemDecoration()方法就可以給RecyclerView添加ItemDecoration了,注意這里是add并不是set,這意味著是可以給一個RecyclerView設(shè)置多個ItemDecoration的。

    // 添加ItemDecoration
    public void addItemDecoration(ItemDecoration decor) {
        addItemDecoration(decor, -1);
    }
    // 添加ItemDecoration
    public void addItemDecoration(ItemDecoration decor, int index) {
        if (mLayout != null) {
            mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
                    + " layout");
        }
        if (mItemDecorations.isEmpty()) {
            setWillNotDraw(false);
        }
        if (index < 0) {
            mItemDecorations.add(decor);
        } else {
            mItemDecorations.add(index, decor);
        }
        markItemDecorInsetsDirty();
        requestLayout();
    }

    // onLayout 最終會調(diào)用到此方法
    Rect getItemDecorInsetsForChild(View child) {
        ....
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            ...
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            ...
        }
        ...
    }

    @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);
        }
    }

    @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);
        }
    }

從源碼可以看出,事實確實如此,ItemDecoration會被add到集合中,然后RecyclerView會根據(jù)add的順序依次調(diào)用(getItemOffsets->onDraw->onDrawOver)的方法,因此,ItemDecoration的使用也變得更加靈活。

使用

介紹了這么多,是時候?qū)扅c(diǎn)代碼用用它了。
比如,給RecyclerView的每個Item設(shè)置間隔,這里我們要區(qū)分下RecyclerView的LayoutManager的類型,以及orientation類型。

LinearLayoutManger

一般情況下,設(shè)計稿會有下面兩種樣子的情形(先考慮HORIZONTAL的情況,VERTICAL處理起來原理也一樣)

  1. 第一排(recyclerview1) 第一個item,最后一個item沒有邊距
  2. 第二排(recyclerview2) 第一個item和最后一個item有邊距

在沒有ItemDecoration之前,我們一般都是在xml布局中調(diào)整Padding或者是Margin,然后在代碼中根據(jù)position來控制,這樣一來的話ViewHolder中會多出一些看上去很臃腫的代碼。對于第二種情況我們也可以通過設(shè)置RecyclerViewpaddingLeft以及paddingRight并設(shè)置clipToPaddingfasle來實現(xiàn),但是滑動到邊緣的時候,感覺會有點(diǎn)怪怪的。

如果我們使用ItemDecoration,將這部分的邏輯抽離出來,這樣的代碼不僅看起來,用起來更舒服,也更加符合面向?qū)ο蟮乃枷搿?/p>

首先我們定義一個類繼承RecyclerView.ItemDecoration,通過構(gòu)造方法傳入item間的間距mSpace以及邊距mEdgeSpace

    /**
     * @param mSpace item間的間距 默認(rèn)沒有邊距
     */
    public OffestDecoration(int mSpace, Context ctx) {
        this(mSpace, 0, ctx);
    }

    /**
     * @param mSpace     item間的間距
     * @param mEdgeSpace 邊距(padding)
     */
    public OffestDecoration(int mSpace, int mEdgeSpace, Context ctx) {
        this.mSpace = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mSpace, ctx.getResources().getDisplayMetrics()) + 0.5f);
        this.mEdgeSpace = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mEdgeSpace, ctx.getResources().getDisplayMetrics()) + 0.5f);
    }

重寫getItemOffsets方法判斷layoutManagerorientation,通過outRect.set()設(shè)置每個ItempaddingorientationHORIZONTAL時,第一個item需要額外設(shè)置左邊距的值,最后一個item需要設(shè)置右邊距的值,其他的item只需要設(shè)置paddingRightorientationVERTICAL時, 只需要把left,right換成top,bottom就ok了。

  @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        Log.i(TAG, "getItemOffsets");
        RecyclerView.LayoutManager manager = parent.getLayoutManager();
        int childPosition = parent.getChildAdapterPosition(view);
        int itemCount = parent.getAdapter().getItemCount();
        if (manager != null) {
            if (manager instanceof GridLayoutManager) {
                // 待會再處理
            } else if (manager instanceof LinearLayoutManager) {
                setLinearOffset(((LinearLayoutManager) manager).getOrientation(), outRect, childPosition, itemCount);
            }
        }
    }
    
    
    private void setLinearOffset(int orientation, Rect outRect, int childPosition, int itemCount) {
        if (orientation == LinearLayoutManager.HORIZONTAL) {
            if (childPosition == 0) {
                // 第一個要設(shè)置PaddingLeft
                outRect.set(mEdgeSpace, 0, mSpace, 0);
            } else if (childPosition == itemCount - 1) {
                // 最后一個設(shè)置PaddingRight
                outRect.set(0, 0, mEdgeSpace, 0);
            } else {
                outRect.set(0, 0, mSpace, 0);
            }
        } else {
            if (childPosition == 0) {
                // 第一個要設(shè)置PaddingTop
                outRect.set(0, mEdgeSpace, 0, mSpace);
            } else if (childPosition == itemCount - 1) {
                // 最后一個要設(shè)置PaddingBottom
                outRect.set(0, 0, 0, mEdgeSpace);
            } else {
                outRect.set(0, 0, 0, mSpace);
            }
        }
    }

GridLayoutManager

很多情況下,我們需要實現(xiàn)GridView樣式的RecyclerView,也分有邊距和沒邊距的情況,如下圖:

為了保證每個itemView在水平方向(orientationvertical時)或者垂直方向(orientationhorizon時)均分,那么必須讓每個itemviewpaddingleft+paddingRight(orientationvertical時)或者paddingTop+paddingBottomorientationhorizon時)相等,如下圖,每個紅色框框的尺寸是相等的,但每個itemviewpaddingLeftpaddingRight不同。

當(dāng)orientationvertical時,我們需要在getItemOffsets方法中計算每個Item的PaddingLeft,以及PaddingRight,保證每個Item的paddingLeft+paddingRight相等,這樣才能達(dá)到均分的目的。由于距離智商巔峰期(高三)已經(jīng)很久了,對數(shù)字也不敏感,我們不妨用最簡單粗暴的方法來找到其中的規(guī)律——套數(shù)字。

無邊距

假如 mSpace(間距)等于14,spanCount等于4,mEdgeSpace(邊距)等于0,那么

totalSpace = mSpace * (itemCount-1) + EdgeSpace * 2 = 42 // space總和
eachSpace = totalSpace / itemCount  = 10.5 // 每個item的leftPadding+rightPadding的和

列出每一列的paddingLeft以及paddingRight:

colunm L R
0 EdgeSpace(0) eachSpace-L0(10.5)
1 mSpace-R0(3.5) eachSpace-L1 (7)
2 mSpace-R1(7) eachSpace-R2(3.5)
3 mSpace-R2(10.5) EdgeSpace(0)

可以看出

Left是從 0 到 eachSpace 等差數(shù)列
Right用eachSpace -Left算出

有邊距

假如 mSpace(間距)等于14,spanCount等于4,mEdgeSpace(邊距)等于12,那么

totalSpace = mSpace * (itemCount-1) + EdgeSpace * 2 = 66 // space總和
eachSpace = totalSpace / itemCount= 16.5 // item的leftPadding+rightPadding的和

列出每一列的paddingLeft以及paddingRight:

colunm L R
0 EdgeSpace(12) eachSpace-L0(4.5)
1 mSpace-R0(9.5) eachSpace-L1 (7)
2 mSpace-R1(7) eachSpace-R2(9.5)
3 mSpace-R2(4.5) EdgeSpace(12)

可以看出

Left是從 EdgeSpace 到 (eachSpace - EdgeSpace)  等差數(shù)列
Right用eachSpace -Left算出

計算

根據(jù)上面得出的規(guī)律,paddingLeft都是等差數(shù)列,而且我們已知a_1以及a_n,根據(jù)等差數(shù)列的公式a_n = (n-1)d + a_1,很容易計算出公差d:

當(dāng)邊距為0時,d = \frac{eachSpace}{spanCount-1} ,當(dāng)邊距不為0時,d = \frac{eachSpace - EdgeSpace-EdgeSpace}{spanCount-1}

所以paddingLeft_n = colunm*dpaddingRight_n = eachSpace-paddingLeft_n

列數(shù)column\equiv childPosition\quad(mod\quad spanCount):

上面的分析并沒有考慮orientationhorizontal的情況,其實只需要把topbottomleftright對調(diào)下就行了,最后貼下代碼:

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        RecyclerView.LayoutManager manager = parent.getLayoutManager();
        int childPosition = parent.getChildAdapterPosition(view);
        int itemCount = parent.getAdapter().getItemCount();
        if (manager != null) {
            if (manager instanceof GridLayoutManager) {
                // manager為GridLayoutManager時
                setGridOffset(((GridLayoutManager) manager).getOrientation(), ((GridLayoutManager) manager).getSpanCount(), outRect, childPosition, itemCount);
            } else if (manager instanceof LinearLayoutManager) {
                // manager為LinearLayoutManager時
                setLinearOffset(((LinearLayoutManager) manager).getOrientation(), outRect, childPosition, itemCount);
            }
        }
    }

    /**
     * 設(shè)置GridLayoutManager 類型的 offest
     *
     * @param orientation   方向
     * @param spanCount     個數(shù)
     * @param outRect       padding
     * @param childPosition 在 list 中的 postion
     * @param itemCount     list size
     */
    private void setGridOffset(int orientation, int spanCount, Rect outRect, int childPosition, int itemCount) {
        float totalSpace = mSpace * (spanCount - 1) + mEdgeSpace * 2; // 總共的padding值
        float eachSpace = totalSpace / spanCount; // 分配給每個item的padding值
        int column = childPosition % spanCount; // 列數(shù)
        int row = childPosition / spanCount;// 行數(shù)
        float left;
        float right;
        float top;
        float bottom;
        if (orientation == GridLayoutManager.VERTICAL) {
            top = 0; // 默認(rèn) top為0
            bottom = mSpace; // 默認(rèn)bottom為間距值
            if (mEdgeSpace == 0) {
                left = column * eachSpace / (spanCount - 1);
                right = eachSpace - left;
                // 無邊距的話  只有最后一行bottom為0
                if (itemCount / spanCount == row) {
                    bottom = 0;
                }
            } else {
                if (childPosition < spanCount) {
                    // 有邊距的話 第一行top為邊距值
                    top = mEdgeSpace;
                } else if (itemCount / spanCount == row) {
                    // 有邊距的話 最后一行bottom為邊距值
                    bottom = mEdgeSpace;
                }
                left = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
                right = eachSpace - left;
            }
        } else {
            // orientation == GridLayoutManager.HORIZONTAL 跟上面的大同小異, 將top,bottom替換為left,right即可
            left = 0;
            right = mSpace;
            if (mEdgeSpace == 0) {
                top = column * eachSpace / (spanCount - 1);
                bottom = eachSpace - top;
                if (itemCount / spanCount == row) {
                    right = 0;
                }
            } else {
                if (childPosition < spanCount) {
                    left = mEdgeSpace;
                } else if (itemCount / spanCount == row) {
                    right = mEdgeSpace;
                }
                top = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
                bottom = eachSpace - top;
            }
        }
        outRect.set((int) left, (int) top, (int) right, (int) bottom);
    }

getItemOffsets的用法基本介紹完了,下一章節(jié)再探討探討onDraw以及onDrawOver的用法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,844評論 2 372

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