天天看點

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

從來不跌倒不算光彩,每次跌倒後能再站起來,才是最大的榮耀。

系列文章: Android自定義控件三部曲文章索引: http://blog.csdn.net/harvic880925/article/details/50995268

一、添加分割線

1.1 引入ItemDecoration

在上一篇中,我們講解了RecyclerView的基本使用方法,但有個問題:為什麼Item之間沒有分割線呢?其實,給RecyclerView添加分割線也非常簡單,隻需要添加上一句話:

DividerItemDecoration  mDivider = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
mRecyclerView.addItemDecoration(mDivider);
           

完整的代碼如下:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_linear);
    
    generateDatas();
    RecyclerView mRecyclerView = (RecyclerView) findViewById(R.id.linear_recycler_view);

    //線性布局
    LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
    linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
    mRecyclerView.setLayoutManager(linearLayoutManager);

    //初始化分隔線、添加分隔線
    DividerItemDecoration mDivider = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
    mRecyclerView.addItemDecoration(mDivider);

    RecyclerAdapter adapter = new RecyclerAdapter(this, mDatas);
    mRecyclerView.setAdapter(adapter);
}
           

這裡實作的效果如下圖所示:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

可以看到,這裡隻需要添加一句:

mRecyclerView.addItemDecoration(mDivider);

就可以給底部添加了一條橫線,那什麼是ItemDecoration呢?

首先,我們肯定了解什麼是Item,在這個布局中,每個Item都單獨占一行,在沒加ItemDecoration時,下面紅線框中就是一個Item:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

1.2 什麼是ItemDecoration

那ItemDecoration與Item是什麼關系呢?對于英語來講,Decoration是裝飾的意思,ItemDecoration就是Item的裝飾。在Item的四周,我們可以給它添加上自定義的裝飾,比如剛才的橫線,就是在底部給它添加一個橫線的裝飾。同樣的,我們也可以在Item的上下左右各添加裝飾,而且這些裝飾是允許我們自定義的。系統隻給我們提供了一個現成的Decoration類就是剛才使用的DividerItemDecoration,如果我們想實作其它的裝飾效果,就需要自定義了。下面這些漂亮的效果都可以使用自定義ItemDecoration來實作:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration
自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration
自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration
自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

2、 自定義ItemDecoration

2.1 getItemOffsets

2.1.1 getItemOffsets的意義

當我們要重寫ItemDecoration時,主要涉及到三個函數:

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

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

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

這三個函數我們會逐個講解,首先,我們來看看getItemOffsets:

getItemOffsets的主要作用就是給item的四周加上邊距,實作的效果類似于margin,将item的四周撐開一些距離,在撐開這些距離後,我們就可以利用上面的onDraw函數,在這個距離上進行繪圖了。在了解了getItemOffsets的作用之後,我們來看看這個函數本身:

getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)

  • Rect outRect:這個是最難了解的部分,outRect就是表示在item的上下左右所撐開的距離,後面詳細講解。
  • View view:是指目前Item的View對象
  • RecyclerView parent: 是指RecyclerView 本身
  • RecyclerView.State state:通過State可以擷取目前RecyclerView的狀态,也可以通過State在RecyclerView各元件間傳遞參數,具體的文檔,大家可以參考:https://developer.android.com/reference/android/support/v7/widget/RecyclerView.State

下面我們專門來看看

Rect outRect

這個參數,outRect 中的 top、left、right、bottom四個點,并不是普通意義的坐标點,而是指的在Item上、左、右、下各撐開的距離,這個值預設是0,示意圖如下所示:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

2.1.2 getItemOffsets示例

我們知道,想要實作分隔線,有一種方法是在Item的上方空出一像素的間隔,這樣就會漏出底線,看起來就是分割線了。是以我們回到剛才的示例,去掉DividerItemDecoration,改為自定義的LinearItemDecoration。

首先,給整個Activity添加上一個紅色的背景色:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ff0000"
    tools:context=".LinearActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/linear_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </android.support.v7.widget.RecyclerView>

</LinearLayout>
           

之後,給每個Item添加上預設的背景色白色,這樣有白色的地方就不會透出背景色的紅色了,而沒有白色的地方就會露出紅色:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="@android:color/white"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/item_tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:padding="10dp" />

</LinearLayout>
           

然後就是自定義LinearItemDecoration :

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

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

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.top=1;
    }
}
           

在這裡,我們将item上方面所撐開的距離寫死為1px;

最後,将LinearItemDecoration添加進RecyclerView:

public class LinearActivity extends AppCompatActivity {
    private ArrayList<String> mDatas = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_linear);

        generateDatas();
        RecyclerView mRecyclerView = (RecyclerView) findViewById(R.id.linear_recycler_view);
		…………
        //添加分隔線
        mRecyclerView.addItemDecoration(new LinearItemDecoration());

        RecyclerAdapter adapter = new RecyclerAdapter(this, mDatas);
        mRecyclerView.setAdapter(adapter);
    }
    //其它代碼,參考源碼
	…………
}
           

效果如下圖所示:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

可以看到每個Item的上方都出現了一條紅線。尤其從第一個Item可以看出來。

同樣的,如果我們改為底部1px,左側50px,右側100px:

public class LinearItemDecoration extends RecyclerView.ItemDecoration {
	…………
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.left=50;
        outRect.right=100;
        outRect.bottom=1;
    }
}
           

看下效果:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

從第一個Item可以看出,頂部是沒有紅線的,因為我們沒有設定outRect.top,是以它預設是0,因為outRect.right=100,而outRect.left=50,明顯可以看出右側的紅色寬度是左側的兩倍。

2.2 onDraw

2.2.1 onDraw的用法

在了解了getItemOffsets的用法以後,我們再來看看onDraw函數:

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

onDraw函數有三個參數,RecyclerView parent, RecyclerView.State state的意義與getItemOffsets的相同。而

  • Canvas c: 是指通過getItemOffsets撐開的空白區域所對應的畫布,通過這個canvas對象,可以在getItemOffsets所撐出來的區域任意繪圖。

那這個就厲害了,我們知道Canvas是有非常豐富的繪圖函數的,那我們先來個簡單的,通過getItemOffsets将Item的左側撐出來150的距離,然後在中間畫一個圓形:

public class LinearItemDecoration extends RecyclerView.ItemDecoration {
    private Paint mPaint;
    public LinearItemDecoration(){
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
    }
    @Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDraw(c, parent, state);
        int childCount = parent.getChildCount();

        for (int i=0;i<childCount;i++){
            View child = parent.getChildAt(i);
            int cx = 100;
            int cy = child.getTop()+child.getHeight()/2;
            c.drawCircle(cx,cy,20,mPaint);
        }
}

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

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.left=200;
        outRect.bottom=1;
    }
}
           

首先,在getItemOffsets中,将左側撐出200px的距離,同樣底部留出1px的空間以顯示底部分割線。

然後在onDraw中,在每個Item的左側中間畫上半徑為20的綠色圓形。

效果如下圖所示:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration
需要注意的是,getItemOffsets是針對每個Item都會走一次,也就是說每個Item的outRect都可以不同,但是onDraw和onDrawOver所整個ItemDecoration隻執行一次的,并不是針對Item的,是以我們需要在onDraw和onDrawOver中繪圖時,一次性将所有Item的ItemDecoration繪制完成。從上面也可以看出,這裡在onDraw函數中繪圖時,通過for循環對每一個item畫上一個綠色圓。

拓展:擷取outRect的各個值

在上面的例子中,我們onDraw中使用到outRect的值時,都是直接使用的數字寫死,比如在outRect是我們将左側撐開的距離設定為200,是以畫圓的中心點的X坐标就是100,是以在onDraw函數中直接使用了

int cx = 100;

很明顯,在實際工作中要嚴格避免類似的寫死,因為寫死會使代碼變得極其難以維護。那我們怎麼在代碼中擷取到getItemOffsets中所設定的各個item的outRect的值呢?

可以通過LayoutManager來擷取,方法如下:其中parent是指RecylerView本身,而child是指RecyclerView的Item的View對象

RecyclerView.LayoutManager manager = parent.getLayoutManager();
int left = manager.getLeftDecorationWidth(child);
int top = manager.getTopDecorationHeight(child);
int right = manager.getRightDecorationWidth(child);
int bottom = manager.getBottomDecorationHeight(child);
           

是以我們上面在onDraw函數中的寫死,就可以用下面的動态擷取代碼來代替:

public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDraw(c, parent, state);
        int childCount = parent.getChildCount();
        RecyclerView.LayoutManager manager = parent.getLayoutManager();
        for (int i=0;i<childCount;i++){
            View child = parent.getChildAt(i);
            //動态擷取outRect的left值
            int left = manager.getLeftDecorationWidth(child);
            int cx = left/2;
            int cy = child.getTop()+child.getHeight()/2;
            c.drawCircle(cx,cy,20,mPaint);
        }
}
           

到這,大家實作開頭講解的這個效果應該不難了:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

該工程源碼位址:https://github.com/vipulasri/Timeline-View ,他是通過自定義View來實作的,大家也可以嘗試通過RecyclerView的ItemDecoration來實作出來。

源碼在文章底部給出

2.2.2 onDraw的問題

如果我們在将上面畫圓的例子修改下,将畫圓改為繪制一個圖檔:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

代碼如下:

public class LinearItemDecoration extends RecyclerView.ItemDecoration {
    private Paint mPaint;
    private Bitmap mBmp;

    public LinearItemDecoration(Context context) {
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inSampleSize = 2;
        mBmp = BitmapFactory.decodeResource(context.getResources(),R.mipmap.icon,options);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            c.drawBitmap(mBmp,0,child.getTop(), mPaint);
        }
    }

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

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.left = 200;
        outRect.bottom = 1;
    }
}
           

首先,因為圖檔比較大,在LinearItemDecoration初始化的時候,通過options.inSampleSize參數将圖檔縮放小為原大小的1/2;

public LinearItemDecoration(Context context) {
	…………
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inSampleSize = 2;
    mBmp = BitmapFactory.decodeResource(context.getResources(),R.mipmap.icon,options);
}
           

同樣,在getItemOffsets中,将左側邊距設定為200,底部預留一像素顯示分割線。

最後,在onDraw中,将圖形在每個Item的左上角顯示出來。

效果如下圖所示:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

如果我們把bitmap縮放去掉:

public LinearItemDecoration(Context context) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        //options.inSampleSize = 2;
        mBmp = BitmapFactory.decodeResource(context.getResources(),R.mipmap.icon,options);
    }
           

效果圖如下:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

可以看到當圖檔過大時,在超出getItemOffsets函數所設定的outRect範圍的部分将是不可見的。這是因為在整個繪制流程中,是選調用ItemDecoration的onDraw函數,然後再調用Item的onDraw函數,最後調用ItemDecoration的onDrawOver函數。

是以在ItemDecoration的onDraw函數中繪制的内容,當超出邊界時,會被Item所覆寫。但是因為最後才調用ItemDecoration的OnDrawOver函數,是以在onDrawOver中繪制的内容就不受outRect邊界的限制,可以覆寫Item的區域顯示。

源碼在文章底部給出

2.3 onDrawOver

上面我們已經提到,ItemDecoration與Item的繪制順序為:decoration 的 onDraw->item的 onDraw->decoration 的 onDrawOver,這三者是依次發生的。

是以,onDrawOver 是繪制在最上層的,是以它的繪制位置并不受限制(當然,decoration 的 onDraw 繪制範圍也不受限制,隻不過不可見,被item所覆寫),是以利用 onDrawOver 可以做很多事情,例如為 RecyclerView 整體頂部繪制一個蒙層、超出itemDecoration的範圍繪制圖像。

比如,我們實作下面這樣的效果:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

在這個效果中,我們在最頂部繪制了一個漸變蒙版,而且每隔五個item繪制一個勳章。動圖效果是這樣的:

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

可見這個蒙層是一直顯示在頂部的。下面我們就來看看具體 實作吧。

1、添加圖檔

第一步,當然是将勳章圖檔(xunzhang.png)加入res/mipmap檔案夾中或者res/drawable檔案夾中。

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

2、初始化

然後在LinearItemDecoration初始化時,将圖檔轉為bitmap對象:

public class LinearItemDecoration extends RecyclerView.ItemDecoration {
    private Paint mPaint;
    private Bitmap mMedalBmp;

    public LinearItemDecoration(Context context) {
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        BitmapFactory.Options options = new BitmapFactory.Options();
        mMedalBmp = BitmapFactory.decodeResource(context.getResources(), R.mipmap.xunzhang);
    }
}
           

3、繪制勳章

在onDrawOver中将勳章每隔五個item繪制出來

public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDrawOver(c, parent, state);
    //畫勳章
    RecyclerView.LayoutManager manager = parent.getLayoutManager();
    int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);
        int index = parent.getChildAdapterPosition(child);
        int left = manager.getLeftDecorationWidth(child);
        if (index % 5 == 0) {
            c.drawBitmap(mMedalBmp, left - mMedalBmp.getWidth() / 2, child.getTop(), mPaint);
        }
    }
}
           

繪制勳章的時候,需要注意,我們需要将勳章的中間位置繪制item與decoration的交界處,是以它的X坐标就是

left - mMedalBmp.getWidth() / 2

4、繪制漸變蒙版

因為蒙版同樣是浮在item之上的,是以我們同樣是在onDrawOver中繪制,在繪制勳章之後,繪制蒙版:

public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDrawOver(c, parent, state);
    //畫勳章
    …………
    //畫蒙版
    View temp = parent.getChildAt(0);
    LinearGradient gradient = new LinearGradient(parent.getWidth() / 2, 0, parent.getWidth() / 2, temp.getHeight() * 3,
            0xff0000ff, 0x000000ff, Shader.TileMode.CLAMP);
    mPaint.setShader(gradient);
    c.drawRect(0, 0, parent.getWidth(), temp.getHeight() * 3, mPaint);
}
           

首先,建立一個LinearGradient對象,讓它從藍色不透明到藍色全透明漸變。然後繪制出來,有關LinearGradient的知識,請大家參考《自定義控件三部曲之繪圖篇(十九)——LinearGradient與閃動文字效果》這裡就不再詳細講解了。

源碼在文章底部給出

到這裡,有關ItemDecoration的知識就講完了,大家了解以後,利用它可以實作很多有趣的效果,下篇我們再來看看LayoutManager的知識。

如果我的文章對你有幫助的話,你應該也會喜歡我的公衆号

自定義控件三部曲視圖篇(五)——RecyclerView系列之二ItemDecoration

如果本文有幫到你,記得加關注哦

CSDN源碼現在不能零分下載下傳了,必須強制最低一分,我設定為了最低分,如果沒分的同學,可以從github上下載下傳。

源碼位址:https://download.csdn.net/download/harvic880925/10709567

github代碼位址:https://github.com/harvic/harvic_blg_share 位于RecylcerView(二)

轉載請标明出處,https://blog.csdn.net/harvic880925/article/details/82959754 謝謝