RecyclerView 知識梳理(4) ItemDecoration

澤毛發表於2017-12-21

一、概述

通過ItemDecoration,可以給RecyclerView或者RecyclerView中的每個Item新增額外的裝飾效果,最常用的就是用來為Item之間新增分割線,今天,我們就來一起學習有關的知識:

  • API
  • DividerItemDecoration解析
  • 自定義ItemDecoration

二、API介紹

當我們實現自己的ItemDecoration時,需要繼承於ItemDecoration,並根據需要實現以下三個方法:

2.1 public void onDraw(Canvas c, RecyclerView parent, State state)

  • canvasRecyclerViewcanvas
  • parentRecyclerView例項
  • StateRecyclerView當前的狀態,值包括START/LAYOUT/ANIMATION

所有在這個方法中的繪製操作,將會在itemViews被繪製之前執行,因此,它會顯示在itemView之下。

2.2 public void onDrawOver(Canvas c, RecyclerView parent, State state)

2.1方法類似,區別在於它繪製在itemViews之上。

2.3 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)

通過outRect,可以設定item之間的間隔,間隔區域的大小就是outRect所指定的範圍,view就是對應位置的itemView,其它的引數解釋和上面相同。

三、DividerItemDecoration解析

3.1 使用方法

上面我們解釋了需要重寫的方法以及其中引數的含義,下面,我們通過官方自帶的DividerItemDecoration,來進一步加深對這些方法的認識。 DividerItemDecoration是為LinearLayoutManager提供的分割線,在建立它的時候,需要指定ORIENTATION,這個方向應當和LinearLayoutManager的方向相同。

    private void init() {
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_content);
        mTitles = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            mTitles.add(String.valueOf(i));
        }
        BaseAdapter baseAdapter = new BaseAdapter(mTitles);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
        recyclerView.setAdapter(baseAdapter);
    }
複製程式碼

最終展示的效果為:

RecyclerView 知識梳理(4)   ItemDecoration

3.2 原始碼解析

3.2.1 繪製

DividerItemDecoration重寫了基類當中的onDraw方法,也就是說這個分割線是在itemView之前繪製的:

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getLayoutManager() == null) {
            return;
        }
        if (mOrientation == VERTICAL) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }
複製程式碼

我們先看縱向排列的RecyclerView分割線:

    @SuppressLint("NewApi")
    private void drawVertical(Canvas canvas, RecyclerView parent) {
        //首先儲存畫布
        canvas.save();
        final int left;
        final int right;
        //確定左右邊界的範圍,如果RecyclerView不允許子View繪製在Padding內,那麼這個範圍為去掉Padding後的範圍
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right,
                    parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            //獲得itemView的範圍,這個範圍包括了margin和offset,它們被儲存在mBounds當中
            parent.getDecoratedBoundsWithMargins(child, mBounds);
            //需要考慮translationY和translationY
            final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
            //由於是垂直排列的,因此上邊界等於下邊界減去分割線的高度.
            final int top = bottom - mDivider.getIntrinsicHeight();
            //設定divider和範圍
            mDivider.setBounds(left, top, right, bottom);
            //繪製.
            mDivider.draw(canvas);
        }
        //回覆畫布.
        canvas.restore();
    }
複製程式碼

整個過程分為三步:

  • 確定子ViewRecyclerView中的繪製範圍
  • 確定每個子View的範圍
  • 確定mDivider的繪製範圍

下圖就是最終計算的結果:

RecyclerView 知識梳理(4)   ItemDecoration
橫向排列的RecyclerView列表和上面的原理是相同的,區別就在於計算mDivider.setBounds的計算:

//....
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
final int right = mBounds.right + Math.round(ViewCompat.getTranslationX(child));
final int left = right - mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
//..
複製程式碼

3.2.2 邊界處理

從上面的分析可以知道,如果將divider直接繪製在itemView的範圍內,那麼由於我們是先繪製divider,再繪製itemView的內容的,那麼它就會被覆蓋,因此,通過重寫getItemOffsets,通過其中的outRect來指定留出的空隙:

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        if (mOrientation == VERTICAL) {
            //如果是縱向排列,那麼要在itemView的下方留出一個下邊界
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            //如果是橫向排列,那麼要在itemView的右方留出一個右邊界
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
複製程式碼

四、自定義ItemDecoration

下面,我們參考上面的寫法,寫一個簡單的GridLayoutManager的分割線:

public class GridDividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[] { android.R.attr.listDivider };
    private Drawable mDivider;
    private final Rect mBounds = new Rect();

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

    public void setDrawable(@NonNull Drawable drawable) {
        mDivider = drawable;
    }

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

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

    private void drawDivider(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(view, mBounds);
            mDivider.setBounds(mBounds.right - mDivider.getIntrinsicWidth(), mBounds.top, mBounds.right, mBounds.bottom);
            mDivider.draw(canvas);
            mDivider.setBounds(mBounds.left, mBounds.bottom - mDivider.getIntrinsicHeight(), mBounds.right , mBounds.bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
   }
}
複製程式碼

最終的效果為:

RecyclerView 知識梳理(4)   ItemDecoration
這裡我們為了演示方便,沒有考慮最後一列或者最後一行沒有分割線的情況,這篇文章寫的比較好:Android RecyclerView 使用完全解析 體驗藝術般的控制元件

五、總結

ItemDecoration的使用並不難,大多數情況下就只需要重寫onDrawonDrawOver中的一個;如果需要在Item之間新增間隔,那麼要重寫getItemOffsets並理解outRect的含義,假如不需要新增間隔,那麼不需要重寫該方法。


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章