RecyclerView通过addItemDecoration()
方法添加item之间的分割线。Android并没有提供实现好的Divider,因此任何分割线样式都需要自己实现。
自定义间隔样式需要继承RecyclerView.ItemDecoration
类,该类是个抽象类,官方目前并没有提供默认的实现类,主要有三个方法。
- onDraw(Canvas c, RecyclerView parent, State state),在Item绘制之前被调用,该方法主要用于绘制间隔样式。
- onDrawOver(Canvas c, RecyclerView parent, State state),在Item绘制之前被调用,该方法主要用于绘制间隔样式。
- getItemOffsets(Rect outRect, View view, RecyclerView parent, State state),设置item的偏移量,偏移的部分用于填充间隔样式,即设置分割线的宽、高;在RecyclerView的
onMesure()
中会调用该方法。
onDraw()
和onDrawOver()
这两个方法都是用于绘制间隔样式,我们只需要复写其中一个方法即可。
Google在sample中给了一个参考的实现类:DividerItemDecoration,这里我们通过分析这个例子来看如何自定义Item Decoration。
自定义的间隔样式的实现步骤
- ① 通过读取系统主题中的 Android.R.attr.listDivider作为Item间的分割线,并且支持横向和纵向。
该分割线是系统默认的,你可以在theme.xml中找到该属性(android:listDivider)的使用情况。
如果要设置,则需要在value/styles.xml中设置:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:listDivider">@drawable/item_divider</item>
</style>
- ② 获取到listDivider以后,该属性的值是个Drawable,在getItemOffsets中,outRect去设置了绘制的范围。
- ③ onDraw中实现了真正的绘制。
① 获取listDivider
首先看构造函数,构造函数中获得系统属性android:listDivider
,该属性是一个Drawable对象。
private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
private Drawable mDivider;
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}
② getItemOffsets
接着来看getItemOffsets()
的实现:
public void getItemOffsets(Rect outRect, int position, RecyclerView parent) {
if (mOrientation == VERTICAL_LIST) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
这里只看mOrientation == VERTICAL_LIST
的情况,outRect是当前item四周的间距,类似margin属性,现在设置了该item下间距为mDivider.getIntrinsicHeight()
。
那么getItemOffsets()
是怎么被调用的呢?
RecyclerView继承了ViewGroup,并重写了measureChild()
,该方法在onMeasure()
中被调用,用来计算每个child的大小,计算每个child大小的时候就需要加上getItemOffsets()
设置的外间距:
public void measureChild(View child, int widthUsed, int heightUsed){
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);//调用getItemOffsets()获得Rect对象
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
//...
}
③ onDraw
这里我们只考虑mOrientation == VERTICAL_LIST
的情况,DividerItemDecoration的onDraw()
实际上调用了drawVertical()
:
public void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
// 画每个item的分割线
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 + Math.round(ViewCompat.getTranslationY(child));
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);/*规定好左上角和右下角*/
mDivider.draw(c);
}
}
那么onDraw()
是怎么被调用的呢?还有ItemDecoration还有一个方法onDrawOver()
,该方法也可以被重写,那么onDraw()
和onDrawOver()
之间有什么关系呢?
我们来看下面的代码:
class RecyclerView extends ViewGroup{
public void draw(Canvas c) {
super.draw(c); //调用View的draw(),该方法会先调用onDraw(),再调用dispatchDraw()绘制children
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
...
}
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
}
根据View的绘制流程,首先调用RecyclerView重写的draw()
方法,随后super.draw()
即调用View的draw()
,该方法会先调用onDraw()
(这个方法在RecyclerView重写了),再调用dispatchDraw()
绘制children。因此:ItemDecoration的onDraw()
在绘制Item之前调用,ItemDecoration的onDrawOver()
在绘制Item之后调用。
当然,如果只需要实现Item之间相隔一定距离,那么只需要为Item的布局设置margin即可,没必要自己实现ItemDecoration这么麻烦。