教你玩轉 Android RecyclerView:深刻解析 RecyclerView ItemDecoration類


前言

  • RecyclerViewAndroid開發中很是經常使用,若是能結合ItemDecoration類使用,那麼將大大提升RecyclerView的表現效果
  • 本文全面解析了ItemDecoration類,包括ItemDecoration類簡介、使用方法 & 實例講解,但願大家會喜歡。

ItemDecoration類屬於RecyclerView的高級用法,閱讀本文前請先學習RecyclerView的使用:Android開發:ListView、AdapterView、RecyclerView全面解析java


目錄

目錄


1. ItemDecoration類 簡介

1.1 定義

RecyclerView類的靜態內部類git

###1.2 做用 向 RecyclerView中的 ItemView 添加裝飾github

即繪製更多內容,豐富ItemViewUI效果bash


2. 具體使用

ItemDecoration類中僅有3個方法,具體以下:微信

public class TestDividerItemDecoration extends RecyclerView.ItemDecoration {

    // 方法1:getItemOffsets()
    // 做用:設置ItemView的內嵌偏移長度(inset)
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
       ...
  }

    // 方法2:onDraw()
    // 做用:在子視圖上設置繪製範圍,並繪製內容
    // 相似平時自定義View時寫onDraw()同樣
    // 繪製圖層在ItemView如下,因此若是繪製區域與ItemView區域相重疊,會被遮擋
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    ...
  }

    // 方法3:onDrawOver()
    // 做用:一樣是繪製內容,但與onDraw()的區別是:繪製在圖層的最上層
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
      ...
}

複製代碼

下面,我將詳細介紹這3個方法。ide


2.1 getItemOffsets()

2.1.1 做用

設置ItemView的內嵌偏移長度(inset)函數

  • 如圖,其實RecyclerView 中的 ItemView 外面會包裹着一個矩形(outRect
  • 內嵌偏移長度 是指:該矩形(outRect)與 ItemView的間隔

示意圖

  • 內嵌偏移長度分爲4個方向:上、下、左、右,並由outRect 中的 top、left、right、bottom參數 控制

top、left、right、bottom參數默認 = 0,即矩形和Item重疊,因此看起來矩形就消失了源碼分析

示意圖

2.1.2 具體使用

@Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
      // 參數說明:
        // 1. outRect:全爲 0 的 Rect(包括着Item)
        // 2. view:RecyclerView 中的 視圖Item
        // 3. parent:RecyclerView 自己
        // 4. state:狀態

      outRect.set(50, 0, 0,50);
      // 4個參數分別對應左(Left)、上(Top)、右(Right)、下(Bottom)
      // 上述語句表明:左&下偏移長度=50px,右 & 上 偏移長度 = 0
       ...
  }

複製代碼

示意圖

2.1.3 源碼分析

  • RecyclerView本質上是一個自定義ViewGroup,子視圖child = 每一個ItemView
  • 其經過 LayoutManager測量並佈局 ItemView
public void measureChild(View child, int widthUsed, int heightUsed) {

// 參數說明:
  // 1. child:要測量的子view(ItemView)
  // 2. widthUsed: 一個ItemView的全部ItemDecoration佔用的寬度(px)
  // 3. heightUsed:一個ItemView的全部ItemDecoration佔用的高度(px)

    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    // 累加當前ItemDecoration 4個屬性值->>分析1
    
    widthUsed += insets.left + insets.right;
    // 計算每一個ItemView的全部ItemDecoration的寬度
    heightUsed += insets.top + insets.bottom;
    // 計算每一個ItemView的全部ItemDecoration的高度

    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
            getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
            canScrollHorizontally());
    // 測量child view(ItemView)的寬度
    // 第三個參數設置 child view 的 padding,即ItemView的Padding
    // 而該參數把 insets 的值算進去,因此insets 值影響了每一個 ItemView 的 padding值

    // 高度同上
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
            getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
            canScrollVertically());
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}

// 分析完畢,請跳出
<-- 分析1:getItemDecorInsetsForChild()-->
Rect getItemDecorInsetsForChild(View child) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    insets.set(0, 0, 0, 0);
    for (int i = 0; i < decorCount; i++) {
        mTempRect.set(0, 0, 0, 0);

        // 獲取getItemOffsets() 中設置的值
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
        // 將getItemOffsets() 中設置的值添加到insets 變量中

        insets.left += mTempRect.left;
        insets.top += mTempRect.top;
        insets.right += mTempRect.right;
        insets.bottom += mTempRect.bottom;
    }
    // 最終返回
    return insets;
}

// insets介紹
  // 1. 做用:
    // a. 把每一個ItemView的全部 ItemDecoration 的 getItemOffsets 中設置的值累加起來,(每一個ItemView可添加多個ItemDecoration)
    // 即把每一個ItemDecoration的left, top, right, bottom 4個屬性分別累加
    // b. 記錄上述結果
    // c. inset就像padding和margin同樣,會影響view的尺寸和位置

  // 2. 使用場景:設置View的邊界大小,使得其大小>View的背景大小
  // 如 按鈕圖標(View的背景)較小,可是咱們但願按鈕有較大的點擊熱區(View的邊界大小)

// 返回到分析1進來的原處
複製代碼

總結

  • 結論:outRect4個屬性值影響着ItemView的Padding值
  • 具體過程:在RecyclerView進行子View寬高測量時(measureChild()),會將getItemOffsets()裏設置的 outRect4個屬性值(Top、Bottom、Left、Right)經過insert值累加 ,並最終添加到子ViewPadding屬性中

2.2 onDraw()

2.2.1 做用

經過 Canvas 對象繪製內容佈局

2.2.2 具體使用

  • 使用方法相似自定義View時的onDraw()

請看我寫的自定義View文章:自定義View Draw過程- 最易懂的自定義View原理系列(4)post

@Override
    public void onDraw(Canvas c, RecyclerView parent, 
                                  RecyclerView.State state) {
      ....
      // 使用相似自定義View時的 onDraw()
      
}
複製代碼

2.2.3 特別注意

**注意點1:ItemdecorationonDraw()繪製會先於ItemViewonDraw()繪製,因此若是在ItemdecorationonDraw()中繪製的內容在ItemView邊界內,就會被ItemView遮擋住。**以下圖:

此現象稱爲onDraw()OverDraw現象

示意圖

解決方案:配合前面的 getItemOffsets() 一塊兒使用在outRect矩形 與 ItemView的間隔區域 繪製內容

即:經過getItemOffsets() 設置與 Item 的間隔區域,從而得到與ItemView不重疊的繪製區域

示意圖

注意點2: getItemOffsets() 針對是每個 ItemView的,而 onDraw() 針對 RecyclerView 自己

解決方案:在 使用onDraw()繪製時,須要先遍歷RecyclerView 的全部ItemView分別獲取它們的位置信息,而後再繪製內容

  1. 此處遍歷的RecyclerViewItemView(即Child view),並非 Adapter 設置的每個 item,而是可見的 item
  2. 由於只有可見的Item 纔是RecyclerViewChild view
@Override
    public void onDraw(Canvas c, RecyclerView parent, 
                                  RecyclerView.State state) {

    // RecyclerView 的左邊界加上 paddingLeft距離 後的座標位置
    final int left = parent.getPaddingLeft();
    // RecyclerView 的右邊界減去 paddingRight 後的座標位置
    final int right = parent.getWidth() - parent.getPaddingRight();
    // 即左右邊界就是 RecyclerView 的 ItemView區域

    // 獲取RecyclerView的Child view的個數
    final int childCount = parent.getChildCount();

    // 設置佈局參數
    final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                .getLayoutParams();

    // 遍歷每一個RecyclerView的Child view
    // 分別獲取它們的位置信息,而後再繪製內容
    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        int index = parent.getChildAdapterPosition(view);
            // 第一個Item不須要繪製
            if ( index == 0 ) {
                continue;
            }
        // ItemView的下邊界:ItemView 的 bottom座標 + 距離RecyclerView底部距離 +translationY
        final int top = child.getBottom() + params.bottomMargin +
                Math.round(ViewCompat.getTranslationY(child));
        // 繪製分割線的下邊界 = ItemView的下邊界+分割線的高度
        final int bottom = top + mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);
    }
}

}
複製代碼

2.2.4 應用場景

在豐富 ItemView 的顯示效果,即在ItemView 的基礎上繪製內容

如分割線等等

2.2.5 實例講解

  • 實例說明:在ItemView設計一個高度爲 10 px 的紅色分割線
  • 思路
    1. 經過getItemOffsets()設置與 Item 的下間隔區域 = 10 px

設置好onDraw()可繪製的區域

  1. 經過onDraw()繪製一個高度 = 10px的矩形(填充顏色=紅色)

示意圖

  • 具體實現

步驟1:自定義ItemDecoration類

ItemDecoration.java

public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    private Paint mPaint;
    
    // 在構造函數裏進行繪製的初始化,如畫筆屬性設置等
    public DividerItemDecoration() {

        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        // 畫筆顏色設置爲紅色
    }
    
    // 重寫getItemOffsets()方法
    // 做用:設置矩形OutRect 與 Item 的間隔區域
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);


        int itemPosition = parent.getChildAdapterPosition(view);
        // 得到每一個Item的位置

        // 第1個Item不繪製分割線
        if (itemPosition != 0) {
            outRect.set(0, 0, 0, 10);
            // 設置間隔區域爲10px,即onDraw()可繪製的區域爲10px
        }
    }
    
    // 重寫onDraw()
    // 做用:在間隔區域裏繪製一個矩形,即分割線
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);

        // 獲取RecyclerView的Child view的個數
        int childCount = parent.getChildCount();

        // 遍歷每一個Item,分別獲取它們的位置信息,而後再繪製對應的分割線
        for ( int i = 0; i < childCount; i++ ) {
            // 獲取每一個Item的位置
            final View child = parent.getChildAt(i);
            int index = parent.getChildAdapterPosition(child);
            // 第1個Item不須要繪製
            if ( index == 0 ) {
                continue;
            }
            
            // 獲取佈局參數
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            // 設置矩形(分割線)的寬度爲10px
            final int mDivider = 10;

            // 根據子視圖的位置 & 間隔區域,設置矩形(分割線)的2個頂點座標(左上 & 右下)

            // 矩形左上頂點 = (ItemView的左邊界,ItemView的下邊界)
            // ItemView的左邊界 = RecyclerView 的左邊界 + paddingLeft距離 後的位置
            final int left = parent.getPaddingLeft();
            // ItemView的下邊界:ItemView 的 bottom座標 + 距離RecyclerView底部距離 +translationY
            final int top = child.getBottom() + params.bottomMargin +
                    Math.round(ViewCompat.getTranslationY(child));

            // 矩形右下頂點 = (ItemView的右邊界,矩形的下邊界)
            // ItemView的右邊界 = RecyclerView 的右邊界減去 paddingRight 後的座標位置
            final int right = parent.getWidth() - parent.getPaddingRight();
            // 繪製分割線的下邊界 = ItemView的下邊界+分割線的高度
            final int bottom = top + mDivider;


            // 經過Canvas繪製矩形(分割線)
            c.drawRect(left,top,right,bottom,mPaint);
        }
    }
}
複製代碼

步驟2:在設置RecyclerView時添加該分割線便可

Rv = (RecyclerView) findViewById(R.id.my_recycler_view);
        //使用線性佈局
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        Rv.setLayoutManager(layoutManager);
        Rv.setHasFixedSize(true);

        // 經過自定義分割線類 添加分割線
        Rv.addItemDecoration(new DividerItemDecoration());

        //爲ListView綁定適配器
        myAdapter = new MyAdapter(this,listItem);
        Rv.setAdapter(myAdapter);
        myAdapter.setOnItemClickListener(this);
複製代碼

2.2.6 結果展現

示意圖

2.2.7 源碼地址

Carson_Ho的Github地址:RecyclerView_ItemDecoration


2.3 onDrawOver()

2.3.1 做用

  • onDraw()相似,都是繪製內容
  • 但與onDraw()的區別是:ItemdecorationonDrawOver()繪製 是後於 ItemViewonDraw()繪製
  1. 即不須要考慮繪製內容被ItemView遮擋的問題,反而 ItemView會被onDrawOver()繪製的內容遮擋
  2. 繪製時機比較: Itemdecoration.onDraw()> ItemView.onDraw() > Itemdecoration.onDrawOver()

示意圖

2.3.2 具體使用

  • 使用方法相似自定義View時的onDraw()

請看我寫的自定義View文章:自定義View Draw過程- 最易懂的自定義View原理系列(4)

@Override
    public void onDrawOver(Canvas c, RecyclerView parent, 
                                  RecyclerView.State state) {
      ....
      // 使用相似自定義View時的 onDraw()
      
}
複製代碼

2.3.3 應用場景

RecyclerView / 特定的 ItemView 上繪製內容,如蒙層、重疊內容等等

2.3.4 實例講解

  • 實例說明:在 RecyclerView 上每一個 ItemView 上疊加一個角標

角度示意圖

  • 具體代碼實現

** 步驟1:自定義 ItemDecoration類**

public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    private Paint mPaint;
    private Bitmap mIcon;

    // 在構造函數裏進行繪製的初始化,如畫筆屬性設置等
    public DividerItemDecoration(Context context) {

        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        // 畫筆顏色設置爲紅色

        // 獲取圖片資源
        mIcon = BitmapFactory.decodeResource(context.getResources(), R.mipmap.logo);
    }

    // 重寫onDrawOver()
    // 將角度繪製到ItemView上
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);

        // 獲取Item的總數
        int childCount = parent.getChildCount();
        // 遍歷Item
        for ( int i = 0; i < childCount; i++ ) {
            // 獲取每一個Item的位置
            View view = parent.getChildAt(i);
            int index = parent.getChildAdapterPosition(view);

            // 設置繪製內容的座標(ItemView的左邊界,ItemView的上邊界)
            // ItemView的左邊界 = RecyclerView 的左邊界 = paddingLeft距離 後的位置
            final int left = parent.getWidth()/2;
            // ItemView的上邊界
            float top = view.getTop();

            // 第1個ItemView不繪製
            if ( index == 0 ) {
                continue;
            }
                // 經過Canvas繪製角標
                c.drawBitmap(mIcon,left,top,mPaint);
        }
    }

}
複製代碼

** 步驟2:在設置RecyclerView時添加便可 **

Rv = (RecyclerView) findViewById(R.id.my_recycler_view);
        //使用線性佈局
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        Rv.setLayoutManager(layoutManager);
        Rv.setHasFixedSize(true);

        //用自定義分割線類設置分割線
        Rv.addItemDecoration(new DividerItemDecoration());

        //爲ListView綁定適配器
        myAdapter = new MyAdapter(this,listItem);
        Rv.setAdapter(myAdapter);
        myAdapter.setOnItemClickListener(this);
複製代碼

2.3.5 結果展現

示意圖

2.3.6 源碼地址

Carson_Ho的Github地址:RecyclerView_ItemDecoration

有一個很是使用的自定義View是基於RecyclerView ItemDecoration類的,具體請看文章Android 自定義View實戰系列 :時間軸


3. 總結

  • 我用一張圖總結RecyclerView ItemDecoration類的使用

    示意圖

  • 下一篇文章我將對講解Android 的相關知識,有興趣能夠繼續關注Carson_Ho的安卓開發筆記


請點贊!由於你的鼓勵是我寫做的最大動力!


歡迎關注carson_ho的微信公衆號

示意圖

示意圖
相關文章
相關標籤/搜索