從來不跌倒不算光彩观堂,每次跌倒后能再站起來妖啥,才是最大的榮耀芝硬。
添加分割線
在上一篇中,我們講解了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文件夾中。
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對象芭商,讓它從藍色不透明到藍色全透明漸變派草。