Android RecyclerView (二)

前言

Linus Benedict Torvalds : RTFSC – Read The Funning Source Code

概述

A flexible view for providing a limited window into a large data set.

前面一章我们已经理解了RecyclerView的特性和简单的使用。总体来说可以上手写代码了。这章开始我们将会更加深入的学习RecyclerView。

ItemTouchHelper

This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView. 翻译:这是一个工具类来给予 RecyclerView 添加拖动排序与滑动删除的支持。

简单使用

首先得要创建一个 ItemTouchHelper.Callback 的子类。这个接口主要是用于监听 “move” 和 “swipe” 事件。还有控制view被选中的状态以及重写默认动画的功能。

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
public class TagTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public boolean isLongPressDragEnabled() {return true;}
@Override
public boolean isItemViewSwipeEnabled() {return false;}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN |
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
int swipeFlags = 0;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {}
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (viewHolder instanceof ITodoTagTouchHelperViewHolder) {
ITagTouchHelperViewHolder itemViewHolder = (ITagTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemFinish();
}
super.clearView(recyclerView, viewHolder);
}
}

isLongPressDragEnabled(): Returns whether ItemTouchHelper should start a drag and drop operation if an item is long pressed.

isItemViewSwipeEnabled(): Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped over the View.

getMovementFlags(…): Should return a composite flag which defines the enabled move directions in each state(idle, swiping, dragging). 这函数可以指定支持的拖动和滑动的方向。在设定完支持的方向后通过makeMovementFlags(…)函数来构造返回的flag。

onMove(…): Called when ItemTouchHelper wants to move the dragged item from its old position to the new position.当拖动项目从一个旧的地方到新的地方时被调用。

onSwiped(…): Called when a ViewHolder is swiped by the user.当项目被滑动时调用。

onMove()和onSwiped()是用于通知底层数据的更新。用法主要是通过接口传给外部去调用。
在上面class TagTouchHelperCallback类中传入我们的接口:

1
2
3
4
public interface ItemTouchHelperAdapter {
void onItemMove(int fromPosition, int toPosition);
void onItemDismiss(int position);
}

通过继承接口来实现我们的方法功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class RecyclerListAdapter extends RecyclerView.Adapter<ItemViewHolder>
implements ItemTouchHelperAdapter {
@Override
public void onItemDismiss(int position) {
mItems.remove(position);
notifyItemRemoved(position);
}
@Override
public void onItemMove(int from, int to) {
Collections.swap(mItems, from, to);
notifyItemMoved(from, to);
}

通过调用notifyItemRemoved()notifyItemMoved()函数 告知Adapter发生了改变。

最后再将创建好的Helper绑定给我们的RecyclerView:

1
2
3
ItemTouchHelper.Callback callback = new TagTouchHelperCallback(mTagControlAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(recyclerView);

设定拖动事件

有时候我们需要点击某个固定的控件才开始拖动而不是马上就拖动,这样我们需要学习设定一个拖动的事件。

首先我们需要一个监听类:

1
2
3
public interface ITagItemDragListener {
void onStartDrag(RecyclerView.ViewHolder viewHolder);
}

这里最关键的是参数viewHolder,它要传递接下来移动的控件到底是哪一个。

然后我们在Adapter的拖动控件里增加监听按键回调并且在你想要的时机调用onStartDrag事件:

1
2
3
4
5
6
7
8
9
10
holder.rlItem.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (iTagItemDragListener != null && position != 0 &&
MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
iTagItemDragListener.onStartDrag(holder);
}
return true;
}
});

最后就是调用ItemTouchHelper通知拖动开始:

1
2
3
4
@Override
public void onStartDrag(RecyclerView.ViewHolder viewHolder) {
itemTouchHelper.startDrag(viewHolder);
}

拖动动画

我们有时候需要在拖动动画或者停止动画上做些自定义,这时候主要需要的就是得知事件监听了。

在继承ItemTouchHelper.Callback后我们重载两个函数:

1
2
3
4
5
// Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed.
public void onSelectedChanged(ViewHolder viewHolder, int actionState);
// Called by the ItemTouchHelper when the user interaction with an element is over and it also completed its animation.
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder);

onSelectedChanged:当item选中事件有更改的时候会发送通知。
clearView:当拖动或者滑动动画结束时进行通知。

通过这两个函数大家可以自定义拖动或者滑动的动画效果了。

LayoutManager原理

LayoutManager主要作用是,测量和摆放RecyclerView中itemView,以及当itemView对用户不可见时循环复用处理。 通过设置Layout Manager的属性,可以实现水平滚动、垂直滚动、方块表格等列表形式。其内部类Properties包含了所需要的大部分属性。

我们知道RecyclerView的本质就是是一个ViewGroup,既然是一个View当然就包含了onMeasure()onLayout()onDraw() 这三个方法。在RecyclerView中,onMeasure()onLayout()的工作统一交给了LayoutManager来完成,来达到更改LayoutManager就能够实现多种不同的效果。

onMeasure

RecyclerView中onMeasure()的调用可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
} else {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
}
}

其中mLayout就是我们的LayoutManager

但是在LayoutManager里面却又看到的是调回RecyclerView的defaultOnMeasure(...)函数:

1
2
3
4
5
6
7
8
9
10
11
12
void defaultOnMeasure(int widthSpec, int heightSpec) {
// calling LayoutManager here is not pretty but that API is already public and it is better
// than creating another method since this is internal.
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));
setMeasuredDimension(width, height);
}

里面的注释真是呵呵了。
chooseSize()方法是直接根据测量值和模式返回了最适大小。
接下来就是调用各个子控件的onMeasure:
setMeasuredDimension():This method must be called by onMeasure(int, int) to store the measured width and measured height. Failing to do so will trigger an exception at measurement time.

onLayout

RecyclerView中onLayout()的调用可以看到:

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
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
dispatchLayout();
}
void dispatchLayout() {
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||
mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
dispatchLayoutStep2();
}
dispatchLayoutStep3();
}
private void dispatchLayoutStep2() {
eatRequestLayout();
onEnterLayoutOrScroll();
// Step 2: Run layout
mLayout.onLayoutChildren(mRecycler, mState);
onExitLayoutOrScroll();
resumeRequestLayout(false);
}

onLayout函数里我们看到跟着调用链一直下去最后还是到了dispatchLayoutStep2()这个关键函数,里面最值得注意的就是onLayoutChildren(...)这个函数。同样它是在我们定义的LayoutManager里面被定义的。

这里我们拿LinearLayoutManager.onLayoutChildren为例:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create layout state
fill(recycler, mLayoutState, state, false);
}

里面函数很长,最核心的算法在前面注释里已经解释了,这里不做阐述。核心的函数fill(…):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Fills the given layout, defined by the layoutState. This is fairly independent from the rest of the LinearLayoutManager and with little change, can be made publicly available as a helper class.
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
}
}
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
addView(view);
}

fill(...):作用就是根据当前状态决定是应该从缓存池中取itemview填充 还是应该回收当前的itemview
layoutChunk():负责从缓存池 recycler 中取 itemview,并调用View.addView()将获取到的 ItemView 添加到 RecyclerView 中去,并调用 itemview 自身的 layout 方法去布局 item 位置。

onDraw

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);
}
}

在这里除了调用系统的onDraw外还调用了mItemDecorations.get(i).onDraw()的draw,它是用来画分割线的。

缓存机制

RecyclerView 自带了一套很完整的缓存机制,用于复用item的便利。

我们首先要关注的是一个叫Recycler的内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// A Recycler is responsible for managing scrapped or detached item views for reuse.
public final class Recycler {
// 未与RecyclerView分离的ViewHolder列表 。
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
// 与RecyclerView分离的ViewHolder列表。
ArrayList<ViewHolder> mChangedScrap = null;
// ViewHolder缓存列表。
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
// 提供复用ViewHolder池。
RecycledViewPool mRecyclerPool;
// 开发者控制的ViewHolder缓存。
private ViewCacheExtension mViewCacheExtension;
}

这里用了三级缓存来创建了一个大型的缓存器。

第一级缓存

当仍然依赖于RecyclerView的item(例如滑出视图但没被移除)在被标记移除的时候会被添加到mAttachedScrap中,在不再依赖是就会添加进mCachedViews中。

第二级缓存

ViewCacheExtension:当RecyclerView从第一级缓存中找不到时就会进入第二级缓存找,但这个ViewCacheExtension是个抽象类,主要方法需要开发者自己创建。

第三级缓存

mRecyclerPool:RecyclerView最核心的缓存池。RecyclerView 缓存的是 ViewHolder。而 ViewHolder 里面包含了一个 View 这也就是为什么在写 Adapter 的时候 必须继承一个固定的 ViewHolder 的原因。

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// RecycledViewPool lets you share Views between multiple RecyclerViews.
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
/**
* Tracks both pooled holders, as well as create/bind timing metadata for the given type.
* Note that this tracks running averages of create/bind time across all RecyclerViews (and, indirectly, Adapters) that use this pool.
* 1) This enables us to track average create and bind times across multiple adapters. Even though create (and especially bind) may behave differently for different Adapter subclasses, sharing the pool is a strong signal that they'll perform similarly, per type.
* 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return false for all other views of its type for the same deadline. This prevents items constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch.
*/
static class ScrapData {
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
private int mAttachCount = 0;
public void clear() {
for (int i = 0; i < mScrap.size(); i++) {
ScrapData data = mScrap.valueAt(i);
data.mScrapHeap.clear();
}
}
public void setMaxRecycledViews(int viewType, int max) {
ScrapData scrapData = getScrapDataForType(viewType);
scrapData.mMaxScrap = max;
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
if (scrapHeap != null) {
while (scrapHeap.size() > max) {
scrapHeap.remove(scrapHeap.size() - 1);
}
}
}
// Returns the current number of Views held by the RecycledViewPool of the given view type.
public int getRecycledViewCount(int viewType) {
return getScrapDataForType(viewType).mScrapHeap.size();
}
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}
int size() {
int count = 0;
for (int i = 0; i < mScrap.size(); i ++) {
ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i).mScrapHeap;
if (viewHolders != null) {
count += viewHolders.size();
}
}
return count;
}
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
long runningAverage(long oldAverage, long newValue) {
if (oldAverage == 0) {
return newValue;
}
return (oldAverage / 4 * 3) + (newValue / 4);
}
void factorInCreateTime(int viewType, long createTimeNs) {
ScrapData scrapData = getScrapDataForType(viewType);
scrapData.mCreateRunningAverageNs = runningAverage(
scrapData.mCreateRunningAverageNs, createTimeNs);
}
void factorInBindTime(int viewType, long bindTimeNs) {
ScrapData scrapData = getScrapDataForType(viewType);
scrapData.mBindRunningAverageNs = runningAverage(
scrapData.mBindRunningAverageNs, bindTimeNs);
}
boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) {
long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs;
return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
}
boolean willBindInTime(int viewType, long approxCurrentNs, long deadlineNs) {
long expectedDurationNs = getScrapDataForType(viewType).mBindRunningAverageNs;
return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
}
void attach(Adapter adapter) {
mAttachCount++;
}
void detach() {
mAttachCount--;
}
/**
* Detaches the old adapter and attaches the new one.
*
* RecycledViewPool will clear its cache if it has only one adapter attached and the new adapter uses a different ViewHolder than the oldAdapter.
*
* @param oldAdapter The previous adapter instance. Will be detached.
* @param newAdapter The new adapter instance. Will be attached.
* @param compatibleWithPrevious True if both oldAdapter and newAdapter are using the same
* ViewHolder and view types.
*/
void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
boolean compatibleWithPrevious) {
if (oldAdapter != null) {
detach();
}
if (!compatibleWithPrevious && mAttachCount == 0) {
clear();
}
if (newAdapter != null) {
attach(newAdapter);
}
}
private ScrapData getScrapDataForType(int viewType) {
ScrapData scrapData = mScrap.get(viewType);
if (scrapData == null) {
scrapData = new ScrapData();
mScrap.put(viewType, scrapData);
}
return scrapData;
}
}

作为一个比较值得看的缓存机制,直接贴代码,当作范例来读吧,不做过多解释。

总结

通过粗浅的分析了源码,这里学习的是怎么去思考一个view布局,还学到了比较优秀的缓存机制。 RecyclerView 固然比较优秀,但我们也可以模仿着为未来我们可能会写到自己的view做基础。