1. 程式人生 > >RecycleView分割線、分組、粘性佈局之ItemDecoration

RecycleView分割線、分組、粘性佈局之ItemDecoration

    我們在使用 ListView 的時候要設定分割線只要在xml檔案中,使用android:divider就可以了,但是在RecyclerView 卻沒有直接的設定方法。ListView中要實現分組效果、粘性佈局,絕大多數都會使用PinnedSectionListView、StickyListHeaders等開源框架來實現這個功能,那麼在RecycleView中該怎麼使用呢?接下來就通過RecycleView的ItemDecoration來實現這些功能。

ItemDecoration類主要包含三個方法:

  •     public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
  •     public void onDraw(Canvas c, RecyclerView parent, State state)
  •     public void onDrawOver(Canvas c, RecyclerView parent, State state)
    方法一:可以實現類似padding的效果。
    方法二:可以實現類似繪製背景的效果,內容在底部。

    方法三:可以繪製在內容的上面,覆蓋內容。

    需要注意的是onDraw在繪製ItemView之前繪製,onDrawOver會在繪製ItemView之後繪製,使用onDraw可以實現分割線效果,使用onDrawOver可以實現蒙層效果,二者可以混合使用,也可以實現一樣的效果,實際使用中應該選擇合適的。

一、RecycleView的下劃線

1、獲取下劃線的Drawable物件

    這裡使用Android自帶的android.R.attr.listDivider下劃線樣式,在構造方法裡面得到Drawable:

        final TypedArray a = context.obtainStyledAttributes(android.R.attr.listDivider);
        mDivider = a.getDrawable(0);
        a.recycle();
2、設定padding

    getItemOffsets方法中設定Margin:

       outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
       或者:
       outRect.bottom = mDivider.getIntrinsicHeight();
3、繪製
     //獲取Recycleview的left、right
    final int left = parent.getPaddingLeft();
    final int right = parent.getWidth() - parent.getPaddingRight();
    //獲取Recycleviewitem個數
    final int childCount = parent.getChildCount();
    //遍歷每一個item,給ItemDecoration繪製線條
    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                .getLayoutParams();
        final int top = child.getBottom() + params.bottomMargin;
        final int bottom = top + mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);

在Activity加入:

recycleview.addItemDecoration(new DividerDecoration(this));

執行效果:


完整程式碼:

public class DividerDecoration extends RecyclerView.ItemDecoration {
    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    private Drawable mDivider;

    public DividerDecoration(Context context) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
    }


    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
}

二、RecyclerView分組

    通過RecycleView的下劃線的設定方法,下一步擴充套件RecyclerView,使其顯示為分組的效果,原理也類似,繪製分組首先需要繪製一個分組的View,和繪製一個文字的View,也就是說View需要兩個,那麼就需要定義兩個畫筆進行繪製。同時分組也需要一個分組的依據,這裡根據以日期為例進行分組,同一日期的為一組。

1、定義分組的依據介面
    public interface DecorationCallback {
        String getData(int position);
    }
2、初始化繪製文字的畫筆和分組背景畫筆。

    這裡初始化畫筆也需要在構造方法裡進行:

    (1)View的畫筆初始化

    Paint paint = new Paint();
    paint.setColor(ContextCompat.getColor(context, R.color.bg_header));

    (2)文字畫筆的初始化

    Paint textPaint = new TextPaint();
    textPaint.setTypeface(Typeface.DEFAULT_BOLD);//普通字型
    textPaint.setFakeBoldText(false);//不加粗
    textPaint.setAntiAlias(true);//抗鋸齒
    textPaint.setTextSize(40);//文字大小
    textPaint.setColor(Color.BLACK);//背景顏色
    textPaint.setTextAlign(Paint.Align.LEFT);//繪製起始位置
3、設定分組的padding

    分組的padding用於顯示分組的head佈局,繪製背景顏色、繪製文字等需求,這裡需要明白那些部分需要設定padding,上面說過咱們是依據日期分組的,那個就要根據日期判斷,日期相同的為一組,不同的視為不同的分組。

    (1)分組依據

    如果是第一個則視為新的分組,記錄當前的日期,然後將後面的和上一個日期對比,一樣則為同一個分組,不一樣則為不同的分組。

    private boolean isHeader(int pos) {
        if (pos == 0) {
            return true;
        } else {
            String preData = callback.getData(pos - 1);
            String data = callback.getData(pos);
            return !preData.equals(data);
        }
    }

    (2)設定padding

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        //獲取繪製的item所在的位置
        int pos = parent.getChildAdapterPosition(view);
        String data = callback.getData(pos);
        if (TextUtils.isEmpty(data)) {
            return;
        }
        //不同組、和第一個才新增padding,其他的不處理
        if (pos == 0 || isHeader(pos)) {
            outRect.top = topHead;
        } else {
            outRect.top = 0;
        }
    }
3、繪製

    矩形的繪製比較簡單,這裡著重介紹下TextView的繪製。一般而言,繪製的起始位置是所畫圖形對應的矩形的左上角點。但在drawText中是非常例外的,y所代表的是基線的位置,只要x座標、基線位置、文字大小確定以後,文字的位置就確定的了,所以這邊需要處理基線位置,才能使文字垂直居中,基線的處理如下:

    Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
    float baseline = (rect.bottom + rect.top - fontMetrics.bottom - fontMetrics.top) / 2;

    繪製原理看程式碼備註:

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        //獲取繪製起始點
        int left = parent.getPaddingLeft();
        //獲取繪製終點
        int right = parent.getWidth() - parent.getPaddingRight();
        //獲取RecyclerView中item的個數
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            //獲取item的View
            View view = parent.getChildAt(i);
            //獲取所在位置
            int position = parent.getChildAdapterPosition(view);
            //獲取回撥日期
            String textLine = callback.getData(position);
            if (TextUtils.isEmpty(textLine)) {
                return;
            }
            //繪製分組日期
            if (position == 0 || isHeader(position)) {
                float top = view.getTop() - topHead;
                float bottom = view.getTop();
                //繪製矩形
                Rect rect = new Rect(left, (int) top, right, (int) bottom);
                c.drawRect(rect, paint);
                //繪製文字基線,文字的的繪製是從繪製的矩形底部開始的
                Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
                float baseline = (rect.bottom + rect.top - fontMetrics.bottom - fontMetrics.top) / 2;
                textPaint.setTextAlign(Paint.Align.CENTER);//文字居中
                //繪製文字
                c.drawText(textLine, rect.centerX(), baseline, textPaint);
            }
        }
    }

    這樣這個分組就完成了,在Activity中進行使用,注意ItemDecoration是可以疊加的:

    recycleview.addItemDecoration(new DividerDecoration(this));
    recycleview.addItemDecoration(new SectionDecoration(this, new SectionDecoration.DecorationCallback() {
        @Override
        public String getData(int position) {
            return mDataList.get(position).data;
        }
    }));

效果如下:


    原始碼地址:https://github.com/yoonerloop/StickyRecycleView 點選開啟連結

三、RecyclerView粘性佈局

    粘性頭佈局要求頭常駐佈局最上面,和RecycleView滑動與不滑動沒有關係,並且隨著RecycleView的滑動header的資料依據特定的分組變化,在這種情況下顯然onDrawOver更加合適,因此需要重寫onDrawOver方法,在這個方法裡面處理,而不是在Draw方法裡面進行處理。構造方法與getItemOffsets方法保持不變,和上面分組的一致。onDrawOver方法重寫的如下:

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        //獲取當前可見的item的數量,不包括分組項,注意區分下面的
        int childCount = parent.getChildCount();
        //獲取所有的的item個數,原始碼中不建議使用Adapter中獲取
        int itemCount = state.getItemCount();
        //考慮padding,得到繪製的x軸起始點和終點的座標
        int left = parent.getLeft() + parent.getPaddingLeft();
        int right = parent.getRight() - parent.getPaddingRight();
        //獲取上一個和當前的日期
        String preDate;
        String currentDate = null;
        //注意:這裡不能使用itemCount
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);
            String textLine = callback.getData(position);
            //前一個Date作為preDate
            preDate = currentDate;
            //獲取當前的currentDate
            currentDate = callback.getData(position);
            //日期相同,跳出本次迴圈
            if (TextUtils.isEmpty(currentDate) || TextUtils.equals(currentDate, preDate)) {
                continue;
            }
            if (TextUtils.isEmpty(textLine)) {
                continue;
            }
            int viewBottom = view.getBottom();
            float textY = Math.max(topHead, view.getTop());
            //下一個和當前不一樣,item小於header的高度時候移動當前的header
            if (position + 1 < itemCount) {
                String nextData = callback.getData(position + 1);
                if (!currentDate.equals(nextData) && viewBottom < textY) {
                    textY = viewBottom;
                }
            }
            //不斷的觸發Canvas繪製,生成動態效果
            Rect rect = new Rect(left, (int) textY - topHead, right, (int) textY);
            c.drawRect(rect, paint);
            //繪製文字基線,文字的的繪製是從繪製的矩形底部開始的
            Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
            float baseline = (rect.bottom + rect.top - fontMetrics.bottom - fontMetrics.top) / 2;
            textPaint.setTextAlign(Paint.Align.CENTER);//文字居中
            //繪製文字
            c.drawText(textLine, rect.centerX(), baseline, textPaint);
        }
    }

    原理總結:

    浮在itemView上層的header被頂上去和被拉下來的效果,只需要當每組最後一個Item的bottom小於header的height時,讓header跟隨這個item就行,換句話說就是此時讓header的bottom等於即將消失的Item的bottom。這裡的核心程式碼如下:

    int viewBottom = view.getBottom();
    float textY = Math.max(topHead, view.getTop());
    //下一個和當前不一樣,item小於header的高度時候移動當前的header
    if (position + 1 < itemCount) {
        String nextData = callback.getData(position + 1);
        if (!currentDate.equals(nextData) && viewBottom < textY) {
            textY = viewBottom;
        }
    }
    //不斷的觸發Canvas繪製,生成動態效果
    Rect rect = new Rect(left, (int) textY - topHead, right, (int) textY);

    1、viewBottom:即每個item的bottom距離螢幕的最上邊的距離。

    2、textY:在設定的header的高度與每個item的top距離螢幕的最上邊的距離之間取最大的。如果header<top會導致存在間隙;如果header>top,會存在下拉延遲,所以一般情況下topHead和view.getTop()相等。

    3、viewBottom < textY:滑動的viewBottom的距離小於header的距離,才使得header的y座標發生變化,使得header的y座標動態的等於viewBottom,這樣就能實現上推效果。


原始碼地址:https://github.com/yoonerloop/StickyRecycleView 記得start點贊哦吐舌頭點選開啟連結