深入 RecyclerView 源码探究三:绘制和滑动

文章目录
  1. 1. 绘制过程
  2. 2. 滑动过程
  3. 3. 参考文献

If you keep on doing what you’ve always done, you’ll keep gettting what you’ve always got.

View 在测量和布局完成后,接下来开始执行绘制。本篇在深入 RecyclerView 源码探究二:测量和布局的基础上继续展开,着重分析 RecyclerView 的绘制和滑动过程。

绘制过程

深入 RecyclerView 源码探究一:宏观设计中简单提到此绘制过程,该部分将作较为详细的探究。RecyclerView 的绘制过程分两部分来看:其一,RecyclerView 负责 ItemDecoration 即分割线集合的绘制;其二,ViewGroup 负责 itemView 的绘制。绘制流程则是 Android 里常规的逻辑流程。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
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);
}
// ...
// If some views are animating, ItemDecorators are likely to move/change with them.
// Invalidate RecyclerView to re-draw decorators. This is still efficient because children's display lists are not invalidated.
if (!needsInvalidate && mItemAnimator != null && mItemDecorations.size() > 0 &&
mItemAnimator.isRunning()) {
needsInvalidate = true;
}

if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
1
2
3
4
5
6
7
8
9
@Override
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);
}
}

绘制 ItemDecoration 并不复杂,RecyclerView.ItemDecoration 的设计也十分灵活。在 RecyclerView 大小及位置确定的情况下,可以在其内部自定义绘制任何东西。这里,前文提到的 measureChildWithMargins() 方法的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;

// ...
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}

关乎父 RecyclerView 的 padding,任一添加的 ItemDecoration 和子控件边距,测量子控件采用标准的测量策略。再看里面的 getItemDecorInsetsForChild() 方法的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Rect getItemDecorInsetsForChild(View child) {
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;
}

其中,getItemOffsets() 方法的源码如下:

1
2
3
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(), parent);
}

该方法在实现一个 RecyclerView.ItemDecoration 时可以重写,目的是不让 itemView 挡住分割线。通过 mTempRect,能为每个 itemView 设置位置偏移量,该偏移量最终会参与计算 itemView 的大小,换句话说,itemView 的大小是包括此位置偏移量的。这里,有细致的地方是,itemView 本身绘制区域并不大,其留出的地方是透明的,于是 ItemDecoration 就透过 itemView 显示出来。更有意思的是,若 ItemDecoration 的偏移量设置为 0,则 itemView 会挡住 ItemDecoration,那么当 itemView 在增加或删除的时候,会短暂地消失(呈透明态),这时候就可以透过 itemView 看到 ItemDecoration 的样子(利用这个特点能做出一些有趣的动画效果)!在重写 getItemOffsets() 时,可以指定任意数值的偏移量,简图如下:

四个方向的位置偏移量分别和 mTempRect 的四个属性 (left, top, right, bottom) 一一对应。以 left offset 的值在水平线性布局中的应用举例来说,若 left offset 值为 0,则 itemView 之间无空隙;若 left offset 值大于 0,则 itemView 之间有空隙;若 left offset 小于 0,则 itemView 之间部分重叠。

事实上,在实现 RecyclerView.ItemDecoration 时,不一定要重写 getItemOffsets(),也不一定要重写 RecyclerView.ItemDecoration.onDraw() 或 RecyclerView.ItemDecoration.onDrawOver() 方法,这几种方法和设置位置偏移量之间无任何联系。如下实现一个 RecyclerView.ItemDecoration:在垂直线性布局下,绘制一条处于 itemView 之间、6 个像素宽、只有 itemView 一半长且与 itemView 居中对齐的红色分割线,分割线位于 itemView 内部 top 位置,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
Paint paint = new Paint();
paint.setColor(Color.RED);

for (int i = 0; i < parent.getLayoutManager().getChildCount(); i++) {
final View child = parent.getChildAt(i);

float left = child.getLeft() + (child.getRight() - child.getLeft()) / 4;
float top = child.getTop();
float right = left + (child.getRight() - child.getLeft()) / 2;
float bottom = top + 6;

c.drawRect(left, top, right, bottom, paint);
}
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.set(0, 0, 0, 0);
}

滑动过程

RecylerView 的滑动过程分两个阶段:其一为 scroll,即手指在屏幕上移动,使 RecyclerView 滑动;其二为 fling,即手指离开屏幕时,RecyclerView 会继续滑动一段距离。RecyclerView 触屏事件处理的 onTouchEvent() 方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@Override
public boolean onTouchEvent(MotionEvent e) {
// ...
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
boolean eventAddedToVelocityTracker = false;
// ...
switch (action) {
// ...
case MotionEvent.ACTION_MOVE: {
// ...
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
// ...
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}

if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];

if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
}
break;
// ...
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally ?
-VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
final float yvel = canScrollVertically ?
-VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
}
break;

case MotionEvent.ACTION_CANCEL: {
cancelTouch();
}
break;
}
// ...
}

以垂直方向的滑动举例说明,当手指开始滑动即 RecyclerView 接收到 ACTION_MOVE 事件后,首先会计算出手指移动距离 (dy),然后与滑动阀值 (mTouchSlop) 比较,若大于该阀值(默认为 8 个像素)时,将滑动状态置为 SCROLL_STATE_DRAGGING,最后调用到 scrollByInternal() 方法,该方法里调用了 LinearLayoutManager.scrollBy() 方法使得 RecyclerView 开始滑动,此为第一阶段的 scroll;当手指离开屏幕即 RecyclerView 接收到 ACTION_UP 事件后,会由之前的滑动距离和时间,通过 VelocityTracker 计算出一个速度 yvel,接下来调用方法 fling(),此为第二阶段的 fling。很显然,滑动过程中关键的两个方法即 scrollByInternal() 与 fling()。重点看 fling() 方法的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean fling(int velocityX, int velocityY) {
// ...
final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
final boolean canScrollVertical = mLayout.canScrollVertically();
// ...
if (!dispatchNestedPreFling(velocityX, velocityY)) {
// ...
if (canScroll) {
// ...
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}

标准的 fling 沿着坐标轴,应有着数个像素每秒的初始速度,若速度小于系统定义的最小速度,则不会触发 fling。源码中,mViewFlinger 是一个实现了 Runnable 接口的 ViewFlinger 对象,RecylerView 中 fling 过程的算法由其控制。先看 ViewFlinger 中下面方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void postOnAnimation() {
if (mEatRunOnAnimationRequest) {
mReSchedulePostAnimationCallback = true;
} else {
removeCallbacks(this);
ViewCompat.postOnAnimation(RecyclerView.this, this);
}
}

public void fling(int velocityX, int velocityY) {
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE,
Integer.MAX_VALUE);
postOnAnimation();
}

其中,postOnAnimation() 方法即在将来的某个时刻执行给定的实现 Runnable 接口 的 mViewFlinger 对象。此外,能清楚地看到,fling 是由 Scroller 实现的。重点看看 ViewFlinger 中的 run() 方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Override
public void run() {
// ...
// keep a local reference so that if it is changed during onAnimation method, it won't
// cause unexpected behaviors
final ScrollerCompat scroller = mScroller;
final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
if (scroller.computeScrollOffset()) {
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
final int dx = x - mLastFlingX;
final int dy = y - mLastFlingY;
// ...
if (mAdapter != null) {
// ...
if (dy != 0) {
vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
overscrollY = dy - vresult;
}
// ...
}
// ...
if (!awakenScrollBars()) {
invalidate();
}
// ...
if (scroller.isFinished() || !fullyConsumedAny) {
setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
} else {
postOnAnimation();
}
}
// call this after the onAnimation is complete not to have inconsistent callbacks etc.
if (smoothScroller != null) {
if (smoothScroller.isPendingInitialRun()) {
smoothScroller.onAnimation(0, 0);
}
if (!mReSchedulePostAnimationCallback) {
smoothScroller.stop(); //stop if it does not trigger any scroll
}
}
enableRunOnAnimationRequests();
}

代码中有个方法 mLayout.scrollVerticalBy(),其最终也会走到 LinearLayoutManager.scrollBy() 方法。显然,这里可以得到,RecyclerView 的滑动虽然分为两个阶段,最终都会走到一处,即有着同样的实现。再仔细看上面代码,其一,dy 即竖直滑动偏移量,即由 Scroller.fling() 根据开始时刻到当前时刻计算出来的,同理,若是 RecyclerView 的 scroll 阶段,则该滑动偏移量即手指滑动距离;其二,上面代码会多次执行,直到 Scroller 判断滑动结束或已滑到边界。注意,postOnAnimation() 保证 RecyclerView 流畅地滑动,这里,涉及到 Android View 里所谓的 “16ms” 机制,即上段代码会以 16ms 一次的速度执行,则 Scroller 每次计算的滑动偏移量是很小的一部分,此时,RecyclerView 会根据该偏移量确定是仅仅平移 itemView,还是除了平移外,另会创建新的 itemView。简单的示意图如下:

重点看看几次提到的 LinearLayoutManager.scrollBy() 方法,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
// ...
final int absDy = Math.abs(dy);
updateLayoutState(layoutDirection, absDy, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
// ...
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
mOrientationHelper.offsetChildren(-scrolled);
// ...
return scrolled;
}

其中,fill() 方法的作用即是向可绘制即滑动偏移量形成的区域填充 itemView,mOrientationHelper.offsetChildren() 的作用即是平移 itemView。

至此,关于 RecyclerView 的绘制和滑动过程探究完毕,最后一篇将从回收复用和动画机制方面展开探究。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.http://blog.csdn.net/qq_23012315/article/details/50807224