版權(quán)聲明:本文源自簡書tianma起趾,轉(zhuǎn)載請務(wù)必注明出處: http://www.reibang.com/p/bc6b5e80e00e
RecyclerView 并沒有 divider 屬性,但是我們可以通過 RecyclerView 的 addItemDecoration() 來添加分割線,該方法參數(shù)為 RecyclerView.ItemDecoration胞谈。
介紹
當(dāng) RecyclerView 添加 ItemDecoration 后歉铝,RecyclerView 在繪制每個 item 的時候,會去繪制 decorator谍婉,也就是會調(diào)用 ItemDecoration 的 onDraw() 和 onDrawOver() 方法更啄。
RecyclerView.ItemDecoration 是抽象類稚疹,主要提供三個方法:
- onDraw(Canvas c, RecyclerView parent, State state): 在繪制item(drawChild) 前調(diào)用
- onDrawOver(Canvas c, RecyclerView parent, State state): 在繪制item(drawChild) 后調(diào)用
- getItemOffsets(Rect outRect, View view, RecyclerView parent, State state):outRect設(shè)置 item 的偏移量,用于繪制 decorator(也就是divider)
關(guān)于 getItemOffsets 函數(shù):
RecyclerView 添加分割線锈死,實際上就是 RecyclerView 的 item 之間添加了用作分割線的View贫堰,自然而然后續(xù)的 item 就會有偏移量,所以用 getItemOffsets 中的 outRect 來保存 item 的偏移量待牵,從而便于繪制 decorator其屏。
實現(xiàn)
實際上在當(dāng)前版本的 RecyclerView (25.3.1)
中已經(jīng)有 ItemDecoration
關(guān)于分割線的默認(rèn)實現(xiàn)類 DividerItemDecoration
:
package android.support.v7.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.view.View;
import android.widget.LinearLayout;
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
// 如果不設(shè)置,則默認(rèn)的分割線為 android.R.attr.listDivider 指定的 drawable
private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };
private Drawable mDivider;
/**
* Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
private int mOrientation;
private final Rect mBounds = new Rect();
/**
* Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a
* {@link LinearLayoutManager}.
*
* @param context Current context, it will be used to access resources.
* @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}
/**
* Sets the orientation for this divider. This should be called if
* {@link RecyclerView.LayoutManager} changes orientation.
*
* @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
*/
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException(
"Invalid orientation. It should be either HORIZONTAL or VERTICAL");
}
mOrientation = orientation;
}
/**
* Sets the {@link Drawable} for this divider.
*
* @param drawable Drawable that should be used as a divider.
*/
public void setDrawable(@NonNull Drawable drawable) {
if (drawable == null) {
throw new IllegalArgumentException("Drawable cannot be null.");
}
mDivider = drawable;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
// 繪制 RecyclerView 為垂直布局時的分割線缨该,此時分割線為水平分割線
@SuppressLint("NewApi")
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
// 需要考慮clipToPadding的boolean值
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right,
parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
// 繪制 RecyclerView 為水平布局時的分割線偎行,此時分割線為垂直分割線
@SuppressLint("NewApi")
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
canvas.save();
final int top;
final int bottom;
// 需要考慮clipToPadding的boolean值
if (parent.getClipToPadding()) {
top = parent.getPaddingTop();
bottom = parent.getHeight() - parent.getPaddingBottom();
canvas.clipRect(parent.getPaddingLeft(), top,
parent.getWidth() - parent.getPaddingRight(), bottom);
} else {
top = 0;
bottom = parent.getHeight();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
final int right = mBounds.right + Math.round(ViewCompat.getTranslationX(child));
final int left = right - mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mOrientation == VERTICAL) { // 垂直方向的RecyclerView, item 的 bottom 偏移量 = 分割線高度
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else { // 水平方向的RecyclerView, item 的 right 偏移量 = 分割線寬度
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}
在代碼中添加:
recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), mLayoutManager.getOrientation()));
就有了分割線贰拿。 默認(rèn)的分割線效果是系統(tǒng)自帶的 listDivider
的效果蛤袒,我們也可以在主題配置文件中自定義全局的分割線,或者調(diào)用 setDivider
為每個 RecyclerView
設(shè)置單獨的分割線膨更。
網(wǎng)絡(luò)流行代碼存在的問題
目前好多博客中關(guān)于 DividerItemDecoration
中 drawVertical()
和 drawHorizontal()
方法與官方的方法其實是有出入的:
public void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
// 網(wǎng)上的方法最主要的問題是沒有考慮 clipToPadding 這個參數(shù)妙真,所以說這里缺少相應(yīng)代碼片段
final int childCount = parent.getChildCount();
// 下面這塊沒什么問題,和官方方案殊途同歸
// 官方的getDecoratedBoundsWithMargins實際上也是通過 LayoutParams 來獲取分割線邊界的
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
public void drawHorizontal(Canvas c, RecyclerView parent) {
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
注釋中說的比較明確了荚守,網(wǎng)上好多方法主要的問題在于 沒有考慮 clipToPadding 屬性值 珍德。 clipToPadding
表示控件的繪制區(qū)域是否在 padding 區(qū)域外面,其默認(rèn)值為 true
矗漾。比如锈候,垂直方向的 RecyclerView,當(dāng) clipToPadding=false
時敞贡,其初始繪制區(qū)域與 padding 值有關(guān)泵琳,但向上滑動時,RecylerView 的 item 會滑到 padding 區(qū)域里面誊役。
下面用示意圖來進(jìn)行解釋获列,RecyclerView 的 paddingTop = 40dp
, clipToPadding = false
, 下圖中白色區(qū)域為 paddingTop
區(qū)域:
小結(jié)
總的來說,目前添加分割線只需要使用 recyclerview-v7
包下的 DividerItemDecoration
類即可蛔垢,分割線可以通過 setDivider 來個性化指定击孩,也可以通過配置主題中的 android:listDivider
來全局指定。
參考
Android RecyclerView 使用完全解析 體驗藝術(shù)般的控件
RecyclerView系列之二:添加分隔線
android:clipToPadding和android:clipChildren