Android RecyclerView自定義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);
}


可以看到,這里只需要添加一句: mRecyclerView.addItemDecoration(mDivider);就可以給底部添加了一條橫線撮躁,那什么是ItemDecoration呢漱病?首先,我們肯定理解什么是Item把曼,在這個布局中杨帽,每個Item都單獨占一行。下面紅線框中就是一個Item:


什么是ItemDecoration

那ItemDecoration與Item是什么關系呢嗤军?對于英語來講注盈,Decoration是裝飾的意思,ItemDecoration就是Item的裝飾叙赚。在Item的四周老客,我們可以給它添加上自定義的裝飾,比如剛才的橫線震叮,就是在底部給它添加一個橫線的裝飾胧砰。同樣的,我們也可以在Item的上下左右各添加裝飾苇瓣,而且這些裝飾是允許我們自定義的尉间。系統(tǒng)只給我們提供了一個現(xiàn)成的Decoration類就是剛才使用的DividerItemDecoration,如果我們想實現(xiàn)其它的裝飾效果击罪,就需要自定義了哲嘲。下面這些漂亮的效果都可以使用自定義ItemDecoration來實現(xiàn):


getItemOffsets的意義

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

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);
    }
}

這三個函數(shù)我們會逐個講解媳禁,首先撤蚊,我們來看看getItemOffsets:
getItemOffsets的主要作用就是給item的四周加上邊距,實現(xiàn)的效果類似于margin损话,將item的四周撐開一些距離侦啸,在撐開這些距離后槽唾,我們就可以利用上面的onDraw函數(shù),在這個距離上進行繪圖了光涂。在了解了getItemOffsets的作用之后庞萍,我們來看看這個函數(shù)本身:
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)

  • Rect outRect:這個是最難理解的部分,outRect就是表示在item的上下左右所撐開的距離忘闻。這個值默認是0
  • View view:是指當前Item的View對象
  • RecyclerView parent: 是指RecyclerView 本身
  • RecyclerView.State state:通過State可以獲取當前RecyclerView的狀態(tài)钝计,也可以通過State在RecyclerView各組件間傳遞參數(shù),具體的文檔齐佳,大家可以參考:https://developer.android.com/reference/android/support/v7/widget/RecyclerView.State

getItemOffsets示例

我們知道私恬,想要實現(xiàn)分隔線,有一種方法是在Item的上方空出一像素的間隔炼吴,這樣就會漏出底線本鸣,看起來就是分割線了。所以我們回到剛才的示例硅蹦,去掉DividerItemDecoration荣德,改為自定義的LinearItemDecoration。
首先童芹,給整個Activity添加上一個紅色的背景色:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ff0000"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:id="@+id/linearRv"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

之后涮瞻,給每個Item添加上默認的背景色白色,這樣有白色的地方就不會透出背景色的紅色了假褪,而沒有白色的地方就會露出紅色:

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

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

</LinearLayout>

然后就是自定義LinearItemDecoration :

package com.example.myrecyclerview;

import android.graphics.Canvas;
import android.graphics.Rect;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

public class LinearItemDecoration extends RecyclerView.ItemDecoration {

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

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull 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:

 LinearItemDecoration linearItemDecoration = new LinearItemDecoration();
        mRv.addItemDecoration(linearItemDecoration);

可以看到每個Item的上方都出現(xiàn)了一條紅線艇抠。尤其從第一個Item可以看出來。同樣的久锥,如果我們改為底部1px,左側(cè)50px,右側(cè)100px:

@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;
    }

從第一個Item可以看出异剥,頂部是沒有紅線的瑟由,因為我們沒有設置outRect.top,所以它默認是0冤寿,因為outRect.right=100,而outRect.left=50歹苦,明顯可以看出右側(cè)的紅色寬度是左側(cè)的兩倍。

onDraw的用法

在理解了getItemOffsets的用法以后督怜,我們再來看看onDraw函數(shù):

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

onDraw函數(shù)有三個參數(shù)殴瘦,RecyclerView parent, RecyclerView.State state的意義與getItemOffsets的相同。而Canvas c: 是指通過getItemOffsets撐開的空白區(qū)域所對應的畫布号杠,通過這個canvas對象蚪腋,可以在getItemOffsets所撐出來的區(qū)域任意繪圖丰歌。那這個就厲害了,我們知道Canvas是有非常豐富的繪圖函數(shù)的屉凯,那我們先來個簡單的立帖,通過getItemOffsets將Item的左側(cè)撐出來200px的距離,然后在中間畫一個圓形:

package com.example.myrecyclerview;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.View;

import androidx.recyclerview.widget.RecyclerView;

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中晓勇,將左側(cè)撐出200px的距離,同樣底部留出1px的空間以顯示底部分割線灌旧。
然后在onDraw中绑咱,在每個Item的左側(cè)中間畫上半徑為20的綠色圓形。


需要注意的是,getItemOffsets是針對每個Item都會走一次枢泰,也就是說每個Item的outRect都可以不同描融,但是onDraw和onDrawOver并不是針對每個Item都會走一次,所以我們需要在onDraw和onDrawOver中繪圖時宗苍,一次性將所有Item的ItemDecoration繪制完成稼稿。從上面也可以看出,這里在onDraw函數(shù)中繪圖時讳窟,通過for循環(huán)對每一個item畫上一個綠色圓让歼。

拓展:獲取outRect的各個值
在上面的例子中,我們onDraw中使用到outRect的值時丽啡,都是直接使用的數(shù)字硬編碼谋右,比如在outRect是我們將左側(cè)撐開的距離設置為200,所以畫圓的中心點的X坐標就是100补箍,所以在onDraw函數(shù)中直接使用了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函數(shù)中的硬編碼终蒂,就可以用下面的動態(tài)獲取代碼來代替:

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);
            //動態(tà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);
        }
    }

到這,大家實現(xiàn)開頭講解的這個效果應該不難了:


該工程源碼地址:https://github.com/vipulasri/Timeline-View 遥诉,他是通過自定義View來實現(xiàn)的拇泣,大家也可以嘗試通過RecyclerView的ItemDecoration來實現(xiàn)出來。

onDraw的問題

如果我們在將上面畫圓的例子修改下矮锈,將畫圓改為繪制一個圖片:


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 = 150;
        outRect.bottom = 1;
    }
}


首先霉翔,因為圖片比較大,在LinearItemDecoration初始化的時候苞笨,通過options.inSampleSize參數(shù)將圖片縮放小為原大小的1/2;在getItemOffsets中债朵,將左側(cè)邊距設置為150px子眶,底部預留一像素顯示分割線。最后葱弟,在onDraw中壹店,將圖形在每個Item的左上角顯示出來。

如果我們把bitmap縮放去掉:

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

可以看到當圖片過大時芝加,在超出getItemOffsets函數(shù)所設定的outRect范圍的部分將是不可見的硅卢。這是因為在整個繪制流程中,是先調(diào)用ItemDecoration的onDraw函數(shù)藏杖,然后再調(diào)用Item的onDraw函數(shù)将塑,最后調(diào)用ItemDecoration的onDrawOver函數(shù)。所以在ItemDecoration的onDraw函數(shù)中繪制的內(nèi)容蝌麸,當超出邊界時点寥,會被Item所覆蓋。但是因為最后才調(diào)用ItemDecoration的OnDrawOver函數(shù)来吩,所以在onDrawOver中繪制的內(nèi)容就不受outRect邊界的限制敢辩,可以覆蓋Item的區(qū)域顯示。

onDrawOver

上面我們已經(jīng)提到弟疆,ItemDecoration與Item的繪制順序為:
Decoration 的 onDraw------>Item的 onDraw------>Decoration 的 onDrawOver泪勒,這三者是依次發(fā)生的灯谣。
所以兑徘,onDrawOver 是繪制在最上層的埋心,所以它的繪制位置并不受限制(當然,Decoration 的 onDraw 繪制范圍也不受限制柑司,只不過不可見迫肖,被Item所覆蓋),所以利用 onDrawOver 可以做很多事情攒驰,例如為RecyclerView 整體頂部繪制一個蒙層蟆湖、超出ItemDecoration的范圍繪制圖像。
比如玻粪,我們實現(xiàn)下面這樣的效果:


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


可見這個蒙層是一直顯示在頂部的。下面我們就來看看具體 實現(xiàn)吧剥纷。

1痹籍、當然是將勛章圖片(medal.png)加入res/mipmap文件夾中或者res/drawable文件夾中。

medal.png

2晦鞋、初始化
然后在LinearItemDecoration初始化時蹲缠,將圖片轉(zhuǎn)為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.medal);
    }
}

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);
}

創(chuàng)建一個LinearGradient對象芭商,讓它從藍色不透明到藍色全透明漸變派草。


項目地址 https://github.com/githubwwj/MyRecyclerView

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市铛楣,隨后出現(xiàn)的幾起案子近迁,更是在濱河造成了極大的恐慌,老刑警劉巖簸州,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鉴竭,死亡現(xiàn)場離奇詭異,居然都是意外死亡岸浑,警方通過查閱死者的電腦和手機搏存,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來助琐,“玉大人祭埂,你說我怎么就攤上這事”ィ” “怎么了蛆橡?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長掘譬。 經(jīng)常有香客問我泰演,道長,這世上最難降的妖魔是什么葱轩? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任睦焕,我火速辦了婚禮,結(jié)果婚禮上靴拱,老公的妹妹穿的比我還像新娘垃喊。我一直安慰自己,他們只是感情好袜炕,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布本谜。 她就那樣靜靜地躺著,像睡著了一般偎窘。 火紅的嫁衣襯著肌膚如雪乌助。 梳的紋絲不亂的頭發(fā)上溜在,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機與錄音他托,去河邊找鬼掖肋。 笑死,一個胖子當著我的面吹牛赏参,可吹牛的內(nèi)容都是我干的志笼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼登刺,長吁一口氣:“原來是場噩夢啊……” “哼籽腕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起纸俭,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤皇耗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后揍很,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體郎楼,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年窒悔,在試婚紗的時候發(fā)現(xiàn)自己被綠了呜袁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡简珠,死狀恐怖阶界,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情聋庵,我是刑警寧澤膘融,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站祭玉,受9級特大地震影響氧映,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜脱货,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一岛都、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧振峻,春花似錦臼疫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春塔逃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背料仗。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工湾盗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人立轧。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓格粪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親氛改。 傳聞我的和親對象是個殘疾皇子帐萎,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354