ItemDecoration深入解析与实战(一)——源码分析
深感歉意,南尘很久没有更新了,确实是各种事情忙的不可开交。近期是不少学弟学妹毕业季,自然找我询问「爱吖校推」的小伙伴也是不下百人,但我基本上都没能提供上什么帮助。在这统一回复一下吧,「爱吖校推」代码质量不好,并且代码量远远超出了一个正常的毕业设计水平,建议大家把数据请求全部切换为本地数据库或者假数据吧,毕设老师不会为难大家的。
RecyclerView,相信作为 Android 开发的你,一定司空见灌,对于官方自带的 ItemDecoration 你一定是信手拈来,但多少人能知道里面的实际原理呢?
下面的这个文章,来自近期看到的一位非常上进的小伙伴的 Blog,一共两篇,我一字不漏地看下来,觉得讲的相当全面,所以一定得分享给大家,如果喜欢的可以点击原文链接去和原作者进行交流。
原文链接:https://www.jianshu.com/p/6d509f21e980
一 概述
ItemDecoration
是 RecyclerView
中的一个抽象静态内部类。
这是官网对 ItemDecoration 的描述,简单来说就是可以为 RecyclerView
的每一个 ItemView 进行一些特殊的绘制或者特殊的布局。从而我们可以为 RecyclerView
添加一些实用好玩的效果,比如分割线,边框,饰品,粘性头部等。
此文会分析ItemDecoration
的使用及原理,然后进行一些Demo的实现,包括分割线,网格布局的边框,以及粘性头部。
二 方法
1. 方法概述
ItemDecoration
中的实际方法只有6个,其中有3个是重载方法,都被标注为 @deprecated
,即弃用了,这些方法如下
修饰符 | 返回值类型 | 方法名 | 标注 |
---|---|---|---|
void | public | onDraw(Canvas c, RecyclerView parent, State state) | |
void | public | onDraw(Canvas c, RecyclerView parent) | @deprecated |
void | pulbic | onDrawOver(Canvas c, RecyclerView parent, State state) | |
void | public | onDrawOver(Canvas c, RecyclerView parent) | @deprecated |
void | public | getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) | |
void | public | getItemOffsets(Rect outRect, View view, RecyclerView parent) | @deprecated |
2. getItemOffsets
除了 getItemOffsets
方法,其他方法的默认实现都为空,而 getItemOffsets
的默认实现方法也很简单:
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
两个getItemOffsets
方法最终都是调用了上面实现,就一行代码,如果我们自定义过 ItemDecoration
的话,就会知道,我们可以为 outRect
设置四边的大小来为 itemView
设置一个偏移量.这个偏移量有点类似于 View
的margin,看下面的图1:
图片很清晰的表示了 ItemView 的结构(该图不是特别精确,后面会说到),这是只有一个 Child 的情况,我们从外往里看:
最外的边界即 RecyclerView 的边界
红色部分是 RecyclerView 的 Padding,这个我们应该能理解
橙色部分是我们为 ItemView 设置的 Margin,这个相信写过布局都能理解
蓝色部分就是我们在
getItemOffsets
方法中给outRect
对象设置的值最后的的黄色部分就是我们的 ItemView 了
总体就是说,getItemOffsets
中设置的值就相当于 margin 的一个存在。"图说无凭",接下来就结合源码讲解一下这个图的"依据"。首先看一下 getItemOffsets
在哪里被调用了:
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
...
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState); //被调用
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
在 RecyclerView
源码中,这是 getItemOffsets
唯一被调用的地方,代码也很简单,就是将 RecyclerView
中所有的(即通过addDecoration()
方法添加的) ItemDecoration
遍历一遍,然后将我们设在 getItemOffsets
中设置的四个方向的值分别累加并存储在insets
这个Rect
当中。那么这个 insets
又在哪里被调用了呢,顺着方法继续跟踪下去:
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
我们看到,在 measureChildWithMargins
方法中,将刚刚得到的 insets
的值与 Recyclerview 的 Padding 以及当前 ItemView 的 Margin 相加,然后作为 getChildMeasureSpec
的第三个参数传进去:
int childDimension, boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
//...省略部分代码
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
getChildMeasureSpec
方法的第三个参数标注为 padding
,在方法体这个 padding
的作用就是计算出 size
这个值,这个 size
是就是后面测量中 Child(ItemView) 能达到的最大值。
也就是说我们设置的 ItemView 的 Margin 以及ItemDecoration.getItemOffsets
中设置的值到头来也是跟 Parent 的 Padding 一起来计算 ItemView 的可用空间,也就印证了上面的图片,在上面说了该图不精确就是因为
parent-padding
layout_margin
insets(all outRect)
他们是一体的,并没有划分成一段一段这样,图中的outRect
也应该改为insets
,但是图中的形式可以更方便我们理解。
3. onDraw
public void onDraw(Canvas c, RecyclerView parent, State state) {onDraw(c, parent);
}
/**
* @deprecated Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDraw(Canvas c, RecyclerView parent) {
}
onDraw
方法有两个重载,一个被标注为 @deprecated
,即弃用了,我们知道,如果重写了 onDraw
,就可以在我们上面的 getItemOffsets
中设置的范围内绘制,知其然还要知其所以然,我们看下源码里面是怎样实现的#RecyclerView.java
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);
}
}
在 ReyclerView
的onDraw
方法中,将会把所有 Decoration
的onDraw
方法调用一遍,而且会把Recyclerview#onDraw(Canvas)
方法中的Canvas传递给Decoration#onDraw
,也就是说我们在Decoration中拿到了整个 RecyclerView 的 Canvas,那么我们基本就可以随意绘制了,但是我们使用中会发现,我们绘制的区域如果在 ItemView 的范围内就会被盖住,这是为什么呢?
由于View的绘制是先执行 draw(Canvas)
再到onDraw(Canvas)
的,我们复习一波自定义View的知识,看下View的绘制流程:#View.java
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas); //注释1
// Step 4, draw the children
dispatchDraw(canvas); //注释2
...
// we're done...
return;
}
}
我们直接看注释1与注释2那段,可以看到,View的绘制是先绘制自身(onDraw调用),然后再绘制child,所以我们在 Decoration#onDraw
中绘制的界面会被 ItemView 遮挡也是理所当然了。
所以我们在绘制中就要计算好绘制的范围,使绘制范围在上面彩图中蓝色区域内,即getItemOffsets
设置的范围内,避免没有显示或者过分绘制的情况。
4.onDrawOver
public void onDrawOver(Canvas c, RecyclerView parent, State state) {onDrawOver(c, parent);
}
/**
* @deprecated
* Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDrawOver(Canvas c, RecyclerView parent) {
}
onDrawOver
跟onDraw
非常类似,也是两个重载,一个被弃用了,看名称我们就基本能知道这个方法的用途,它是用于补充 onDraw
的一个方法,由于onDraw
会被 ItemView 覆盖,所以我们想要绘制一些漂浮在RecyclerView顶层的装饰就无法实现,所以就有了这个方法,他是在 ItemView 绘制完毕后才会被调用的,看下源码的实现:#RecyclerView.java
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
...
}
super.draw(c)
就是我们在上面分析的View#draw(Canvas)
方法,会调用一系列的绘制流程,包括onDraw
(ItemDecoration的onDraw)以及dispatchDraw
(ItemView的绘制),走完这些流程后才会调用Decoration#onDrawOver
方法.
到此,我们就可以得出 onDraw
>dispatchDraw
(ItemView的绘制)>onDrawOver
的执行流程。
5. 总结
getItemOffsets
用于提供一些空间(类似Margin)给onDraw
绘制onDraw
方法绘制的内容如果在 ItemView 的区域则可能被覆盖(没效果)onDraw
>dispatchDraw
(ItemView的绘制)>onDrawOver
从左到右执行
三 实战
实战将会从易到难进行几个小的Demo练习。由于这篇文章内容已经比较充实了,就把实战部分放到下篇讲解。
感谢你的阅读,由于水平有限,如有错误恳请提醒。
—————END—————
我是南尘,只做比心的公众号,欢迎关注我。
推荐阅读:
欢迎关注南尘的公众号:nanchen
做不完的开源,写不完的矫情,只做比心的公众号,如果你喜欢,你可以选择分享给大家。如果你有好的文章,欢迎投稿,让我们一起来分享。