譯文的GitHub地址:RecyclerView之ItemDecoration由淺入深
譯者注:RecyclerView第一篇,希望后面堅持下來
RecyclerView沒有像之前ListView提供divider屬性,而是提供了方法
recyclerView.addItemDecoration()
其中ItemDecoration需要我們自己去定制重寫,一開始可能有人會覺得麻煩不好用,最后你會發(fā)現(xiàn)這種可插拔設計不僅好用,而且功能強大。
ItemDecoration類主要是三個方法:
public void onDraw(Canvas c, RecyclerView parent, State state)
public void onDrawOver(Canvas c, RecyclerView parent, State state)
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
官方源碼雖然都寫的很清楚,但還不少小伙伴不知道怎么理解,怎么用或用哪個方法,下面我畫個簡單的圖來幫你們理解一下。

圖畫的丑請見諒,首先我們假設綠色區(qū)域代表的是我們的內容,紅色區(qū)域代表我們自己繪制的裝飾,可以看到:
圖1:代表了getItemOffsets(),可以實現(xiàn)類似padding的效果
圖2:代表了onDraw(),可以實現(xiàn)類似繪制背景的效果,內容在上面
圖3:代表了onDrawOver(),可以繪制在內容的上面,覆蓋內容
注意上面是我個人從應用角度的看法,事實上實現(xiàn)上面的效果可能三個方法每個方法都可以實現(xiàn)。只不過這種方法更好理解。
下面是我們沒有添加任何ItemDecoration的界面

主頁布局界面很簡單,背景設成灰色
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
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="@color/gray">//灰色背景
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"/>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
/>
</android.support.design.widget.CoordinatorLayout>
ok 接下來,讓我們來實現(xiàn)實際開發(fā)中常遇到的場景。
padding
從前面的圖可以看到實現(xiàn)這個效果,需要重寫getItemOffsets方法。
public class SimplePaddingDecoration extends RecyclerView.ItemDecoration {
private int dividerHeight;
public SimplePaddingDecoration(Context context) {
dividerHeight = context.getResources().getDimensionPixelSize(R.dimen.divider_height);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.bottom = dividerHeight;//類似加了一個bottom padding
}
}
沒錯,就這么2行代碼,然后添加到RecyclerView
recyclerView.addItemDecoration(new SimplePaddingDecoration(this));
實現(xiàn)效果:

分割線
分割線在app中是經常用到的,用ItemDecoration怎么實現(xiàn)呢,其實上面padding改成1dp就實現(xiàn)了分割線的效果,但是分割線的顏色只能是背景灰色,所以不能用這種方法。
要實現(xiàn)分割線效果需要 getItemOffsets()和 onDraw()2個方法,首先用 getItemOffsets給item下方空出一定高度的空間(例子中是1dp),然后用onDraw繪制這個空間
public class SimpleDividerDecoration extends RecyclerView.ItemDecoration {
private int dividerHeight;
private Paint dividerPaint;
public SimpleDividerDecoration(Context context) {
dividerPaint = new Paint();
dividerPaint.setColor(context.getResources().getColor(R.color.colorAccent));
dividerHeight = context.getResources().getDimensionPixelSize(R.dimen.divider_height);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.bottom = dividerHeight;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
int childCount = parent.getChildCount();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
for (int i = 0; i < childCount - 1; i++) {
View view = parent.getChildAt(i);
float top = view.getBottom();
float bottom = view.getBottom() + dividerHeight;
c.drawRect(left, top, right, bottom, dividerPaint);
}
}
}
實現(xiàn)效果:

標簽
現(xiàn)在很多電商app會給商品加上一個標簽,比如“推薦”,“熱賣”,“秒殺”等等,可以看到這些標簽都是覆蓋在內容之上的,這就可以用onDrawOver()來實現(xiàn),我們這里簡單實現(xiàn)一個有趣的標簽
public class LeftAndRightTagDecoration extends RecyclerView.ItemDecoration {
private int tagWidth;
private Paint leftPaint;
private Paint rightPaint;
public LeftAndRightTagDecoration(Context context) {
leftPaint = new Paint();
leftPaint.setColor(context.getResources().getColor(R.color.colorAccent));
rightPaint = new Paint();
rightPaint.setColor(context.getResources().getColor(R.color.colorPrimary));
tagWidth = context.getResources().getDimensionPixelSize(R.dimen.tag_width);
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
int pos = parent.getChildAdapterPosition(child);
boolean isLeft = pos % 2 == 0;
if (isLeft) {
float left = child.getLeft();
float right = left + tagWidth;
float top = child.getTop();
float bottom = child.getBottom();
c.drawRect(left, top, right, bottom, leftPaint);
} else {
float right = child.getRight();
float left = right - tagWidth;
float top = child.getTop();
float bottom = child.getBottom();
c.drawRect(left, top, right, bottom, rightPaint);
}
}
}
}
實現(xiàn)效果:

組合
不要忘記的是ItemDecoration是可以疊加的
recyclerView.addItemDecoration(new LeftAndRightTagDecoration(this));
recyclerView.addItemDecoration(new SimpleDividerDecoration(this));
我們把上面2個ItemDecoration同時添加到RecyclerView看下什么效果

是不是有種狂拽炫酷吊炸天的趕腳。。。
三個方法都用了一遍,你以為這就結束了?呵呵 并沒有
section
這個是什么呢,先看下我們實現(xiàn)的效果

一看這個就很熟悉吧,手機上面的通訊錄聯(lián)系人,知乎日報都是這樣效果,可以叫分組,也可以叫section分塊 先不管它叫什么。
這個怎么實現(xiàn)呢? 其實和實現(xiàn)分割線是一樣的道理 ,只是不是所有的item都需要分割線,只有同組的第一個需要。
我們首先定義一個接口給activity進行回調用來進行數(shù)據(jù)分組和獲取首字母
public interface DecorationCallback {
long getGroupId(int position);
String getGroupFirstLine(int position);
}
然后再來看我們的ItemDecoration
public class SectionDecoration extends RecyclerView.ItemDecoration {
private static final String TAG = "SectionDecoration";
private DecorationCallback callback;
private TextPaint textPaint;
private Paint paint;
private int topGap;
private Paint.FontMetrics fontMetrics;
public SectionDecoration(Context context, DecorationCallback decorationCallback) {
Resources res = context.getResources();
this.callback = decorationCallback;
paint = new Paint();
paint.setColor(res.getColor(R.color.colorAccent));
textPaint = new TextPaint();
textPaint.setTypeface(Typeface.DEFAULT_BOLD);
textPaint.setAntiAlias(true);
textPaint.setTextSize(80);
textPaint.setColor(Color.BLACK);
textPaint.getFontMetrics(fontMetrics);
textPaint.setTextAlign(Paint.Align.LEFT);
fontMetrics = new Paint.FontMetrics();
topGap = res.getDimensionPixelSize(R.dimen.sectioned_top);//32dp
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int pos = parent.getChildAdapterPosition(view);
Log.i(TAG, "getItemOffsets:" + pos);
long groupId = callback.getGroupId(pos);
if (groupId < 0) return;
if (pos == 0 || isFirstInGroup(pos)) {//同組的第一個才添加padding
outRect.top = topGap;
} else {
outRect.top = 0;
}
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View view = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(view);
long groupId = callback.getGroupId(position);
if (groupId < 0) return;
String textLine = callback.getGroupFirstLine(position).toUpperCase();
if (position == 0 || isFirstInGroup(position)) {
float top = view.getTop() - topGap;
float bottom = view.getTop();
c.drawRect(left, top, right, bottom, paint);//繪制紅色矩形
c.drawText(textLine, left, bottom, textPaint);//繪制文本
}
}
}
private boolean isFirstInGroup(int pos) {
if (pos == 0) {
return true;
} else {
long prevGroupId = callback.getGroupId(pos - 1);
long groupId = callback.getGroupId(pos);
return prevGroupId != groupId;
}
}
public interface DecorationCallback {
long getGroupId(int position);
String getGroupFirstLine(int position);
}
}
可以看到和divider實現(xiàn)一樣,都是重寫getItemOffsets()和onDraw()2個方法,不同的是根據(jù)數(shù)據(jù)做了處理。
在Activity中使用
recyclerView.addItemDecoration(new SectionDecoration(this, new SectionDecoration.DecorationCallback() {
@Override
public long getGroupId(int position) {
return Character.toUpperCase(dataList.get(position).getName().charAt(0));
}
@Override
public String getGroupFirstLine(int position) {
return dataList.get(position).getName().substring(0, 1).toUpperCase();
}
}));
干凈舒服,不少github類似的庫都是去adapter進行處理 侵入性太強 或許ItemDecoration是個更好的選擇,可插拔,可替換。
到這里細心的人就會發(fā)現(xiàn)了,header不會動啊,我手機上的通訊錄可是會隨的滑動而變動呢,這個可以實現(xiàn)么?
StickyHeader
這個東西怎么叫我也不知道啊 粘性頭部?英文也有叫 pinned section 取名字真是個麻煩事。
先看下我們簡單實現(xiàn)的效果

首先一看到圖,我們就應該想到header不動肯定是要繪制item內容之上的,需要重寫onDrawOver()方法,其他地方和section實現(xiàn)一樣。
public class PinnedSectionDecoration extends RecyclerView.ItemDecoration {
private static final String TAG = "PinnedSectionDecoration";
private DecorationCallback callback;
private TextPaint textPaint;
private Paint paint;
private int topGap;
private Paint.FontMetrics fontMetrics;
public PinnedSectionDecoration(Context context, DecorationCallback decorationCallback) {
Resources res = context.getResources();
this.callback = decorationCallback;
paint = new Paint();
paint.setColor(res.getColor(R.color.colorAccent));
textPaint = new TextPaint();
textPaint.setTypeface(Typeface.DEFAULT_BOLD);
textPaint.setAntiAlias(true);
textPaint.setTextSize(80);
textPaint.setColor(Color.BLACK);
textPaint.getFontMetrics(fontMetrics);
textPaint.setTextAlign(Paint.Align.LEFT);
fontMetrics = new Paint.FontMetrics();
topGap = res.getDimensionPixelSize(R.dimen.sectioned_top);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int pos = parent.getChildAdapterPosition(view);
long groupId = callback.getGroupId(pos);
if (groupId < 0) return;
if (pos == 0 || isFirstInGroup(pos)) {
outRect.top = topGap;
} else {
outRect.top = 0;
}
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int itemCount = state.getItemCount();
int childCount = parent.getChildCount();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
float lineHeight = textPaint.getTextSize() + fontMetrics.descent;
long preGroupId, groupId = -1;
for (int i = 0; i < childCount; i++) {
View view = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(view);
preGroupId = groupId;
groupId = callback.getGroupId(position);
if (groupId < 0 || groupId == preGroupId) continue;
String textLine = callback.getGroupFirstLine(position).toUpperCase();
if (TextUtils.isEmpty(textLine)) continue;
int viewBottom = view.getBottom();
float textY = Math.max(topGap, view.getTop());
if (position + 1 < itemCount) { //下一個和當前不一樣移動當前
long nextGroupId = callback.getGroupId(position + 1);
if (nextGroupId != groupId && viewBottom < textY ) {//組內最后一個view進入了header
textY = viewBottom;
}
}
c.drawRect(left, textY - topGap, right, textY, paint);
c.drawText(textLine, left, textY, textPaint);
}
}
}
好了,現(xiàn)在發(fā)現(xiàn)ItemDecoration有多強大了吧! 當然還有更多就需要你自己去發(fā)現(xiàn)了。