这是这个系列的第二篇文章,第一篇 ItemDecoration深刻解析与实战(一)——源码分析 是偏原理性的,而这篇是偏应用性的。没看过上一篇文章对阅读此文也基本没多大影响,不过了解原理会加深对本文Demo的理解。java
这篇文章将会实现上篇文章最后说的几个实战点,包括:android
LinearLayoutManager
) 最简单的分割线实现LinearLayoutManager
) 自定义分割线实现GridLayoutManager
) 网格布局下的均分等距间距(分割线)StaggeredLayoutManger
) 瀑布流布局下均分等距间距(分割线)GridLayoutManager
) 网格布局下实现表格式边框看完这6点标题,应该会知道这篇文章的篇幅会稍长,不过由于是实战类型的文章,因此也不会特别枯燥。git
LinearLayoutManager
) 最简单的分割线实现像这种单一颜色的分割线实现起来很简单,就是一行代码:github
public class SimpleDividerDecoration extends RecyclerView.ItemDecoration {
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
outRect.set(0,0,0,5);
}
}
复制代码
这个5对应的就是outRect.bottom
,看过这系列的上篇文章就能容易理解,这个跟在 ItemView
的布局文件中增长一个 marginBottom
是同样的效果的。不过这样默认是没有颜色的,这个分割线的颜色就取决于 RecyclerView
的背景颜色。如咱们的效果图的实现:算法
RecyclerView rvTest = findViewById(R.id.rv_test);
rvTest.addItemDecoration(new SimpleDividerDecoration());
复制代码
这种实现很简单,可是缺点也很突出,由于他是依赖于 RecyclerView 的背景的,而若是咱们为 RecyclerView 设置一个padding,就会变成这样:canvas
就是说万一咱们的需求是有padding,并且背景颜色要跟分割线颜色不一样那就没办法了。若是要解决这一问题,就要看第2点。缓存
LinearLayoutManager
) 自定义分割线实现因为 support 包中已经有了一个默认的实现,因此就没有本身写了,这是官方自带的 ItemDecoration
实现类,先看下怎么用:bash
rvTest.setLayoutManager(new LinearLayoutManager(this));
DividerItemDecoration decoration = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
decoration.setDrawable(getResources().getDrawable(R.drawable.divider_gradient));
rvTest.addItemDecoration(decoration);
复制代码
在示例中,我为这个Decoration添加了一个 Drawable
,这个 Drawable
就是上图的一个分割线效果,若是没有设置这个,那么将会有一个默认的灰色分割线:ide
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:endColor="#19f5e7"
android:startColor="#b486e2" />
<size android:height="4dp" />
</shape>
复制代码
分割线的高度就是这个Drawable
的高。源码分析
用法很简单,但正所谓知其然,还要知其因此然,咱们看一下这个 DividerItemDecoration
里面的具体实现是怎样的:
getItemOffsets
方法的具体实现// DividerItemDecoration.java
private Drawable mDivider;
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0);
return;
}
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); //注释1
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
复制代码
直接看注释1,mOrientation == VERTICA
的状况,在 getItemOffsets
方法中,也是用了咱们第1个实战点中最简单的那种方式,只不过他的高度变成了mDivider.getIntrinsicHeight()
而已,这个mDivider
就是咱们 setDrawable
中设置的一个 Drawable
对象,若是没有设置,那就会有一个默认的。
onDraw
方法@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null || mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
复制代码
这里也分为两种状况,咱们直接看 VERTICAL
下的,即 drawVertical(c, parent)
方法:
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
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); //注释1
final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
复制代码
咱们先看注释分割线的上边,逻辑很简单,主要就是为了拿到 Child 最大可用空间的左右边界,若是咱们没有设置,parent.getClipToPadding()
默认是返回 ture
的,即最大可用空间的要减去RecyclerView
的padding,这是为了让padding不被分割线覆盖。
再看注释分割线的下边,这里遍历了全部的 Child 。先看注释1这句代码,parent.getDecoratedBoundsWithMargins(child, mBounds)
,这个方法有什么用呢,其实看名称就能大概猜出来,这个方法能够拿到
child边界+decoration + margin
所组成的Rect的边界值mbounds
,即下图里面的橙色区域的外边框所对应的值。
注意:此图不严谨,详细内容请看这系列的上一篇文章
而后便会将mbounds
的 bottom 跟 top ,以及 上面获得的 left 跟 right 设置到 mDivider
的边界中,就得到的咱们上图的红色虚线边框的矩形,若是咱们没有为 itemView 设置 margin,那么就会获得绿色虚线边框的范围,再将这部分画出来,就获得了咱们想要的分割线了。
GridSpaceDecoration
效果如上图,解决了下面的常见问题:
public GridSpaceDecoration(int horizontal, int vertical){
//...
}
public GridSpaceDecoration(int horizontal, int vertical, int left, int right){
//...
}
/**
* @param horizontal 内部水平距离(px)
* @param vertical 内部竖直距离(px)
* @param left 最左边距离(px),默认为0
* @param right 最右边距离(px),默认为0
* @param top 最顶端距离(px),默认为0
* @param bottom 最底端距离(px),默认为0
*/
public GridSpaceDecoration(int horizontal, int vertical, int left, int right, int top, int bottom){
//...
}
复制代码
该类提供了三个构造方法,直接设置相应的值,而后 add 到 RecyclerView
中便可。
要实现的功能很清晰,就是要解决上面的常见问题。其中,第二、3点比较麻烦,为何呢?先分析一下
先看下上图,当使用 GridLayoutManager
时,GridLayoutManager
会将每一个 Item 的最大可用空间平均分配开来,就像上图黑线所对应的三个框就是3个 Item 的最大可分配空间。橙色区域就是 Decoration 设置的值跟 item 的 margin ,若是 margin 为0,那么橙色区域即是在 getItemOffsets
方法中设置的值(下面简称 offsets)。绿色虚线所围成的区域就是咱们 itemView 的实际空间。
经过上图,当咱们为 item 设置相同的间距时,会发现 item 1 的空间被压缩了,那么怎么解决这一问题呢?
咱们要解决的就是上面的问题
先讨论第1点,由于每一个 item 的最大可用空间(黑色框格子)是一致的,因此想要让 item 的宽度同样,就是让每一个 item 的 offsets 保持一致。咱们能够获得下面的公式:
sizeAvg = (left + right + center * (spanCount-1)) / spanCount
其中,left 、right 为最左、左右边间距,center 为中间间距,spanCount 为每一行的 span 个数,就能够得出每一个 item 须要设置的 offsets 大小 sizeAvg,这样就能够保证每一个 item 的宽度一致(均分)
再看第2点,咱们要保证每一个中间间距都同样,左右间距达到咱们设置的大小。首先,最左边的间距是已经肯定了的,即 left,那么最左边 item 的右边 right1 就能够得出为 sizeAvg - left,第二个 item 左边间距 left2 就是 center - right1 同理能够推出接下来的 item ,看下图会更清晰:
而后把中间的实体线给去掉:
就能够看到每一个 item 的宽度同样了,并且间距也是符合预期的效果。(图片是人工画的,可能会有点小偏差)
上面分析完成,接着看看算法实现:
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
if (isFirst) {
init(parent);
isFirst = false;
}
if (mManager.getOrientation() == LinearLayoutManager.VERTICAL) {
handleVertical(outRect, view, parent, state); //注释1
} else {
handleHorizontal(outRect, view, parent, state);
}
}
复制代码
很简单,先是作了一点初始化,而后分两个方向进行不一样处理。直接看注释1(orientation == VERTICAL)部分:
private void handleVertical(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) view.getLayoutParams();
int childPos = parent.getChildAdapterPosition(view);
int sizeAvg = (int) ((mHorizontal * (mSpanCount - 1) + mLeft + mRight) * 1f / mSpanCount);
int spanSize = lp.getSpanSize();
int spanIndex = lp.getSpanIndex();
outRect.left = computeLeft(spanIndex, sizeAvg); //注释1
if (spanSize == 0 || spanSize == mSpanCount) {
outRect.right = sizeAvg - outRect.left;
} else {
outRect.right = computeRight(spanIndex + spanSize - 1, sizeAvg);
}
outRect.top = mVertical / 2;
outRect.bottom = mVertical / 2;
if (isFirstRaw(childPos)) {
outRect.top = mTop;
}
if (isLastRaw(childPos)) {
outRect.bottom = mBottom;
}
}
复制代码
这里的 sizeAvg 就是咱们上面分析的那个 sizeAvg,而后再调用 computeLeft
方法(注释1),先看下这个方法这怎样的实现:
private int computeLeft(int spanIndex, int sizeAvg) {
if (spanIndex == 0) {
return mLeft;
} else if (spanIndex >= mSpanCount / 2) {
//从右边算起
return sizeAvg - computeRight(spanIndex, sizeAvg);
} else {
//从左边算起
return mHorizontal - computeRight(spanIndex - 1, sizeAvg);
}
}
private int computeRight(int spanIndex, int sizeAvg) {
if (spanIndex == mSpanCount - 1) {
return mRight;
} else if (spanIndex >= mSpanCount / 2) {
//从右边算起
return mHorizontal - computeLeft(spanIndex + 1, sizeAvg);
} else {
//从左边算起
return sizeAvg - computeLeft(spanIndex, sizeAvg);
}
}
复制代码
其实就是一个递归的算法,用的就是上面分析的逻辑,不清楚能够回去翻翻上面的图。计算出水平的 offsets 后,后面的就很简单了,接下来会判断是否第一行跟最后一行来设置最顶部 top 跟最底部 bottom 。
这个GridSpaceDecoration
就算完成了,主要就是完成一个 offsets 的设置,若是想要自定义一些分割线的效果,能够继承此类并实现 onDraw
方法便可。
StaggeredLayoutManger
) 瀑布流布局下均分等距间距(分割线)这个实现跟上面的基本差很少,因此贴一下代码就行了:
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
RecyclerView.LayoutManager originalManager = parent.getLayoutManager();
if (originalManager == null || !(originalManager instanceof StaggeredGridLayoutManager)) {
return;
}
StaggeredGridLayoutManager manager = (StaggeredGridLayoutManager) originalManager;
if (manager.getOrientation() == StaggeredGridLayoutManager.VERTICAL) {
handleVertical(outRect, view, parent);
} else {
handleHorizontal(outRect, view, parent);
}
}
private void handleVertical(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent) {
StaggeredGridLayoutManager.LayoutParams params =
(StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
int spanIndex = params.getSpanIndex();
int adapterPos = parent.getChildAdapterPosition(view);
int sizeAvg = (int) ((mHorizontal * (mSpanCount - 1) + mLeft + mRight) * 1f / mSpanCount);
int left = computeLeft(spanIndex, sizeAvg);
int right = computeRight(spanIndex, sizeAvg);
outRect.left = left;
outRect.right = right;
outRect.top = mVertical / 2;
outRect.bottom = mVertical / 2;
if (isFirstRaw(adapterPos, spanIndex)) {
//第一行
outRect.top = mTop;
}
if (isLastRaw(spanIndex)) {
//最后一行
outRect.bottom = mBottom;
}
}
复制代码
GridLayoutManager
) 网格布局下实现表格式边框StaggeredSpaceDecoration
TableDecoration
TableDecoration
是继承于上面第3点的 GridSpaceDecoration
来实现的,GridSpaceDecoration
负责间距处理,TableDecoration
则是将分割线给画出来。因此主要就是 onDraw
方法的实现:
先看构造方法:
public class TableDecoration extends GridSpaceDecoration {
private Drawable mDivider;
private int mSize;
private Rect mBounds;
/**
* @param color 边框颜色
* @param size 边框大小(px)
*/
public TableDecoration(@ColorInt int color, int size) {
super(size, size, size, size, size, size);
mSize = size;
mDivider = new ColorDrawable(color);
mBounds = new Rect();
}
}
复制代码
就是将 item 的全部边框都设置为 size ,而后根据传进来的 color 建立一个 Drawable 对象。接着看 onDraw
方法:
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View view = parent.getChildAt(i);
draw(c, parent, view);
}
drawLast(c, parent);
}
复制代码
先是遍历全部 child ,而后进行每一个 child 的绘制:
private void draw(Canvas canvas, RecyclerView parent, View view) {
canvas.save();
int translationX = Math.round(view.getTranslationX());
int translationY = Math.round(view.getTranslationY());
int viewLeft = view.getLeft() + translationX;
int viewRight = view.getRight() + translationX;
int viewTop = view.getTop() + translationY;
int viewBottom = view.getBottom() + translationY;
parent.getDecoratedBoundsWithMargins(view, mBounds);
drawLeft(canvas, mBounds, viewLeft);
drawRight(canvas, mBounds, viewRight);
drawTop(canvas, mBounds, viewTop);
drawBottom(canvas, mBounds, viewBottom);
canvas.restore();
}
private void drawLeft(Canvas canvas, Rect bounds, int left) {
mDivider.setBounds(bounds.left, bounds.top, left, bounds.bottom);
mDivider.draw(canvas);
}
//...
复制代码
逻辑也不难,跟第2点 自定义分割线实现 里的逻辑差很少,将咱们设置的 item 的全部间距画出来,这里就不细说了。画完全部 item 后,还会在 onDraw
调用一个 drawLast
方法,咱们先看看没有调用这个方法是怎样的效果:
能够很明显看出,最后那里若是 item 不是铺满整一行的话,会致使后面那里有一部分的缺陷,这个缺陷其实咱们在第3点 网格布局下的均分等距间距(分割线) 即 GridSpaceDecoration
时分析过程当中就能够发现了,因为每一个 item 的上下左右 offsets 并不必定一致,因此会致使当没有最后一行有空缺的话就会形成一个边框的缺陷。
缘由了解了,那么问题解决应该也不难:
private void drawLast(Canvas canvas, RecyclerView parent) {
View lastView = parent.getChildAt(parent.getChildCount() - 1);
int pos = parent.getChildAdapterPosition(lastView);
if (isLastColumn((GridLayoutManager.LayoutParams) lastView.getLayoutParams(),pos)){
return;
}
int translationX = Math.round(lastView.getTranslationX());
int translationY = Math.round(lastView.getTranslationY());
int viewLeft = lastView.getLeft() + translationX;
int viewRight = lastView.getRight() + translationX;
int viewTop = lastView.getTop() + translationY;
int viewBottom = lastView.getBottom() + translationY;
parent.getDecoratedBoundsWithMargins(lastView, mBounds);
canvas.save();
if (mManager.getOrientation() == LinearLayoutManager.VERTICAL) {
int contentRight = parent.getRight() - parent.getPaddingRight() - Math.round(parent.getTranslationX());
//空白区域上边缘
mDivider.setBounds(mBounds.right, mBounds.top, contentRight, viewTop);
mDivider.draw(canvas);
//空白区域左边缘
mDivider.setBounds(viewRight, viewTop, viewRight + mSize, mBounds.bottom);
mDivider.draw(canvas);
}else {
int contentBottom = parent.getBottom()-parent.getPaddingBottom()-Math.round(parent.getTranslationY());
//空白区域上边缘
mDivider.setBounds(mBounds.left,viewBottom,mBounds.right,viewBottom+mSize);
mDivider.draw(canvas);
//空白区域左边缘
mDivider.setBounds(mBounds.left,mBounds.bottom,viewLeft,contentBottom);
mDivider.draw(canvas);
}
canvas.restore();
}
复制代码
主要逻辑就是将空缺出来的地方给补齐。
GridLayoutManager
) 打造粘性头部StickHeaderDecoration
上面的几个例子中,getItemOffsets
以及 onDraw
方法都用过了,Decoration 中三大方法还有一个 onDrawOver
,这个效果就是用 onDrawOver
来实现的。
逻辑是这样的:要实现这样的效果,咱们须要在 RecyclerView 的顶部画上一个 StickHeader,也就是咱们的第一个 Child。 同时也有一个问题就是咱们怎么知道哪一个 item 是能够当成头部(StickHeader)的,这里我提供了一个接口来进行判断:
public interface StickProvider {
boolean isStick(int position);
}
复制代码
这是 StickHeaderDecoration
的一个内部实现类,须要将它的一个对象做为 StickHeaderDecoration
的构造方法的参数,例如:
StickHeaderDecoration decoration = new StickHeaderDecoration(new StickHeaderDecoration.StickProvider() {
@Override
public boolean isStick(int position) {
return mList.get(position).type == StickBean.TYPE_HEADER;
}
});
//使用labamda会更简洁
StickHeaderDecoration decoration =
new StickHeaderDecoration(position -> mList.get(position).type == StickBean.TYPE_HEADER);
复制代码
而后咱们就能够经过这个StickProvider
对象进行判断是不是须要显示的头部了,接着看主要的方法onDrawOver
:
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
RecyclerView.Adapter adapter = parent.getAdapter();
if (adapter == null || !(adapter instanceof StickProvider)) {
return;
}
int itemCount = adapter.getItemCount();
if (itemCount == 1) {
return;
}
//找到当前的StickHeader对应的position
int currStickPos = currStickPos(parent); //注释1
if (currStickPos == -1) {
return;
}
c.save();
if (parent.getClipToPadding()) {
//考虑padding的状况
c.clipRect(parent.getPaddingLeft(), parent.getPaddingTop(),
parent.getWidth() - parent.getPaddingRight(),
parent.getHeight() - parent.getPaddingBottom());
}
int currStickType = adapter.getItemViewType(currStickPos);
//当前显示的StickHeader相应的ViewHolder,先看有没有缓存
RecyclerView.ViewHolder currHolder = mViewMap.get(currStickType);
if (currHolder == null) {
//没有缓存则新生成
currHolder = adapter.createViewHolder(parent, currStickType);
//主动测量并布局
measure(currHolder.itemView, parent);
mViewMap.put(currStickType, currHolder);
}
adapter.bindViewHolder(currHolder, currStickPos);
c.translate(currHolder.itemView.getLeft(), currHolder.itemView.getTop());
currHolder.itemView.draw(c);
c.restore();
}
复制代码
总体逻辑并不难,先是找到当前要显示的头部,这个头部怎么来的呢,看看注释1处的 currStickPos
方法:
private int currStickPos(RecyclerView parent) {
int childCount = parent.getChildCount();
int paddingTop = parent.getPaddingTop();
int currStickPos = -1;
for (int i = 0; i < childCount; i++) {
//考虑到parent padding 的状况,第一个item有可能不可见状况
//从第1个child向后找
View child = parent.getChildAt(i);
if (child.getTop() >= paddingTop) {
break;
}
int pos = parent.getChildAdapterPosition(child);
if (mProvider.isStick(pos)) {
currStickPos = pos;
}
}
if (currStickPos != -1) {
return currStickPos;
}
for (int i = parent.getChildAdapterPosition(parent.getChildAt(0)) - 1; i >= 0; i--) {
//从第一个child的前一个开始找
if (mProvider.isStick(i)) {
return i;
}
}
return -1;
}
复制代码
主要逻辑分为两步:
再回到 onDrawOver
方法中,当找到当前要显示的 Header 后,并会为他进行测量,而后布局(具体看项目源码),接着再调用 Adapter 的 bindViewHolder
方法进行数据绑定,最后再画出来就ok了,接着看看效果:
看到效果图并非咱们想要达到的效果,很明显缺乏一个推进的效果,那么这个怎么实现呢:
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
//...
//寻找下一个StickHeader
RecyclerView.ViewHolder nextStickHolder = nextStickHolder(parent, currStickPos);
if (nextStickHolder != null) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) currHolder.itemView.getLayoutParams();
int bottom = parent.getPaddingTop() + params.topMargin + currHolder.itemView.getMeasuredHeight();
int nextStickTop = nextStickHolder.itemView.getTop();
//下一个StickHeader若是顶部碰到了当前StickHeader的屁股,那么将当前的向上推
if (nextStickTop < bottom && nextStickTop > 0) {
c.translate(0, nextStickTop - bottom);
}
}
adapter.bindViewHolder(currHolder, currStickPos);
c.translate(currHolder.itemView.getLeft(), currHolder.itemView.getTop());
currHolder.itemView.draw(c);
c.restore();
}
复制代码
逻辑也不难,就是找到下一个 Header ,若是它碰到了上面那个的屁股的话,就将上面那个向上移动一点,就能够造成咱们的推进效果啦。
从决定说要学习这个开始,到写完Demo,写完文章,大概花了2个星期,其中有一些点也是深刻了解了部分源码,掉了很多头发才总结出来。其中也碰到很多坑,并且这个系列目前网上的文章比较杂,不多有一个总体的分析,甚至有一些理解是错的,因此这篇文章写了相对详细不少。
因为编者水平有限,文章不免会有错漏的地方,若有发现,恳请指正,若是有更好的实现思路也能够提供。
要看项目源码或者Demo的戳这里