天天看點

【筆記】ItemDecoration——網格布局GridItemDecoration

RecyclerView可以說是ListView和GridView的更新綜合版,在Android5.0推出,其特點如下:

1、支援不同方向、不同排版模式,實作多種資料展示形式(包含ListView、GridView、瀑布流);

2、裝載了ViewHolder的回收機制,無需我們考慮View的複用;

3、取消了OnItemClick點選事件,需自己實作;

4、可控制Item增删動畫;

5、可以設定Item的間隔樣式;

它的優點很多,但也給我們帶來了一定的不便,ItemDecoration用于實作Item之間的分割樣式。谷歌官方提供了DividerItemDecoration友善大家對橫向縱向清單進行分割線的添加,但是網格布局GridLayoutManager模式下的分割線布局依然需要自己實作,而且它的實作還是有一定複雜度,需要好好計算的。

以下實作有邊框和無邊框的網格布局GridItemDecoration:

1 getItemOffsets

設定每個Item的上下左右偏移,因為每行每列item個數與分割線條數都是不一樣的,是以在每兩個item之間都有分割線,但繪制的其實終止位置都需要計算。

1.1無邊框

左右方向上,每個item内分割線占的寬度 = ( item個數 - 1 ) * 分割線寬度 / item個數

每兩個item之間的分割線寬度是固定的,所有每個分割線寬度都被相鄰兩個item瓜分,其左右偏移計算如下:

left = 行中位置 * ( 分割線寬度 - 每個item内分割線占用的寬度 ) ;

right = 每個item内分割線占用的寬度 - left;

而上下分割線相對可以簡單點,上下相鄰的Item各占一半即可,第一行和最後一行不需要偏移,設定成0。

【筆記】ItemDecoration——網格布局GridItemDecoration

1.2 有邊框

對于有邊框的網狀分割線,左右方向上,每個item内分割線占的寬度 = ( item個數 + 1 ) * 分割線寬度 / item個數

第一列與最後一列都需要一條完整的分割線,中間部分分割線寬度則被相鄰的item瓜分,左右偏移計算如下:

left = ( 行中位置 + 1 ) * 分割線寬度 - 行中位置 * 每個item内分割線占用的寬度;

right = 每個item内分割線占用的寬度 - left;

上下偏移也是各占分割線的一半,第一行和最後一行需要有完整的分割線高度。

【筆記】ItemDecoration——網格布局GridItemDecoration

1.3 代碼化

@Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
//        super.getItemOffsets(outRect, view, parent, state);
        if (mVerticalDivider == null && mHorizontalDivider == null) {
            outRect.set(0, 0, 0, 0);
            return;
        }
        //清單中item個數
        int itemCount = parent.getAdapter().getItemCount();
        //清單中位置
        int position = parent.getChildAdapterPosition(view);
        //列數
        int spanCount = getSpanCount(parent);

        //左右偏移
        //行中位置
        int indexHorizontal = position % spanCount;
        //縱向分割線寬度
        int dividerWidth = mVerticalDivider.getIntrinsicWidth();
        //有邊界  itemWidth = ( 列數 + 1 ) * 分割線寬度 / 列數
        //無邊界  itemWidth = ( 列數 - 1 ) * 分割線寬度 / 列數
        //每個item内分割線占用的寬度,
        // 無邊框:每個item内分割線占的寬度 = ( item個數 - 1 ) * 分割線寬度 / item個數
        // 有邊框:每個item内分割線占的寬度 = ( item個數 + 1 ) * 分割線寬度 / item個數
        int itemDividerWidth = (spanCount + (hasBorder ? 1 : -1)) * dividerWidth / spanCount;
        int left;
        int right;
        if (hasBorder) {
            //有邊框
            // left = ( 行中位置 + 1 ) * 分割線寬度 - 行中位置 * 每個item内分割線占用的寬度
            left = (indexHorizontal + 1) * dividerWidth - indexHorizontal * itemDividerWidth;
            //right = 每個item内分割線占用的寬度 - left
            right = itemDividerWidth - left;
        } else {
            //無邊框
            //left = 行中位置 * ( 分割線寬度 - 每個item内分割線占用的寬度 )
            left = indexHorizontal * (dividerWidth - itemDividerWidth);
            //right = 每個item内分割線占用的寬度 - left
            right = itemDividerWidth - left;
        }

        //上下偏移
        //橫向分割線高度
        int dividerHeight = mHorizontalDivider.getIntrinsicHeight();
        int top;
        int bottom;
        if (hasBorder) {
            //有邊框,最上面偏移分割線高度,最下面偏移分割線高度,其他都上下各偏移分割線一半的高度
            top = isFirstRow(position, spanCount) ? dividerHeight : (dividerHeight / 2);
            bottom = isLastRow(position, itemCount, spanCount) ? dividerHeight : (dividerHeight / 2);
        } else {
            //無邊框,最上面高度偏移0,最下面高度偏移0,其他上下各偏移分割線一半高度
            top = isFirstRow(position, spanCount) ? 0 : (dividerHeight / 2);
            bottom = isLastRow(position, itemCount, spanCount) ? 0 : (dividerHeight / 2);
        }
        outRect.set(left, top, right, bottom);
    }
           

2 onDraw

繪制的時候,一般情況下繪制一個item的下和左分割線(之是以繪制左分割線是因為在item的layout_width=“wrap_content”時擷取到每個item的右邊位置就是item的右邊位置而不是平分後的右邊位置),隻有在第一行需要再次繪制上分割線,最後一列需要繪制最右側分割線。

2.1 繪制水準分割線

先擷取每個item的上下左右位置,左邊位置就是分割線的左邊,右邊就是左邊位置加上左邊位置+每個item的均分寬度:

left = mBounds.left;

right = left + itemWidth;

每個item下分割線的位置就是目前item下邊位置向上移動分割線的一半高度(之前設定偏移量的時候就設定了一半的高度):

top = mBounds.bottom - dividerHeight / 2;

bottom = top + dividerHeight;

注:第一行和最後一行需要根據有無分割線做特殊處理,是否繪制。

/**
     * 繪制水準分割線
     */
    private void drawHorizontal(Canvas canvas, RecyclerView parent) {
        canvas.save();
        //總item數
        int itemCount = parent.getAdapter().getItemCount();
        //列數
        int spanCount = getSpanCount(parent);
        //每個item寬度
        int itemWidth = (parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight()) / spanCount;
        //分割線高度
        int dividerHeight = mVerticalDivider.getIntrinsicHeight();
        int left;
        int right;
        int bottom;
        int top;
        int position;

        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = parent.getChildAt(i);
            position = parent.getChildAdapterPosition(childView);
            parent.getDecoratedBoundsWithMargins(childView, mBounds);
            //分割線左右坐标
            left = mBounds.left;
            right = left + itemWidth;
            //每個item下邊分割線上邊沿(getItemOffsets時每個item多空了一個分割線高度的一半)
            top = mBounds.bottom - dividerHeight / 2;
            if (isLastRow(position, itemCount, spanCount)) {
                //最後一行,有邊界需要完整分割線高度,沒邊界減掉
                top += hasBorder ? -dividerHeight / 2 : dividerHeight / 2;
            }
            if (position >= itemCount - spanCount && !hasBorder) {
                //最後幾個,且沒有邊框
                bottom = top;
            } else {
                bottom = top + dividerHeight;
            }
            mVerticalDivider.setBounds(left, top, right, bottom);
            mVerticalDivider.draw(canvas);

            if (isFirstRow(position, spanCount) && hasBorder) {
                //第一行且有邊界,需要最上面一條
                top = mBounds.top;
                bottom = top + dividerHeight;
                mVerticalDivider.setBounds(left, top, right, bottom);
                mVerticalDivider.draw(canvas);
            }
        }
        canvas.restore();
    }
           

2.2 繪制垂直分割線

垂直分割線稍微麻煩點,畢竟設定偏移量的時候也是通過計算得出的。

上下位置不用說就是每個item的上下位置:

top = mBounds.top;

bottom = mBounds.bottom;

這裡有些需要特殊處理,比如:

1、無邊框時,item不是整行數的時候,倒數第二行最後幾個下分割線不需要繪制;

2、有邊框時還需要繪制最後一列邊框,最後一個item又分為是否是滿格的最後一個;

左右位置可以和設定偏移時候的item對應計算,是每個item的左邊位置再向左移動:

if (hasBorder) {
        //有邊界
        left = mBounds.left - (itemDividerWidth - dividerWidth) * indexHorizontal;
    } else {
        left = mBounds.left - (dividerWidth - indexHorizontal * (dividerWidth - itemDividerWidth));
    }
           

右邊位置就是左邊位置+分割線寬度:

right = left + dividerWidth;

/**
     * 繪制垂直分割線
     */
    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        //總item數
        int itemCount = parent.getAdapter().getItemCount();
        //列數
        int spanCount = getSpanCount(parent);
        //每個item寬度
        int itemWidth = (parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight()) / spanCount;
        //分割線寬度
        int dividerWidth = mHorizontalDivider.getIntrinsicWidth();
        //每個item中分割線占的寬度
        int itemDividerWidth = (spanCount + (hasBorder ? 1 : -1)) * dividerWidth / spanCount;

        int top;
        int bottom;
        int right;
        int left;
        int position;

        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
            position = parent.getChildAdapterPosition(child);
            //分割線上下坐标
            top = mBounds.top;
            bottom = mBounds.bottom;
            if (!hasBorder) {
                //沒有邊界時
                if (position + spanCount == itemCount) {
                    //最後一個item上面那個
                    bottom += mVerticalDivider.getIntrinsicHeight() / 2;
                } else if ((itemCount % spanCount != 0) && (itemCount % spanCount < position % spanCount) && (position > itemCount - spanCount)) {
                    //item不是整行數時,倒數第二行最後幾個的下邊界線需要減去
                    bottom -= mVerticalDivider.getIntrinsicHeight() / 2;
                }
            }
            //行中位置
            int indexHorizontal = position % spanCount;
            if (hasBorder) {
                //有邊界
                left = mBounds.left - (itemDividerWidth - dividerWidth) * indexHorizontal;
            } else {
                left = mBounds.left - (dividerWidth - indexHorizontal * (dividerWidth - itemDividerWidth));
            }
            right = left + dividerWidth;
            //畫左邊縱向分割線
            mVerticalDivider.setBounds(left, top, right, bottom);
            mVerticalDivider.draw(canvas);
            if (hasBorder && isLastColumn(position, spanCount, itemCount)) {
                //最後一列
                if ((indexHorizontal + 1) % spanCount == 0) {
                    //每行滿格最後一個
                    left = parent.getWidth() - parent.getPaddingRight() - dividerWidth;
                } else {
                    //不滿格的最後一個
                    left = left + itemWidth - (itemDividerWidth - dividerWidth);
                }
                right = left + dividerWidth;
                mVerticalDivider.setBounds(left, top, right, bottom);
                mVerticalDivider.draw(canvas);
            }
        }
        canvas.restore();
    }
           

完整代碼