自定義RecyclerView.ItemDecoration,實現Item的等間距分割以及分割線效果

1.背景

RecyclerView 是谷歌 V7 包下新增的控件,用來替代 ListView 和 GridView 使用的一個控件。在使用的過程中,往往需要使用到 divider 的效果 ( item 之間的分割線 )。而 RecyclerView 并不像 ListView 一樣自帶有 divider 的屬性。而是需要用到 RecyclerView.ItemDecoration 這樣一個類,但是 ItemDecoration 是一個抽象類,而且 android 內部并沒有給它做一些效果的實現。那么就需要我們自己去繼承并實現其中的方法,本文講述的就是在 GridLayoutManager 和 LinearLayoutManager 下如何去實現 ItemDecoration。至于 RecyclerView.ItemDecoration 的具體分析,大家可以去看看這篇文章 深入理解 RecyclerView 系列之一:ItemDecoration 這里不作過多的闡述。

2.實現基本的 Item 的 divider

2.1 創建 SpacesItemDecoration

創建一個類 SpacesItemDecoration 繼承于 RecyclerView.ItemDecoration ,實現其中的 onDraw 和 getItemOffsets 方法,在這里我們的設計是左右距離相等,上下距離相等。

public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
    private int leftRight;
    private int topBottom;

    public SpacesItemDecoration(int leftRight, int topBottom) {
        this.leftRight = leftRight;
        this.topBottom = topBottom;
    }

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

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    
    }
}

在這里我們主要實現的方法是 onDraw 和 getItemOffsets, getItemOffsets 主要是確定 divider 的范圍,而 onDraw 是對 divider 的具體實現。

2.2 LinearLayoutManager 下 divider 的實現

首先在 getItemOffsets 方法中需要判斷當前的 RecyclerView 所采用的哪種 LayoutManager。這里要注意的是 GridLayoutManager 是繼承 LinearLayoutManager 的,所以需要先判斷是否為 GridLayoutManager。

private SpacesItemDecorationEntrust getEntrust(RecyclerView.LayoutManager manager) {
        SpacesItemDecorationEntrust entrust = null;
        //要注意這邊的GridLayoutManager是繼承LinearLayoutManager,所以要先判斷GridLayoutManager
        if (manager instanceof GridLayoutManager) {
            entrust = new GridEntrust(leftRight, topBottom, mColor);
        } else {//其他的都當做Linear來進行計算
            entrust = new LinearEntrust(leftRight, topBottom, mColor);
        }
        return entrust;
    }

然后我們來看具體的實現,首先判斷是 VERTICAL 還是 HORIZONTAL 。對于 VERTICAL,每一個 item 必需的是 top,left 和 right,但是最后一個 item 還需要 bottom。而對于 HORIZONTAL ,每一個 item 必需的是 top,left 和 bottom,但是最后一個 item 還需要 right。

 @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
        //豎直方向的
        if (layoutManager.getOrientation() == LinearLayoutManager.VERTICAL) {
            //最后一項需要 bottom
            if (parent.getChildAdapterPosition(view) == layoutManager.getItemCount() - 1) {
                outRect.bottom = topBottom;
            }
            outRect.top = topBottom;
            outRect.left = leftRight;
            outRect.right = leftRight;
        } else {
            //最后一項需要right
            if (parent.getChildAdapterPosition(view) == layoutManager.getItemCount() - 1) {
                outRect.right = leftRight;
            }
            outRect.top = topBottom;
            outRect.left = leftRight;
            outRect.bottom = topBottom;
        }
    }

就這樣,divider 效果就實現了(當然是沒有任何的顏色的)。調用方式只需要。

  int leftRight = dip2px(7);
  int topBottom = dip2px(7);
  rv_content.addItemDecoration(new SpacesItemDecoration(leftRight, topBottom));
VERTICAL
HORIZONTAL.png

2.3 GridLayoutManager 下 divider 的實現

對于 GridLayoutManager 下的實現,相比 LinearLayoutManager 要復雜一些。首先當然是判斷 VERTICAL 還是 HORIZONTAL。一般來說有三種情況的,如圖所示:


樣式.png

由于第二種布局樣式考慮的情況比較多,目前沒有找到比較好的方法去進行判斷,所以在這里只對另外兩種布局樣式進行考慮

@Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
        final GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) view.getLayoutParams();
        final int childPosition = parent.getChildAdapterPosition(view);
        final int spanCount = layoutManager.getSpanCount();

        if (layoutManager.getOrientation() == GridLayoutManager.VERTICAL) {
            //判斷是否在第一排
            if (layoutManager.getSpanSizeLookup().getSpanGroupIndex(childPosition, spanCount) == 0) {//第一排的需要上面
                outRect.top = topBottom;
            }
            outRect.bottom = topBottom;
            //這里忽略和合并項的問題,只考慮占滿和單一的問題
            if (lp.getSpanSize() == spanCount) {//占滿
                outRect.left = leftRight;
                outRect.right = leftRight;
            } else {
                outRect.left = (int) (((float) (spanCount - lp.getSpanIndex())) / spanCount * leftRight);
                outRect.right = (int) (((float) leftRight * (spanCount + 1) / spanCount) - outRect.left);
            }
        } else {
            if (layoutManager.getSpanSizeLookup().getSpanGroupIndex(childPosition, spanCount) == 0) {//第一排的需要left
                outRect.left = leftRight;
            }
            outRect.right = leftRight;
            //這里忽略和合并項的問題,只考慮占滿和單一的問題
            if (lp.getSpanSize() == spanCount) {//占滿
                outRect.top = topBottom;
                outRect.bottom = topBottom;
            } else {
                outRect.top = (int) (((float) (spanCount - lp.getSpanIndex())) / spanCount * topBottom);
                outRect.bottom = (int) (((float) topBottom * (spanCount + 1) / spanCount) - outRect.top);
            }
        }
    }

在這里,對于 VERTICAL 下,每個 item 需要的是 bottom,然后第一排需要 top,同時在這里使用了 GridLayoutManager 的一個方法 layoutManager.getSpanSizeLookup().getSpanGroupIndex(childPosition, spanCount) 該方法可以用于判斷 item 在布局中所處于的行數。因為這里的 outRect 的值會一起統計到每個 item 的寬高之中。為了保證每個 item 的大小一致,所以這里的每個 item 的 left 和 right 的和必須保持一致,具體的計算方法如下:

計算方式.png

這樣,GridLayoutManager 的效果就實現了,調用方法跟LinearLayoutManager 下是一樣的。效果如下
VERTICAL

HORIZONTAL

3.實現 Item 的帶顏色分割線的效果

3.1 LinearManager 下的實現

上述基本實現了 item 分割的效果,但是它沒有辦法設置顏色。要實現顏色,首先我們得傳入一個顏色色值。

//color的傳入方式是resouce.getcolor
protected Drawable mDivider;

public SpacesItemDecorationEntrust(int leftRight, int topBottom, int mColor) {
        this.leftRight = leftRight;
        this.topBottom = topBottom;
        if (mColor != 0) {
            mDivider = new ColorDrawable(mColor);
        }
    }

有了顏色,那么我們就需要去重寫 onDraw 方法了,我們需要去確定繪制的區域。先貼上代碼

 @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
        //沒有子view或者沒有沒有顏色直接return
        if (mDivider == null || layoutManager.getChildCount() == 0) {
            return;
        }
        int left;
        int right;
        int top;
        int bottom;
        final int childCount = parent.getChildCount();
        if (layoutManager.getOrientation() == GridLayoutManager.VERTICAL) {
            for (int i = 0; i < childCount - 1; i++) {
                final View child = parent.getChildAt(i);
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
                //將有顏色的分割線處于中間位置
                float center = (layoutManager.getTopDecorationHeight(child) - topBottom) / 2;
                //計算下邊的
                left = layoutManager.getLeftDecorationWidth(child);
                right = parent.getWidth() - layoutManager.getLeftDecorationWidth(child);
                top = (int) (child.getBottom() + params.bottomMargin + center);
                bottom = top + topBottom;
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        } else {
            for (int i = 0; i < childCount - 1; i++) {
                final View child = parent.getChildAt(i);
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
                //將有顏色的分割線處于中間位置
                float center = (layoutManager.getLeftDecorationWidth(child) - leftRight) / 2;
                //計算右邊的
                left = (int) (child.getRight() + params.rightMargin + center);
                right = left + leftRight;
                top = layoutManager.getTopDecorationHeight(child);
                bottom = parent.getHeight() - layoutManager.getTopDecorationHeight(child);
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }
    }

RecyclerView 的機制是去繪制要顯示在屏幕中的 View,而沒有顯示出來的是不會去繪制。所以這邊需要使用的是 layoutManager.getChildCount() 而不是 layoutManager.getItemCount()。對于 LinearManager 下來說,需要繪制分割線的區域是兩個 item 之間,這里分為 VERTICAL 和 HORIZONTAL。我們拿 VERTICAL 來進行分析,首先獲取 item 以及它的 LayoutParams,在這里計算 float center = (layoutManager.getTopDecorationHeight(child) - topBottom) / 2; 因為一個 RecyclerView 可以添加多個 ItemDecoration,而且方法的調用順序是先實現所有 ItemDecoration 的 getItemOffsets 方法,然后再去實現 onDraw 方法。目前沒有找到辦法去解決每個 ItemDecoration 的具體區域。所以退而求其次的將分割線繪制在所有 ItemDecoration 的中間區域(基本能滿足一般的需求,當然可以自己修改位置滿足自己的需求)。然后我們要去確定繪制的區域,left 就是所有 ItemDecoration 的寬度,right 就是 parent 的寬度減去所有 ItemDecoration 的寬度。top 是 child 的底部位置然后還要加上center(center的目的是繪制在中間區域),bottom 就是 top 加上需要繪制的高度。同理在 HORIZONTAL 模式下可以類似的實現。使用一個 ItemDecoration 的效果

 int leftRight = dip2px(2);
 int topBottom = dip2px(2);
 rv_content.addItemDecoration(new SpacesItemDecoration(leftRight, topBottom,getResources().getColor(R.color.colorPrimary)));
VERTICAL

HORIZONTAL

當然你也可以使用多個 ItemDecoration

 int leftRight = dip2px(10);
 int topBottom = dip2px(10);
 rv_content.addItemDecoration(new SpacesItemDecoration(leftRight, topBottom));
 rv_content.addItemDecoration(new SpacesItemDecoration(dip2px(2), dip2px(2), getResources().getColor(R.color.colorPrimary)));
VERTICAL

HORIZONTAL

3.2 GridManager 下的實現

GridManager 下的實現的步驟類似與 LinearManager,不同的是確定繪制分割線的區域。它的分割線的區域是相鄰的 item 之間都需要有分割線。廢話不多說,先上代碼。

@Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
        final GridLayoutManager.SpanSizeLookup lookup = layoutManager.getSpanSizeLookup();

        if (mDivider == null || layoutManager.getChildCount() == 0) {
            return;
        }
        //判斷總的數量是否可以整除
        int spanCount = layoutManager.getSpanCount();
        int left, right, top, bottom;
        final int childCount = parent.getChildCount();
        if (layoutManager.getOrientation() == GridLayoutManager.VERTICAL) {
            for (int i = 0; i < childCount; i++) {
                final View child = parent.getChildAt(i);
                //將帶有顏色的分割線處于中間位置
                final float centerLeft = ((float) (layoutManager.getLeftDecorationWidth(child) + layoutManager.getRightDecorationWidth(child))
                        * spanCount / (spanCount + 1) + 1 - leftRight) / 2;
                final float centerTop = (layoutManager.getBottomDecorationHeight(child) + 1 - topBottom) / 2;
                //得到它在總數里面的位置
                final int position = parent.getChildAdapterPosition(child);
                //獲取它所占有的比重
                final int spanSize = lookup.getSpanSize(position);
                //獲取每排的位置
                final int spanIndex = lookup.getSpanIndex(position, layoutManager.getSpanCount());
                //判斷是否為第一排
                boolean isFirst = layoutManager.getSpanSizeLookup().getSpanGroupIndex(position, spanCount) == 0;
                //畫上邊的,第一排不需要上邊的,只需要在最左邊的那項的時候畫一次就好
                if (!isFirst && spanIndex == 0) {
                    left = layoutManager.getLeftDecorationWidth(child);
                    right = parent.getWidth() - layoutManager.getLeftDecorationWidth(child);
                    top = (int) (child.getTop() - centerTop) - topBottom;
                    bottom = top + topBottom;
                    mDivider.setBounds(left, top, right, bottom);
                    mDivider.draw(c);
                }
                //最右邊的一排不需要右邊的
                boolean isRight = spanIndex + spanSize == spanCount;
                if (!isRight) {
                    //計算右邊的
                    left = (int) (child.getRight() + centerLeft);
                    right = left + leftRight;
                    top = child.getTop();
                    if (!isFirst) {
                        top -= centerTop;
                    }
                    bottom = (int) (child.getBottom() + centerTop);
                    mDivider.setBounds(left, top, right, bottom);
                    mDivider.draw(c);
                }
            }
        } else {
            for (int i = 0; i < childCount; i++) {
                final View child = parent.getChildAt(i);
                //將帶有顏色的分割線處于中間位置
                final float centerLeft = (layoutManager.getRightDecorationWidth(child) + 1 - leftRight) / 2;
                final float centerTop = ((float) (layoutManager.getTopDecorationHeight(child) + layoutManager.getBottomDecorationHeight(child))
                        * spanCount / (spanCount + 1) - topBottom) / 2;
                //得到它在總數里面的位置
                final int position = parent.getChildAdapterPosition(child);
                //獲取它所占有的比重
                final int spanSize = lookup.getSpanSize(position);
                //獲取每排的位置
                final int spanIndex = lookup.getSpanIndex(position, layoutManager.getSpanCount());
                //判斷是否為第一列
                boolean isFirst = layoutManager.getSpanSizeLookup().getSpanGroupIndex(position, spanCount) == 0;
                //畫左邊的,第一排不需要左邊的,只需要在最上邊的那項的時候畫一次就好
                if (!isFirst && spanIndex == 0) {
                    left = (int) (child.getLeft() - centerLeft) - leftRight;
                    right = left + leftRight;
                    top = layoutManager.getRightDecorationWidth(child);
                    bottom = parent.getHeight() - layoutManager.getTopDecorationHeight(child);
                    mDivider.setBounds(left, top, right, bottom);
                    mDivider.draw(c);
                }
                //最下的一排不需要下邊的
                boolean isRight = spanIndex + spanSize == spanCount;
                if (!isRight) {
                    //計算右邊的
                    left = child.getLeft();
                    if (!isFirst) {
                        left -= centerLeft;
                    }
                    right = (int) (child.getRight() + centerTop);
                    top = (int) (child.getBottom() + centerLeft);
                    bottom = top + leftRight;
                    mDivider.setBounds(left, top, right, bottom);
                    mDivider.draw(c);
                }
            }
        }
    }

我們就 VERTICAL 的情況下來進行分析,首先橫向的分割線,只需要在最左側的 item 繪制出來的時候進行分割線的繪制就行了。當然最后一排是不需要的。

 if (!isFirst && spanIndex == 0) {
        left = layoutManager.getLeftDecorationWidth(child);
        right = parent.getWidth() - layoutManager.getLeftDecorationWidth(child);
        top = (int) (child.getTop() - centerTop) - topBottom;
        bottom = top + topBottom;
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);
 }

水平的分割線的計算方式類似與 LinearLayoutManager 下的計算方式。這里不過多闡述。而豎直方向的會有一些區別。由于 GridLayoutManager 下,item 的數量不一定能夠剛好整除每排的數量。所以這邊的繪制區域是根據每個 item 來進行確定的。能被整除的或者當數量不足的時候最后一項不需要豎直的分割線。同時要注意補齊 centerTop(分割線繪制在中間區域的位置)。

//最右邊的一排不需要右邊的
 boolean isRight = spanIndex + spanSize == spanCount;
 if (!isRight) {
    //計算右邊的
    left = (int) (child.getRight() + centerLeft);
    right = left + leftRight;
    top = child.getTop();
    if (!isFirst) {
      top -= centerTop;
    }
    bottom = (int) (child.getBottom() + centerTop);
    mDivider.setBounds(left, top, right, bottom);
    mDivider.draw(c);
}

HORIZONTAL 下的情況可以進行類似的分析,代碼的調用方式跟LinearLayoutManager 下是一樣的。


VERTICAL

HORIZONTAL

4 最后

至此,RecyclerView 的 divide r效果已經基本實現了。當然,你可以在這基礎上進行修改,尤其是 GridLayoutManager 情況下比較復雜,可以根據實際的布局進行對應的修改,滿足自己的一些需求。歡迎大家一起相互交流。代碼已經上傳 https://github.com/hzl123456/SpacesItemDecoration
(ps:在實際的使用過程中,當對 RecyclerView 的 item 進行增加和刪除的操作是,會使 ItemDecoration 的分割區域計算錯誤。原因是在添加和刪除操作的時候,只會計算更新的部分區域的 OutRect,導致出現問題,這個時候我們只需要在添加和刪除操作之后調用 RecyclerView 的 invalidateItemDecorations() 方法就可以解決問題了)

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

推薦閱讀更多精彩內容