版权声明:本文为博主原创文章,未经博主允许不得转载。https://blog.csdn.net/sinat_25074703/article/details/83216019
在实际的项目开发过程中,为了更好的用户体验,分割线是必不可少的,甚至有异想天开的产品会脑补出千姿百态的分割效果。这里主要通过简单的直线分割来阐述RecyclerView#ItemDecoration的风采。
Google官方对RecyclerView#ItemDecoration的定义如下:
/**
* An ItemDecoration allows the application to add a special drawing and layout offset
* to specific item views from the adapter’s data set. This can be useful for drawing dividers
* between items, highlights, visual grouping boundaries and more.
*
*All ItemDecorations are drawn in the order they were added, before the item
* views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
* and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
* RecyclerView.State)}.
*/
译文:
ItemDecoration允许(既然是允许,不添加也不犯法)应用添加一种特殊的绘制和布局补偿系数(注意这两个词汇,绘制是为了渲染,绘在什么地方由补偿系数说了算)到指定的item views中,这些item views来自于adapter数据源(这里的adapter指的是前面提到的RecyclerView#Adapter,具体到本项目是StaggeredAdapter)。这种被允许的方式一定(注意情态动词can be)是有用的,哪里有用?对于绘制items之间的分割线,高亮,可视化分组分割线等一系列需求都是有用的。
*
所有的ItemDecorations按照被添加的顺序绘制(这一句信息量大啊!他告诉我们ItemDecorations可以随意添加,无论多少个。但是他们要按照添加的顺序排队绘制,先绘制的或许会被覆盖看不到。砌墙的砖头——后来居上嘛!),所有的这些ItemDecorations的添加时间段是在ItemDecoration#onDraw()方法被调用之前(我能理解,不然无法绘制), 在ItemDecoration#onDrawOver()之后(我理解不了啊,因为没有意义啊!在此,我大胆推测Google想表达的是,绘制期间不能有添加逻辑,否则就会报异常)。
看完了定义,我们可以确定RecyclerView#ItemDecoration的一些关键信息:使用前需要先绘制,而绘制使用的onDraw()和onDrawOver()方法。那么我们来看一下这个类吧!
public abstract static class ItemDecoration{
}
// 这是一个共有的静态抽象内部类,从这点来说跟ViewHolder和Adapter没啥两样,都是待价而沽,静候明主。
既然是抽象类必然有抽象方法,如下图:
截图中清晰地展示ItemDecoration的三个抽象方法onDraw()、onDrawOver()、getItemOffsets(),有人说你瞎吗?明明是6个。我不瞎,另外三个是同名过期的方法。
/**
* Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
* Any content drawn by this method will be drawn before the item views are drawn,
* and will thus appear underneath the views.
*
* 绘制任何适合的decorations进入到画布中,这张画布是提供给RecyclerView使用的(这个当然,否则如何保证同步呢)
* 任何被这个方法绘制的内容都是在item views(指的是RecyclerView的每一个ItemView)被绘制之前绘制,并且(该方法绘制的东西)将出现在所有view的最底层。【这一段传递两个信息,ItemDecoration会被首先绘制,它在最底层。换句话说,如果不做布局上的调整,哪辈子也看不到它】
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
* @param state The current state of RecyclerView
*/
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
/**
* Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
* Any content drawn by this method will be drawn after the item views are drawn
* and will thus appear over the views.
* 绘制任何适合的decorations进入到画布中,这张画布是提供给RecyclerView使用的(这个当然,否则如何保证同步呢)
* 任何被这个方法绘制的内容都是在item views(指的是RecyclerView的每一个ItemView)被绘制之后绘制,并且(该方法绘制的东西)将出现在所有view的最顶层。【这一段传递两个信息,ItemDecoration会被最后绘制,它在最顶层。换句话说,如果不做布局上的调整,天天都能看到它】
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
* @param state The current state of RecyclerView.
*/
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
/**
* Retrieve any offsets for the given item. Each field of outRect
specifies
* the number of pixels that the item view should be inset by, similar to padding or margin.
* The default implementation sets the bounds of outRect to 0 and returns.
* 译文:
* 取回指定item的任何补偿系数(这里指的是布局补偿系数)。参数outRect的每个属性是以item应该被插入的像素数为基准的(就是说item上下左右都可以插入,但插入的是一个矩形,就算他在视觉上是一条线,它的本质是一个矩形),类似于margin或padding
* 默认outRect的边界设置为0,就是没有布局。
*
* If this ItemDecoration does not affect the positioning of item views, it should set
* all four fields of outRect
(left, top, right, bottom) to zero
* before returning.
* 译文:
* 如果该ItemDecoration不影响item views的位置(比如绘制的位置没有交叉重叠),那么outRect的四个属性值都可以设置为0,但是需要在返回之前设置。
*
* If you need to access Adapter for additional data, you can call
* {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the View.
* 译文:如果你需要访问Adapter里的其他数据的话,可以通过调用RecyclerView#getChildAdapterPosition(View)方法来获取中指定位置的view
*
* @param outRect Rect to receive the output.
* @param view The child view to decorate
* @param parent RecyclerView this ItemDecoration is decorating
* @param state The current state of RecyclerView.
*/
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
通过源码追踪不难发现,getItemOffsets()最终会被StaggeredGridLayoutManager的onLayoutChildren()调用【onLayoutChildren()—>fill()—>measureChildWithDecorationsAndMargin()—>calculateItemDecorationsForChild()—>getItemDecorInsetsForChild()—>getItemOffsets()】,StaggeredGridLayoutManager是RecyclerView#LayoutManager的派生类,onLayoutChildren()是其派生方法,定义在RecyclerView中,用于完成布局任务。结合View的绘制流程:测量(onMeasure)——布局(onLayout)——绘制(onDraw),ItemDecoration的执行流程应该是线调用getItemOffsets,接着调用onDraw,最后调用onDrawOver
在项目文件夹中找到staggered文件夹右键创建StaggeredItemDecoration并继承自RecyclerView.ItemDecoration,代码如下:
package com.edwin.idea.staggered;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.Log;
import android.view.View;
/**
* Created by Edwin,CHEN on 2018/9/18.
*/
public class StaggeredItemDecoration extends RecyclerView.ItemDecoration {
private static final String TAG = StaggeredItemDecoration.class.getSimpleName();
@NonNull
public static StaggeredItemDecoration createDefault(Context context) {
return new StaggeredItemDecoration(context);
}
private StaggeredItemDecoration(Context context) {
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
Log.d(TAG, "getItemOffsets");
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
Log.d(TAG, "onDraw");
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
Log.d(TAG, "onDrawOver");
}
}
在StaggeredFragment.java文件中声明并添加ItemDecoration实例到RecyclerView中,代码如下:
private StaggeredItemDecoration staggeredItemDecoration;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
viewGroupRoot = (ViewGroup) inflater.inflate(R.layout.fragment_staggered, null);
recyclerView = (RecyclerView) viewGroupRoot.findViewById(R.id.recycler_view);
staggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
staggeredItemDecoration = StaggeredItemDecoration.createDefault(getContext().getApplicationContext());
staggeredAdapter = new StaggeredAdapter();
initData();
staggeredAdapter.setData(list);
recyclerView.setLayoutManager(staggeredGridLayoutManager);
//注意:这里是addXXX()不是setXXX()
recyclerView.addItemDecoration(staggeredItemDecoration);
recyclerView.setAdapter(staggeredAdapter);
return viewGroupRoot;
}
@Override
protected void initData() {
list = new ArrayList<>();
for (int i = 0; i < 50; i++) {
StaggeredVO staggeredVO = new StaggeredVO();
staggeredVO.setPrice("$ " + i);
if (i % 2 == 1) {
staggeredVO.setDescription("Staggered Formal Item " + i);
}
list.add(staggeredVO);
}
}
执行代码,会得出如下图信息,
从图中可以看到三个方法的执行,这证明顺序证明了我们在8.2结尾处的假设。
绘制分割线的本质就是绘制一个Drawable,Google对Drawable的定义是:
A Drawable is a general abstraction for “something that can be drawn.”
Drawable就是对“将要绘制的东西”的提取
关于Drawable既可以自定义xml文件,也可以通过系统获取,
1.这里我们以系统提供的分割线属性为例,如下图是系统提供的自定义属性:(android包的res/values/attrs.xml文件中)
在StaggeredItemDecoration的构造方法中,获取系统提供的自定义属性如下:
//获取系统提供的分割线属性,并初始化单元素数组ATTRS
private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
//分割线
private Drawable mDivider;
//分割线高度
private int mDividerHeight;
//分割线宽度
private int mDividerWidth;
private StaggeredItemDecoration(Context context) {
TypedArray a = context.obtainStyledAttributes(ATTRS);
// 此处获取reference不同于Boolean、Integer等的获取方式,类似的还有getText()、getString()
mDivider = a.getDrawable(0);
a.recycle();
//假设分割线的基础单元是1*1,宽高必须定义
mDividerWidth = 1;
mDividerHeight = 1;
}
2.自定义Drawable代码如下:【未验证】
public StaggeredItemDecoration(Context context, Drawable drawable) {
ColorDrawable dividerDrawable = new ColorDrawable(context.getResources().getColor(android.R.color.holo_red_dark));
mDivider = dividerDrawable;
//假设分割线的基础单元是1*1,宽高必须定义
mDividerWidth = 1;
mDividerHeight = 1;
}
接着在getItemOffsets中设置布局参数,由于分割线在右下方,宽度都是1,outRect的参数设置如下:
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
Log.d(TAG, "getItemOffsets");
outRect.set(0/*left*/, 0/*top*/, 1/*right*/, 1/*bottom*/);
}
现在运行代码,应该看不到什么效果,因为一像素的白色实在不是人眼所能分辨的,建议改成111像素试试,被切割的感觉如何?
接下来可以就是onDraw()方法的调用了,通过它可以完成绘制工作。分为两步:水平分割线和垂直分割线。
画图的本质就是确定颜色搭配,起始位置,中间路径。画直线只考虑起始位置就好了,因为图的颜色是系统默认的。一个矩形图,分为左上右下四个位置,计算好了调用它的draw方法绘制就好。对于每一个itemView,需要获取其当前位置的LayouParams信息,根据其布局信息,才能画出分割线。
/**
* 绘制水平分割线,分割竖直方向的item
* @param c
* @param recyclerView
*/
private void drawHorizontal(Canvas c, RecyclerView recyclerView) {
int childCount = recyclerView.getChildCount(); // visible children【看不见,不用绘制,系统也不调用。】
if (childCount <= 1) { //只有一个不分割
return;
}
for (int i = 0; i < childCount; i++) {
// 获取当前的itemView,child就是itemView及其布局参数
View child = recyclerView.getChildAt(i);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
int left = child.getLeft() - layoutParams.leftMargin; //左边起点,别忘了去掉margin和padding
int top = child.getBottom() + layoutParams.bottomMargin; // 水平线矩形在下边
// 注意,mDividerWidth是竖直分割线矩形宽度,要加上
int right = child.getRight() + layoutParams.rightMargin + mDividerWidth;
// 注意,mDividerHeight是水平分割线矩形宽度,要加上
int bottom = child.getBottom() + layoutParams.bottomMargin + mDividerHeight;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
/**
* 绘制垂直分割线,分割水平方法的item
* @param c
* @param recyclerView
*/
private void drawVertical(Canvas c, RecyclerView recyclerView) {
int childCount = recyclerView.getChildCount(); // visible children
int itemCount = recyclerView.getAdapter().getItemCount();// total items
if (childCount <= 1) {
return;
}
for (int i = 0; i < childCount; i++) {
View child = recyclerView.getChildAt(i); // child就是itemView
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
int left = child.getRight() + layoutParams.rightMargin; // 竖直分割线在右边
int top = child.getTop() - layoutParams.topMargin; //需要减去顶部布局margin
// 注意,mDividerWidth是竖直分割线矩形宽度,要加上
int right = child.getRight() + layoutParams.rightMargin + mDividerWidth;
// 注意,mDividerHeight是水平分割线矩形宽度,绘制水平线时已经添加,这里不可以再添加
int bottom = child.getBottom() + layoutParams.bottomMargin /*+ mDividerHeight*/;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
将这两个方法添加到onDraw()方法中,代码如下:
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
Log.d(TAG, "onDraw");
drawHorizontal(c, parent);
drawVertical(c, parent);
}
运行一下代码,结果如下图:
最后一个itemView左边的分割线怎么没了,一首《凉凉》送给在下~
其实,这也是我写这篇文章的初衷,因为我一个朋友遇到这个问题之后果断放弃了,这增加了我解决该问题的决心。
该问题的直接感受是左边的最后一个itemView没有竖直分割线,原因是它右边没有兄弟啊,没人帮他补这条线。解决方案当然是,绘制右边的分割线了,我的方案是:
1.确定最后一个itemView是否在右边边【可以试一下,最后一个在左边不存在这种情况】
2.记录最后一次左边竖直分割线线的结束位置
3.给最后一个itemView绘制三条线,主要是左边这条
更改drawVertical()方法代码如下:
/**
* 绘制垂直分割线,分割水平方法的item
* @param c
* @param recyclerView
*/
private void drawVertical(Canvas c, RecyclerView recyclerView) {
int childCount = recyclerView.getChildCount(); // visible children
int itemCount = recyclerView.getAdapter().getItemCount();// total items
if (childCount <= 1) {
return;
}
for (int i = 0; i < childCount; i++) {
View child = recyclerView.getChildAt(i); // child就是itemView
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
int left = child.getRight() + layoutParams.rightMargin; // 竖直分割线在右边
int top = child.getTop() - layoutParams.topMargin;
// 注意,mDividerWidth是竖直分割线矩形宽度,要加上
int right = child.getRight() + layoutParams.rightMargin + mDividerWidth;
// 注意,mDividerHeight是水平分割线矩形宽度,绘制水平线时已经添加,这里不可以再添加
int bottom = child.getBottom() + layoutParams.bottomMargin /*+ mDividerHeight*/;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
// 特殊处理最后两个itemView,其实应该是最后三个
int itemPosition = recyclerView.getChildAdapterPosition(child);
if (itemPosition == itemCount - 1 || itemPosition == itemCount - 2) {
// 最后两个特殊处理
StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) child.getLayoutParams();
int spanIndex = lp.getSpanIndex(); // leftItemSpanIndex = 0, rightItemSpanIndex = 1
if (itemPosition == itemCount - 2 && spanIndex == 0) {
previousBottom = bottom;
} else if (itemPosition == itemCount - 1 && spanIndex == 1) {
// 注意补充的线在左边
mDivider.setBounds(child.getLeft() - layoutParams.leftMargin, previousBottom, child.getLeft() - layoutParams.leftMargin + mDividerWidth, bottom);
mDivider.draw(c);
}
}
}
}
再次使用快捷键Ctrl+R运行一下代码吧~
结果如下图:
终于写完了,代码中存在一些重复绘制,onDrawOver也没有验证。对的,我就是不想验证~
诗云:
滚滚长江东逝水,浪花淘尽英雄。是非成败转头空,青山依旧在,几度夕阳红。
白发渔樵江渚上,惯看秋月春风。一壶浊酒喜相逢,古今多少事,都付笑谈中!