上次我写了 理解RecyclerView的RecyclerView.ItemDecoration(一),介绍了ItemDecoration的第一个设置它padding的getItemOffsets方法,今天我们就来了解一下它的第二个方法onDraw()。这个方法主要是给每一个RecyclerView的item做一个装饰,这个装饰我们可以理解为很多种,其中一种最简单的就是画divider,即分割线。 还有,这个方法的执行时间在RecyclerView绘制每一个item之前,那么它所绘制的view在RecyclerView的item层的下面。
为了讲清楚自己所理解的ItemDecoration,大家主要注意转换一个观点,而这个观点是对比ListView的。那么还是从ListView的分割线开始吧。我们知道ListView的分割线是ListView的一个属性值就可以决定的:
<ListView
android:id="@+id/list_view"
android:divider="@color/colorPrimary"
android:dividerHeight="2dp"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
代码中divider和dividerHeight就可以决定我们的分割线的样式和大小了。但是RecyclerView却没有这个功能,原因很容易去想,因为RecyclerView可以配置不同的自定义的LayoutManager,不同的LayoutManager控制了RecyclerView中每一个Item的排列方式,不同的排列方式就意味着不同的divider排列方式和逻辑,这样对于RecyclerView来说是灾难性的,为了解决这个问题,所以RecyclerView就引出了ItemDecoration,而RecyclerView的中每一个item都拥有了自己的ItemDecoration,通过设置假设的padding值,去绘制自己的周围的东西,然后完成想要的结果。我们可以来看看下图:
在ListView的时代里,这种效果是很难实现的,因为那时候的divider都是一致的,执行的是同一个标准,但是当今时代耳朵RecyclerView是很容易实现的,原因是它把装饰每一个Item的任务都放开了,不管了,交给了ItemDecoration来打理了,通过ItemDecoration很容易实现这个效果了。
所以我们需要转变的角度就是,对于像分割线这样的任务,RecyclerView不是像ListView那样,把所有的样式和统一掉了,而是采取了一种放任不管的态度,对于每一个item,自己想怎么画就怎么画,充分展示了RecyclerView的多样性和高度的解耦性。这也就是ItemDecoration类的名字如此贴切的原因了,意为条目装饰了。
既然我们了解了ItemDecoration的本质,那么给它画个分割线是非常容易事了,还是来画个图分析一下吧:
对于一个竖直的LinearLayoutManager的LayoutManager,假设它的排列方式是vertical的,那么他们就是一直从上到下的排列,此时如果我想需要一个divider,那么首先需要设置一下这个:
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.bottom = mDecorationHeight;
}
先设置一下outRect的bottom值,这个不是很了解的,请看上一篇文章,那么就意味着我们每一个item底部就有了一个mDecorationHeight的padding值了,现在的我们,就需要在这个item底部的padding区域里面上色了,那么这个上色的过程就是今天需要讲的onDraw方法了:
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
}
那么对于每一个item来讲,现在最重要的是找到bottompadding的区域,然后使用我们的Paint进行绘制了:
可以看出,我们需要知道绿色矩形的坐标值,知道坐标值我们就能对这个矩形添加颜色了,我们可以遍历RecyclerView的每一个View,通过View的位置来计算Rect的坐标值,这个很简单,相信大家看图就知道了,我就列一下代码大家观摩一下:
private void drawVertical(Canvas c, RecyclerView parent) {
//对于最后一个item我们不需要进行绘制
int childCount = parent.getChildCount() - 1;
Rect rect = new Rect();
for (int i = 0; i < childCount; i++) {
View childView = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) childView.getLayoutParams();
//矩形的left点与RecyclerView的paddingleft()一致
rect.left = parent.getPaddingLeft();
rect.right = parent.getMeasuredWidth() - parent.getPaddingRight();
//这个就不说了,大家用心体会
rect.top = childView.getBottom() + params.bottomMargin;
rect.bottom = rect.top + mDecorationHeight;
//设置不同的颜色
mPaint.setColor(mColors[mRandom.nextInt(mColors.length)]);
c.drawRect(rect, mPaint);
}
}
基本上得到的结果就是图一了,这个就没啥好说的啊。对于LinearLayoutManager.Horizontal,看下图估计也可以进行写出相应代码了:
基本代码如下,我也不多说了,思路大致一样。
private void drawHorizontal(Canvas c, RecyclerView parent) {
int childCount = parent.getChildCount() - 1;
Rect rect = new Rect();
for (int i = 0; i < childCount; i++) {
View childView = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) childView.getLayoutParams();
rect.left = childView.getRight() + params.rightMargin;
rect.right = rect.left + mDecorationHeight;
rect.top = parent.getPaddingTop();
rect.bottom = rect.top + childView.getMeasuredHeight() + params.bottomMargin;
mPaint.setColor(mColors[mRandom.nextInt(mColors.length)]);
c.drawRect(rect, mPaint);
}
}
好了,现在到了一个最难的,为StaggeredGridLayoutManager设置StaggeredGridLayoutItemDecoration了,为啥说这是最难的呢,待我我们慢慢去看。可能对StaggeredGridLayout的认识度不是很高,很多东西都不是太理解。
我们最终需要完成的样子如下:
对于StaggeredGridLayoutManager,我们所感觉的最多的也就是瀑布流了,好多照片墙的设置就是瀑布流的形式,图片的瀑布流的形成,是我们设置了图片的不同的高度,造成了视觉上的错觉,然后发现了美,好了,扯远了,现在来分析分析这玩意怎么加padding吧,然后才能画我们的Paint了。
我们先来声明一下,所有的item都是左边和下边加上padding,排在第一排的item上边加上paddingTop值,在最右边的加上item加上paddingRight值,说了这么多,大伙看图:
看好了,这就是我们的设置padding的地方,那么代码如下:
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
//先获取spanCount值
if (mSpanCount < 0) {
StaggeredGridLayoutManager manager = (StaggeredGridLayoutManager) parent.getLayoutManager();
mSpanCount = manager.getSpanCount();
}
//获取位置
outRect.left = mItemWidth;
outRect.bottom = mItemWidth;
int position = parent.getChildLayoutPosition(view);
// 位于第一排的 需要来个paddingTop
if (position < mSpanCount) {
outRect.top = mItemWidth;
}
//位于最右边的加上paddingRight
if((position +1 )% mSpanCount == 0) {
outRect.right = mItemWidth;
}
}
这样对么??当然了,要是这么简单,我就是不认为它是坑了,原因是不对的,我们看看运行的结果:
我们看到了第2,5,8,11,14项出现了paddingRight,但是它并不是位于最右边的位置,这样就坑爹了,为啥呢??其实这需要从StaggeredGridLayoutManager的对item的排列方式说起了,它的layout并不是我们所想象的那样,感觉就是简单的顺序排列,而是一种非常复杂的方式进行layout的。怎么说呢?咋们来用图描述描述:
图中有了5个item进行插入了,但是它们的排列顺序并不是我们想象的那样,感觉item3需要排列在第0列,但是结果它却排在了第2列,item4需要排列在第1列,却现在排在了第2列。这个问题深究起来需要对StaggeredGridLayoutManager比较熟悉,我还是比较梗,但是我还是想说一下,因为我也不是很懂它的源码。还记得我们初始化StaggeredGridLayoutManager时传入了一个参数吗?
mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL));
这里传入了一个3,当然你可能觉得这个3就是告诉StaggeredGridLayoutManager我就需要三列,当然要是这么简单就很容易了,当你传入了一个3之后,StaggeredGridLayoutManager内部就会初始化三个Span类来记录每一列的高度,当每一个span类都有数值时,也就是说明它到了第二行,此时就需要比较三个span中哪一个数值是最小的,如果是最小的,我就把下一个view插入到这一列下,然后再增加该列下的span值的高度。理解了这个,你就会发现下面的公式是计算的不准确的,所以需要改啊,怎么改呢??
//位于最右边的加上paddingRight
if((position +1 )% mSpanCount == 0) {
outRect.right = mItemWidth;
}
幸好,stackoverflow上早有大神给出了答案,获取每一个view上的spanIndex,通过spanIndex来判断View是否在最右边。正因为这个,我还修复了github上一个给出的时间线Dome, 如果你有时间,可以去瞅瞅,顺便给个star.
//位于最右边的加上right
StaggeredGridLayoutManager.LayoutParams params = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
int spanIndex = params.getSpanIndex();
//当然了,这个还是需要些技巧的啊。
if (spanIndex == mSpanCount - 1) {
outRect.right = mItemWidth;
}
好了,我们解决了这个问题之后,现在就需要draw我们的padding值了,这个也基本上和LinearLayoutManager上差不多,只不过需要 drawVertical和drawHorizontal了,也差不多了吧,稍后代码会打包上来的,别急。
写到这里感觉也差不多,虽然写了很多废话,但是锻炼一下自己的认知能力,还是有必要的。好了,这是第二篇介绍这玩意的,就写到这里了吧。
代码