Android RecyclerView

RecyclerView 是 Android 5.0 提出的新 UI 控件,用来替代 ListView。

RecyclerView 官方定义如下:

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

重要类介绍

LayoutManager 布局管理器

LayoutManager 负责 RecyclerView 的布局。LayoutManager 是一个抽象类,SDK 实现有 LinearLayoutManagerGridLayoutManagerStaggeredGridLayoutManager,分别是线性布局、网格布局和瀑布流布局。

LayoutManager#onLayoutChildren() 是对 RecyclerView 进行布局的入口方法。LinearLayoutManager 的核心实现如下:

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
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//将原来所有的 ItemView 全部放到 Recycler 的 ScrapHeap 或 RecyclePool
detachAndScrapAttachedViews(recycler);
// 填充现在所有的 ItemView
fill(recycler, mLayoutState, state, false);
}

//对剩余空间不断地调用 layoutChunk(),直到填充完为止
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);
}
}

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//从缓存中获取 ItemView
View view = layoutState.next(recycler);
if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
measureChildWithMargins(view, 0, 0);//计算View的大小
//计算 4 个边距
if (mOrientation == VERTICAL) {
// 垂直排列
} else {
// 水平排列
}
// 通知子 View 布局
layoutDecoratedWithMargins(view, left, top, right, bottom);//布局View
}

ViewHolder

负责 itemView 中 childView 的管理。

1
2
3
4
5
6
7
public static class VH extends RecyclerView.ViewHolder {
private TextView mTextView;
public VH(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(android.R.id.text1);
}
}

Adapter

负责创建视图并把指定位置上的数据填充到 View 上。

1
public abstract static class Adapter<VH extends ViewHolder>
  • notifyDataSetChanged()
  • notifyItemChanged()/notifyItemRangeChanged()
  • notifyItemInserted()/notifyItemRangeInserted()
  • notifyItemMoved()/notifyItemRangeRemoved()
  • notifyItemRangeChanged()

ItemAnimator 动画

负责处理数据添加或者删除时的动画效果。
SimpleItemAnimator 是 ItemAnimator 的子类,也是抽象类。DefaultItemAnimator 是 SimpleItemAnimator 的子类,是 SDK 提供的 RecycleView 默认动画。

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
public class RecyclerView {
ItemAnimator mItemAnimator = new DefaultItemAnimator();

private Runnable mItemAnimatorRunner = new Runnable() {
@Override
public void run() {
//通过 runPendingAnimations() 方法执行动画
mItemAnimator.runPendingAnimations();
}
};

//调用动画 Runnable
void postAnimationRunner() {
ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
}

public void setItemAnimator(ItemAnimator animator) {
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
mItemAnimator.setListener(null);
}
mItemAnimator = animator;
if (mItemAnimator != null) {
mItemAnimator.setListener(mItemAnimatorListener);
}
}

/**
* This class defines the animations that take place on items as changes are made
* to the adapter.
*/
public abstract static class ItemAnimator {
//当ViewHolder在屏幕上消失时被调用(可能是remove或move)
public abstract boolean animateDisappearance(ViewHolder viewHolder,
ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo);

//当ViewHolder在屏幕上出现时被调用(可能是add或move)
public abstract boolean animateAppearance(ViewHolder viewHolder,
ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo);

//在没调用notifyItemChanged()和notifyDataSetChanged()的情况下布局发生改变时被调用
public abstract boolean animatePersistence(ViewHolder viewHolder,
ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo);

//在显式调用notifyItemChanged()或notifyDataSetChanged()时被调用。
public abstract boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo);

/*RecyclerView 动画的执行方式并不是立即执行,而是每帧执行一次。
比如两帧之间添加了多个Item,则会将这些将要执行的动画保存在成员变量中,等到下一帧一起执行。
该方法执行的前提是前面animateXxx()返回true。
*/
public abstract void runPendingAnimations();
}
}
  • RecyclerView 默认使用的是 DefaultItemAnimator 动画,也可以通过 setItemAnimator() 设置自定义动画。
  • ItemAnimator 是抽象类,定义了动画相关的抽象方法。
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
/**
* A wrapper class for ItemAnimator that records View bounds and decides whether it should run
* move, change, add or remove animations. This class also replicates the original ItemAnimator
* API.
*
* It uses RecyclerView.ItemAnimator.ItemHolderInfo to track the bounds information of the Views. If you would like
* to extend this class, you can override obtainHolderInfo() method to provide your own info
* class that extends RecyclerView.ItemAnimator.ItemHolderInfo.
*/
public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator {
//当Item删除时被调用
public abstract boolean animateRemove(RecyclerView.ViewHolder holder);
//当Item添加时被调用
public abstract boolean animateAdd(RecyclerView.ViewHolder holder);
//当Item移动时被调用
public abstract boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY);
//当显式调用notifyItemChanged()或notifyDataSetChanged()时被调用
public abstract boolean animateChange(RecyclerView.ViewHolder oldHolder,
RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop);

//动画开始前需要调用
public final void dispatchXxxStarting(RecyclerView.ViewHolder item) {}
//动画结束后需要调用
public final void dispatchXxxFinished(RecyclerView.ViewHolder item) {}
}

SimpleItemAnimator 对 ItemAnimator 进行了封装,提供了更简洁、更容易理解的 Api。

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
public class DefaultItemAnimator extends SimpleItemAnimator {

@Override
public boolean animateAdd(final RecyclerView.ViewHolder holder) {
resetAnimation(holder);
holder.itemView.setAlpha(0);
mPendingAdditions.add(holder);
return true;
}

void animateAddImpl(final RecyclerView.ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimator animation = view.animate();
mAddAnimations.add(holder);
animation.alpha(1).setDuration(getAddDuration())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchAddStarting(holder);//动画开始前调用
}

@Override
public void onAnimationCancel(Animator animator) {
view.setAlpha(1);
}

@Override
public void onAnimationEnd(Animator animator) {
animation.setListener(null);
dispatchAddFinished(holder);//动画结束后调用
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();//所有动画结束后回调,通知监听器
}
}).start();
}

@Override
public void runPendingAnimations() {
/*
1. 判断 Pending 集合是否为空
2. 执行 Remove 动画
3. 同时执行 Move 和 Change 动画
4. 执行 Add 动画
*/

// First, remove stuff
for (RecyclerView.ViewHolder holder : mPendingRemovals) {
animateRemoveImpl(holder);
}
// Next, move stuff
if (movesPending) {
// 复制数据
final ArrayList<MoveInfo> moves = new ArrayList<>();
moves.addAll(mPendingMoves);
Runnable mover = new Runnable() {
@Override
public void run() {
for (MoveInfo moveInfo : moves) {
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
moveInfo.toX, moveInfo.toY);
}
}
};
if (removalsPending) {
View view = moves.get(0).holder.itemView;
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
} else {
mover.run();
}
}
// Next, change stuff, to run in parallel with move animations
if (changesPending) {
// 同 Move 动画类似
}
// Next, add stuff
if (additionsPending) {
if (removalsPending || movesPending || changesPending) {
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
//等remove,move,change动画全部做完后,开始执行add动画
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
adder.run();
}
}
}
}

RecyclerView 动画三方库

ItemDecoration 装饰

负责给 Item 添加额外的操作,例如画分隔线。ItemDecoration 可以添加多个,保存在集合中。

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
public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
final Rect mDecorInsets = new Rect();
}

Rect getItemDecorInsetsForChild(View child) {
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);//重置 rect
//inset 值保存到 mTempRect 中
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;
}

public abstract static class ItemDecoration {
public void onDraw(Canvas c, RecyclerView parent, State state) {
//绘制底层内容
}

public void onDrawOver(Canvas c, RecyclerView parent, State state) {
//绘制顶层内容
}

public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
// 通过 outRect 给 itemView 设置 insets。
}
}

public void layoutDecoratedWithMargins(View child, int left, int top, int right, int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = lp.mDecorInsets;
//正值:ItemView 宽度变小,Item 之间有边距;负值:ItemView 宽度变大,Item 之间重合
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}

@Override
public void draw(Canvas c) {
super.draw(c);

//因为是在 draw() 方法后,所以最后才绘制,显示在最顶层。
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
// 绘制顶层装饰
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
}

@Override
public void onDraw(Canvas c) {
// 调用 onDraw() -> dispatchDraw(canvas) 绘制 child
super.onDraw(c);

final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
// 绘制底层装饰
mItemDecorations.get(i).onDraw(c, this, mState);
}
}

Item 滑动删除和拖拽

Android 提供了 ItemTouchHelper 类,使得 RecyclerView 能够轻易地实现滑动和拖拽。

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
/**
* This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.
*/
public class ItemTouchHelper extends RecyclerView.ItemDecoration
implements RecyclerView.OnChildAttachStateChangeListener {

//默认长按 Item 拖拽,可以通过此方法自定义拖拽功能
public void startDrag(ViewHolder viewHolder) {}

/**
This class is the contract between ItemTouchHelper and your application.
It lets you control which touch behaviors are enabled per each ViewHolder and
also receive callbacks when user performs these actions.
*/
public abstract static class Callback {

//设置拖拽和滑动方向的标记
public abstract int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)

//拖拽回调
public abstract boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target);

//滑动回调
public abstract void onSwiped(ViewHolder viewHolder, int direction);

// ViewHolder 状态改变回调。一共有三种状态:ACTION_STATE_IDLE、ACTION_STATE_SWIPE 和 ACTION_STATE_DRAG
public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
ItemTouchUIUtilImpl.INSTANCE.onSelected(viewHolder.itemView);
}

//拖拽结束后回调
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
ItemTouchUIUtilImpl.INSTANCE.clearView(viewHolder.itemView);
}

public boolean isLongPressDragEnabled() {
return true;
}
}
}

示例:

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
public class SimpleItemTouchCallback extends ItemTouchHelper.Callback {

private MyAdapter mAdapter;
private List<ObjectModel> mData;

public SimpleItemTouchCallback(MyAdapter adapter, List<ObjectModel> data) {
mAdapter = adapter;
mData = data;
}

@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN; //上下拖拽
int swipeFlag = ItemTouchHelper.START | ItemTouchHelper.END; //左右滑动
return makeMovementFlags(dragFlag, swipeFlag);
}

@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
Collections.swap(mData, from, to);
mAdapter.notifyItemMoved(from, to);
return true;
}

@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int pos = viewHolder.getAdapterPosition();
mData.remove(pos);
mAdapter.notifyItemRemoved(pos);
}

@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
viewHolder.itemView.setBackgroundColor(Color.BLACK);
}
}

@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.setBackgroundColor(Color.White);
}
}

ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SimpleItemTouchCallback(adapter, data));
itemTouchHelper.attachToRecyclerView(mRecyclerView);

缓存机制

RecyclerView-Cache-Mechanism
RecyclerView-Cache-Mechanism

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
// RecyclerView.java
/**
* A Recycler is responsible for managing scrapped or detached item views for reuse.
*
* A "scrapped" view is a view that is still attached to its parent RecyclerView but
* that has been marked for removal or reuse.
*
* Typical use of a Recycler by a LayoutManager will be to obtain views for
* an adapter's data set representing the data at a given position or item ID.
* If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
* If not, the view can be quickly reused by the LayoutManager with no further work.
* Clean views that have not requested layout
* may be repositioned by a LayoutManager without remeasurement.
*/
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;

final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
return holder;
}
}

RecyclerView 缓存的是 RecyclerView.ViewHolder。内部实现了四级缓存:

  • mAttachedScrap 和 mChangedScrap:第一级缓存,用来保存被 RecycledView 移除掉但最近又马上要使用的缓存,比如说 RecycledView 中自带 item 的动画效果。
  • mCachedViews:第二级缓存,缓存屏幕外的 ViewHolder,默认为 2 个。
  • mViewCacheExtension: 第三级缓存,自定义的缓存,默认不实现。
  • mRecyclerPool: 第四级缓存,缓存池,多个 RecyclerView 共用。

LinearLayoutManager 在 fill() 方法中获取缓存的 ViewHolder 进行填充。具体方法是 View view = layoutState.next(recycler) 源码如下:

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
static class LayoutState {
View next(RecyclerView.Recycler recycler) {
final View view = recycler.getViewForPosition(mCurrentPosition);
return view;
}


public final class Recycler {
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
ViewHolder holder = null;
//1. 从 mAttachedScrap, mCachedViews 中获取
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);

if (holder == null) {
final int type = mAdapter.getItemViewType(offsetPosition);
//默认为false
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);


//2. 从 ViewCacheExtention 获取,默认不实现
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type);
}
//3. 从 RecycledViewPool 中获取
if (holder == null) {
holder = getRecycledViewPool().getRecycledView(type);
}
//4. 最后通过 Adapter 创建 ViewHolder
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
tryBindViewHolderByDeadline(...)



private boolean tryBindViewHolderByDeadline(...){
mAdapter.bindViewHolder(holder, offsetPosition);
}

从上述实现可以看出,依次从 mAttachedScrap, mCachedViews, mViewCacheExtension, mRecyclerPool 中寻找可复用的 ViewHolder。

  1. 如果是从 mAttachedScrap 或 mCachedViews 中获取的 ViewHolder,则不会调用 onCreateViewHolder()创建方法,只会调用onBindViewHolder()绑定数据。
  2. 如果从 mViewCacheExtension 或 mRecyclerPool 中获取的 ViewHolder,则会调用 onBindViewHolder()。
  • mAttachedScrap: 不参与滑动时的回收复用,只保存重新布局时从RecyclerView分离的item的无效、未移除、未更新的holder。因为RecyclerView在onLayout的时候,会先把children全部移除掉,再重新添加进入,mAttachedScrap临时保存这些holder复用。
  • mChangedScrap: mChangedScrap和mAttachedScrap类似,不参与滑动时的回收复用,只是用作临时保存的变量,它只会负责保存重新布局时发生变化的item的无效、未移除的holder,那么会重走adapter绑定数据的方法。
  • mCachedViews: 用于保存最新被移除(remove)的ViewHolder,已经和RecyclerView分离的视图;它的作用是滚动的回收复用时如果需要新的ViewHolder时,精准匹配(根据position/id判断)是不是原来被移除的那个item;如果是,则直接返回ViewHolder使用,不需要重新绑定数据;如果不是则不返回,再去mRecyclerPool中找holder实例返回,并重新绑定数据。这一级的缓存是有容量限制的,最大数量为2。
  • mViewCacheExtension: RecyclerView给开发者预留的缓存池,开发者可以自己拓展回收池,一般不会用到,用RecyclerView系统自带的已经足够了。
  • mRecyclerPool: 是一个终极回收站,真正存放着被标识废弃(其他池都不愿意回收)的ViewHolder的缓存池,如果上述mAttachedScrap、mChangedScrap、mCachedViews、mViewCacheExtension都找不到ViewHolder的情况下,就会从mRecyclerPool返回一个废弃的ViewHolder实例,但是这里的ViewHolder是已经被抹除数据的,没有任何绑定的痕迹,需要重新绑定数据。它是根据itemType来存储的,是以SparseArray嵌套一个ArraryList的形式保存ViewHolder的。

ListView 缓存机制:

ListView 缓存
RecyclerView 缓存

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
public abstract class AbsListView {
/**
The RecycleBin facilitates reuse of views across layouts.
The RecycleBin has two levels of storage: ActiveViews and ScrapViews.
ActiveViews are those views which were onscreen at the start of a layout. By construction, they are displaying current information.
At the end of layout, all views in ActiveViews are demoted to ScrapViews.
ScrapViews are old views that could potentially be used by the adapter to avoid allocating views unnecessarily.
*/
class RecycleBin {
//存储屏幕上的View
private View[] mActiveViews = new View[0];
//每类 ViewType 对应一个 ArrayList
private ArrayList<View>[] mScrapViews;
//ViewType 的数量为 1 时的集合
private ArrayList<View> mCurrentScrap;

void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
final int viewType = lp.viewType;
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
}

/**
* @return A view from the ScrapViews collection. These are unordered.
*/
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}

//从回收列表中取出最后一个元素复用
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
final View scrap = scrapViews.remove(size - 1);
return scrap;
} else {
return null;
}
}

/**
* Move all views remaining in mActiveViews to mScrapViews.
*/
void scrapActiveViews() { }

View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >=0 && index < activeViews.length) {
final View match = activeViews[index];
activeViews[index] = null;
return match;
}
return null;
}
}

/**
* Subclasses must override this method to layout their children.
*/
protected void layoutChildren() {}

View obtainView(int position, boolean[] outMetadata) {
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
return child;
}
}

public class ListView extends AbsListView {
@Override
protected void layoutChildren() {
//如果数据被改变了,则将所有 ItemView 回收至 scrapView
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
//根据 mLayoutMode 执行不同的填充 fillXxx() 方法

//回收多余的activeView
mRecycler.scrapActiveViews();
}

private View fillSpecific(int position, int top) {
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
}

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) {
if (!mDataChanged) {
// Try to use an existing view for this position.
//可以直接复用指定位置的 View
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
// Found it. We're reusing an existing child, so it just needs
// to be positioned like a scrap view.
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}

// Make a new view for this position, or convert an unused view if
// possible.
//从缓存或 Adapter 获取
final View child = obtainView(position, mIsScrap);

// This needs to be positioned and measured.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

return child;
}
}

ListView 和 RecyclerView 缓存机制基本一致:

  1. mActiveViews 和 mAttachedScrap 功能相似,意义在于快速重用屏幕上可见的 ItemView,而不需要重新 createView 和 bindView;
  2. mScrapView 和 mCachedViews + mReyclerViewPool 功能相似,意义在于缓存离开屏幕的 ItemView,目的是让即将进入屏幕的 ItemView 重用;
  3. RecyclerView 的优势在于:
    • mCacheViews 的使用,可以做到屏幕外的列表项 ItemView 进入屏幕内时也无须 bindView 快速重用;
    • mRecyclerPool 可以供多个 RecyclerView 共同使用。在特定场景下,如 viewpaper+多个列表页下有优势.客观来说,RecyclerView 在特定场景下对 ListView 的缓存机制做了补强和完善。

RecyclerView 优化

预取功能 (Prefetch)

为了充分利用 CPU,当 CPU 空闲时 RecyclerView 会预取接下来可能要显示的 item,在下一帧到来之前提前处理完数据,然后将得到的 itemholder 缓存起来,等到真正要使用的时候直接从缓存取出来即可。

LinearLayoutManager#setInitialPrefetchItemCount(int itemCount)

1
2
3
4
5
6
7
8
9
10
11
// RecycledView.java
@Override
public boolean onTouchEvent(MotionEvent e) {
case MotionEvent.ACTION_MOVE: {
if (mScrollState == SCROLL_STATE_DRAGGING) {
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
}
}

根据创建同类型的 ViewHolder 所需要的时间来判断。

setHasFixedSize(true)

RecyclerView 的大小不会因数据集的更新而改变时设置为 ture, 当添加和删除 item 时不会重新 requestLayout()。

stackoverflow 回答如下:

RecyclerView size changes every time you add something no matter what. What setHasFixedSize does is that it makes sure (by user input) that this change of size of RecyclerView is constant. The height (or width) of the item won’t change. Every item added or removed will be the same. If you dont set this it will check if the size of the item has changed and thats expensive. Just clarifying because this answer is confusing. – ArnoldB May 25 at 18:42

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* RecyclerView can perform several optimizations if it can know in advance that RecyclerView's
* size is not affected by the adapter contents. RecyclerView can still change its size based
* on other factors (e.g. its parent's size) but this size calculation cannot depend on the
* size of its children or contents of its adapter (except the number of items in the adapter).
*
* If your use of RecyclerView falls into this category, set this to {@code true}. It will allow
* RecyclerView to avoid invalidating the whole layout when its adapter contents change.
*
* @param hasFixedSize true if adapter changes cannot affect the size of the RecyclerView.
*/
public void setHasFixedSize(boolean hasFixedSize) {
mHasFixedSize = hasFixedSize;
}
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
public class RecyclerView{
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
// LinearLayoutManager GridLayoutManager 重写为 true
if (mLayout.isAutoMeasureEnabled()) {
// ...
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
}
}

}

private class RecyclerViewDataObserver extends AdapterDataObserver {
@Override
public void onChanged() {
requestLayout();
}

@Override
public void onItemRangeXXX(int positionStart, int itemCount, Object payload) {
triggerUpdateProcessor();
}

void triggerUpdateProcessor() {
// >= 4.1 && mHasFixedSize 为 true && RecyclerView attach 到 Window
if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
requestLayout();
}
}
}

swapAdapter()

setAdapter() 会直接清空 RecycledView 上的所有缓存,而 swapAdapter() 会重用 ViewHolder,而且不会清空 RecycledViewPool。适用于两个数据集大致相同的情况。

获取 ViewHolder 的位置

  • findViewHolderForPosition()
  • findViewHolderForAdapterPosition()
  • findViewHolderForLayoutPosition()

RecyclerView 拓展

添加 Item 点击事件

添加 HeaderView 和 FooterView

添加 EmptyView

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
public class EmptyRecyclerView extends RecyclerView {
private RecyclerView.AdapterDataObserver mObserver = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter.getItemCount() == 0) {
mEmptyView.setVisibility(View.VISIBLE);
setVisibility(View.GONE);
} else {
mEmptyView.setVisibility(View.GONE);
setVisibility(View.VISIBLE);
}
}
};

public void setAdapter(RecyclerView.Adapter adapter){
super.setAdapter(adapter);
adapter.registerAdapterDataObserver(mObserver);
mObserver.onChanged();
}

public void setEmptyView(View view){
mEmptyView = view;
((ViewGroup)getRootView()).addView(mEmptyView);
}
}

嵌套滑动机制

遇到的问题

RecyclerView 嵌套 ScrollView

滑动冲突:

  1. ScrollView 换成 NestedScrollView;
  2. 设置 RecyclerView#setNestedScrollingEnabled(false);

RecyclerView#setHasFixedSize(boolean hasFixedSize):

  • 不嵌套 NestedScrollView
    不管设置 true 或 false 都会根据屏幕显示进行渲染。

  • 嵌套 NestedScrollView

    1. 不管设置 true 或 false 都会渲染全部数据。
    2. RecyclerView.ViewHolder 无法复用。

notifyDataSetChanged() 和 notifyItemInserted():

  • notifyDataSetChanged() 会渲染整个数据集。
  • notifyItemInserted() 不嵌套 NestedScrollView 时,在屏幕内才渲染。嵌套 NestedScrollView 时,添加数据时就会渲染,不管新添加的数据在不在屏幕内。但是滑动到屏幕内时就不再渲染了。
  • 当 Adapter 添加新数据时 notifyDataSetChanged() 会创建新的 ViewHolder,而 notifyItemInserted() 不会

notifyItemInserted() 不显示数据问题:

设置 RecyclerView#setHasFixedSize(false),重新测量 RecyclerView 的大小。

RecyclerView.OnScrollListener 不回调问题:

  1. ScrollView 和 RecycleView 都是垂直方向
    • 如果 RecyclerView.setNestedScrollingEnabled(false),RecyclerView.OnScrollListener 接收不到通知。
    • 如果 RecyclerView.setNestedScrollingEnabled(true),onScrolled() 只回调一次,onScrollStateChanged() 在一定方向上每次会回调一次,但是再反向滚动回去就不会回调了。
  2. ScrollView 垂直方向, RecycleView 横向。没有问题.
  3. HorizontalScrollView(横向)嵌套 RecyclerView(横向),RecycleView 不能滚动。

CoordinatorLayout + AppBarLayout + RecyclerView,滚动到最底部延迟问题

material 包从某个升级引入了惯性下滑的概念。滑动的速度越快,下滑的时间越长,就会导致 RecyclerView 实际上已经滑动到最底部了,但是还是卡在了下滑的过程之中,等惯性下滑的触发结束以后,才会触发 RecyclerView#onScrollStateChanged()。
用户体验上就是 RecyclerView 明明已经滑动到最底部,应该显示加载中去获取下一页数据了,却一直卡着不动。需要等一小会才加载数据。

解决方法是自定义一个 behavior 类继承 AppBarLayout.Behavior,加速结束惯性下滑的回调,当滑动到顶部或到底就认为滑动结束。

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
public class ScrollAppBarLayoutBehavior extends AppBarLayout.Behavior {
public ScrollAppBarLayoutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dx, int dy, int[] consumed, int type) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
stopNestedScrollIfNeeded(dy, child, target, type);
}

@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target,
int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, int[] consumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
stopNestedScrollIfNeeded(dyUnconsumed, child, target, type);
}

private void stopNestedScrollIfNeeded(int dy, AppBarLayout child, View target, int type) {
if (type == ViewCompat.TYPE_NON_TOUCH) {
final int currOffset = getTopAndBottomOffset();
if ((dy < 0 && currOffset == 0) || (dy > 0 && currOffset == -child.getTotalScrollRange())) {
ViewCompat.stopNestedScroll(target, ViewCompat.TYPE_NON_TOUCH);
}
}
}
}

java.lang.IllegalArgumentException: Called attach on a child which is not detached

报错日志如下:

1
2
java.lang.IllegalArgumentException: Called attach on a child which is not detached: c419ef6e position=11 id=-1, oldPos=-1, pLpos:-1
...

出现这个问题的原因是更新了不在屏幕中显示的 item。操作的这个 ViewHolder 当前不是被绑定的,因为 RecyclerView 有缓存机制,未在屏幕上显示的 item 会被暂时回收,即 detached。

出现这个问题的原因是更新了不在屏幕中显示的 item。解决办法是判断要更新的 item 是不是在屏幕中,判断方法是获取 RecyclerView 的 LayoutManager,前提是 RecyclerView 设置的 LayoutManager 是 LinearLayoutManager。获取第一个可见位置和最后一个可见位置的 position,判断当前要更新的 item 的 position 在这个范围内才更新。

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
// RecyclerView.java
@Override
public void attachViewToParent(View child, int index,
ViewGroup.LayoutParams layoutParams) {
final ViewHolder vh = getChildViewHolderInt(child);
if (vh != null) {
if (!vh.isTmpDetached() && !vh.shouldIgnore()) {
throw new IllegalArgumentException("Called attach on a child which is not"
+ " detached: " + vh + exceptionLabel());
}
vh.clearTmpDetachFlag();
}
RecyclerView.this.attachViewToParent(child, index, layoutParams);
}

@Override
public void detachViewFromParent(int offset) {
final View view = getChildAt(offset);
if (view != null) {
final ViewHolder vh = getChildViewHolderInt(view);
if (vh != null) {
if (vh.isTmpDetached() && !vh.shouldIgnore()) {
throw new IllegalArgumentException("called detach on an already"
+ " detached child " + vh + exceptionLabel());
}
vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
}
}
RecyclerView.this.detachViewFromParent(offset);
}

相关工具类

DiffUtil

SortedList

适用于列表有序的场景(城市列表页,中文首字母排序)。并且 SortedList 会帮助你比较数据的差异,定向刷新数据,而不是简单粗暴的 notifyDataSetChanged()。

AsyncListUtil

用于异步加载数据,我们无需在 UI 线程上查询游标,同时它可以保持 UI 和缓存同步,并且始终只在内存中保留有限数量的数据。使用它可以获得更好的用户体验。
这个类使用单线程来加载数据,因此它适合从磁盘、数据库加载数据,不适用于从网络加载数据。

AsyncListDiffer

SnapHelper

用于辅助 RecyclerView 在滚动结束时将 Item 对齐到某个位置。

参考

[1] Developer Docs
[2] 【腾讯 Bugly 干货分享】Android ListView 与 RecyclerView 对比浅析—缓存机制
[3] 腾讯 BuglyRecyclerView 必知必会