您的位置:首页 > 移动开发 > Android开发

Android 掌握自定义LayoutManager(二) 实现流式布局

2016-12-20 21:41 471 查看
转载请标明出处: 
http://blog.csdn.net/zxt0601/article/details/52956504 

本文出自:【张旭童的博客】

本系列文章相关代码传送门: 
自定义LayoutManager实现的流式布局 

欢迎star,pr,issue。

本系列文章目录: 
掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API。

掌握自定义LayoutManager(二) 实现流式布局


一 概述

在开始之前,我想说,如果需求是每个Item宽高一样,实现起来复杂度比每个Item宽高不一样的,要小10+倍。 

然而我们今天要实现的流式布局,恰巧就是至少每个Item的宽度不一样,所以在计算坐标的时候算的我死去活来。先看一下效果图: 


 

艾玛,换成妹子图后貌似好看了许多,我都不认识它了,好吧,项目里它一般长下面这样: 


 

往常这种效果,我们一般使用自定义ViewGroup实现,我以前也写了一个。自定义VG实现流式布局 

这不最近再研究自定义LayoutManager么,想来想去也没有好的创意,就先拿它开第一刀吧。 

(后话:流式布局Item宽度不一,不知不觉给自己挖了个大坑,造成拓展一些功能难度倍增,观之网上的DEMO,99%Item的大小都是一样的,so,这个系列的下一篇我计划 实现一个Item大小一样 的酷炫LayoutManager。但是最终做成啥样的效果还没想好,有朋友看到酷炫的效果可以告诉我,我去高仿一个。)


自定义LayoutManager的步骤:

以本文的流式布局为例,需求是一个垂直滚动的布局,子View以流式排列。先总结一下步骤:

一 实现 generateDefaultLayoutParams() 

二 实现 onLayoutChildren() 

三 竖直滚动需要 重写canScrollVertically()和scrollVerticallyBy()

下面我们就一步一步来吧。


二 实现generateDefaultLayoutParams()

如果没有特殊需求,大部分情况下,我们只需要如下重写该方法即可。
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
1
2
3
4
1
2
3
4

RecyclerView.LayoutParams
是继承自
Android.view.ViewGroup.MarginLayoutParams
的,所以可以方便的使用各种margin。

这个方法最终会在
recycler.getViewForPosition(i)
时调用到,在该方法浩长源码的最下方:
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
//这里会调用mLayout.generateDefaultLayoutParams()为每个ItemView设置LayoutParams
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrap && bound;
return holder.itemView;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

重写完这个方法就能编译通过了,只不过然并卵,界面上是一片空白,下面我们就走进
onLayoutChildren()
方法 ,为界面添加Item。

注:99%用不到的情况:如果需要存储一些额外的东西在
LayoutParams
里,这里返回你自定义的
LayoutParams
即可。 

当然,你自定义的
LayoutParams
需要继承自
RecyclerView.LayoutParams


三 onLayoutChildren()

该方法是LayoutManager的入口。它会在如下情况下被调用: 

1 在RecyclerView初始化时,会被调用两次。 

2 在调用adapter.notifyDataSetChanged()时,会被调用。 

3 在调用setAdapter替换Adapter时,会被调用。 

4 在RecyclerView执行动画时,它也会被调用。 

即RecyclerView 初始化 、 数据源改变时 都会被调用。 

(关于初始化时为什么会被调用两次,我在系列第一篇文章里已经分析过。)

在系列开篇我已经提到,它相当于ViewGroup的onLayout()方法,所以我们需要在里面layout当前屏幕可见的所有子View,千万不要layout出所有的子View。本文如下编写:
private int mVerticalOffset;//竖直偏移量 每次换行时,要根据这个offset判断
private int mFirstVisiPos;//屏幕可见的第一个View的Position
private int mLastVisiPos;//屏幕可见的最后一个View的Position
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {//没有Item,界面空着吧
detachAndScrapAttachedViews(recycler);
return;
}
if (getChildCount() == 0 && state.isPreLayout()) {//state.isPreLayout()是支持动画的
return;
}
//onLayoutChildren方法在RecyclerView 初始化时 会执行两遍
detachAndScrapAttachedViews(recycler);
//初始化
mVerticalOffset = 0;
mFirstVisiPos = 0;
mLastVisiPos = getItemCount();

//初始化时调用 填充childView
fill(recycler, state);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这个
fill(recycler, state);
方法将是你自定义LayoutManager之旅一生的敌人,简单的说它承担了以下任务: 

在考虑滑动位移的情况下: 

1 回收所有屏幕不可见的子View 

2 layout所有可见的子View

在这一节,我们先看一下它的简单版本,不考虑滑动位移,不考虑滑动方向等,只考虑初始化时,从头至尾,layout所有可见的子View,在下一节我会配合滑动事件放出它的完整版.
int topOffset = getPaddingTop();//布局时的上偏移
int leftOffset = getPaddingLeft();//布局时的左偏移
int lineMaxHeight = 0;//每一行最大的高度
int minPos = mFirstVisiPos;//初始化时,我们不清楚究竟要layout多少个子View,所以就假设从0~itemcount-1
mLastVisiPos = getItemCount() - 1;
//顺序addChildView
for (int i = minPos; i <= mLastVisiPos; i++) {
//找recycler要一个childItemView,我们不管它是从scrap里取,还是从RecyclerViewPool里取,亦或是onCreateViewHolder里拿。
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
//计算宽度 包括margin
if (leftOffset + getDecoratedMeasurementHorizontal(child) <= getHorizontalSpace()) {//当前行还排列的下
layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));

//改变 left  lineHeight
leftOffset += getDecoratedMeasurementHorizontal(child);
lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
} else {//当前行排列不下
//改变top  left  lineHeight
leftOffset = getPaddingLeft();
topOffset += lineMaxHeight;
lineMaxHeight = 0;

//新起一行的时候要判断一下边界
if (topOffset - dy > getHeight() - getPaddingBottom()) {
//越界了 就回收
removeAndRecycleView(child, recycler);
mLastVisiPos = i - 1;
} else {
layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));

//改变 left  lineHeight
leftOffset += getDecoratedMeasurementHorizontal(child);
lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
}
}
}
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
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

用到的一些工具函数(在系列开篇已介绍过):
//模仿LLM Horizontal 源码

/**
* 获取某个childView在水平方向所占的空间
*
* @param view
* @return
*/
public int getDecoratedMeasurementHorizontal(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}

/**
* 获取某个childView在竖直方向所占的空间
*
* @param view
* @return
*/
public int getDecoratedMeasurementVertical(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}

public int getVerticalSpace() {
return getHeight() - getPaddingTop() - getPaddingBottom();
}

public int getHorizontalSpace() {
return getWidth() - getPaddingLeft() - getPaddingRight();
}
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
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

如上编写一个超级简单的
fill()
方法,运行,你的程序应该就能看到流式布局的效果出现了。 

可是千万别开心,因为痛苦的计算远没到来。 

如果这些都看不懂,那么我建议: 

一,直接下载完整代码,配合后面的章节看,看到后面也许前面的就好理解了= =。 

二,去学习一下自定义ViewGroup的知识。

此时虽然界面上已经展示了流式布局的效果,可是它并不能滑动,下一节我们让它动起来。


四,动起来

想让我们自定义的LayoutManager动起来,最简单的写法如下:
@Override
public boolean canScrollVertically() {
return true;
}

@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
int realOffset = dy;//实际滑动的距离, 可能会在边界处被修复

offsetChildrenVertical(-realOffset);

return realOffset;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13

offsetChildrenVertical(-realOffset);
这句话移动所有的childView. 

返回值会被RecyclerView用来判断是否达到边界, 如果返回值!=传入的dy,则会有一个边缘的发光效果,表示到达了边界。而且返回值还会被RecyclerView用于计算fling效果。

写完编译,哇塞,真的跟随手指滑动了,只不过能动的总共就我们在上一节layout的那些Item,Item并没有回收,也没有新的Item出现。

好了,下面开始正经的写它吧,
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//位移0、没有子View 当然不移动
if (dy == 0 || getChildCount() == 0) {
return 0;
}

int realOffset = dy;//实际滑动的距离, 可能会在边界处被修复
//边界修复代码
if (mVerticalOffset + realOffset < 0) {//上边界
realOffset = -mVerticalOffset;
} else if (realOffset > 0) {//下边界
//利用最后一个子View比较修正
View lastChild = getChildAt(getChildCount() - 1);
if (getPosition(lastChild) == getItemCount() - 1) {
int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(lastChild);
if (gap > 0) {
realOffset = -gap;
} else if (gap == 0) {
realOffset = 0;
} else {
realOffset = Math.min(realOffset, -gap);
}
}
}

realOffset = fill(recycler, state, realOffset);//先填充,再位移。

mVerticalOffset += realOffset;//累加实际滑动距离

offsetChildrenVertical(-realOffset);//滑动

return realOffset;
}
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
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

这里用
realOffset
变量保存实际的位移,也是return 回去的值。大部分情况下它=dy。 

在边界处,为了防止越界,做了一些处理,realOffset 可能不等于dy。 

和别的文章不同的是,我参考了LinearLayoutManager的源码,先考虑滑动位移进行View的回收、填充(
fill()
函数),然后再真正的位移这些子Item。

fill()
的过程中


流程:

一 会先考虑到dy,回收界面上不可见的Item。 

二 填充布局子View 

三 判断是否将dy都消费掉了,如果消费不掉:例如滑动距离太多,屏幕上的View已经填充完了,仍有空白,那么就要修正dy给realOffset。


注意事项一:考虑滑动的方向

在填充布局子View的时候,还要考虑滑动的方向,即填充的顺序,是从头至尾填充,还是从尾至头部填充。 

如果是向底部滑动,那么是顺序填充,显示底端position更大的Item。( dy>0) 

如果是向顶部滑动,那么是逆序填充,显示顶端positon更小的Item。(dy<0)


注意事项二:流式布局 逆序布局子View的问题

再啰嗦最后一点,我们想象一下这个逆序填充的过程: 

正序过程可以自上而下,自左向右layout 子View,每次layout之前判断当前这一行宽度+子View宽度,是否超过父控件宽度,如果超过了就另起一行。 

逆序时,有两种方案:


1 利用Rect保存子View边界

正序排列时,保存每个子View的Rect, 
逆序时,直接拿出来,layout。


2 逆序化

自右向左layout子View,每次layout之前判断当前这一行宽度+子View宽度,是否超过父控件宽度, 

如果超过了就另起一行。并且判断最后一个子View距离父控件左边的offset,平移这一行的所有子View,较复杂,采用方案1. 

(我个人认为这两个方案都不太好,希望有朋友能提出更好的方案。) 

下面上码:
private SparseArray<Rect> mItemRects;//key 是View的position,保存View的bounds ,
/**
* 填充childView的核心方法,应该先填充,再移动。
* 在填充时,预先计算dy的在内,如果View越界,回收掉。
* 一般情况是返回dy,如果出现View数量不足,则返回修正后的dy.
*
* @param recycler
* @param state
* @param dy       RecyclerView给我们的位移量,+,显示底端, -,显示头部
* @return 修正以后真正的dy(可能剩余空间不够移动那么多了 所以return <|dy|)
*/
private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int dy) {

int topOffset = getPaddingTop();

//回收越界子View
if (getChildCount() > 0) {//滑动时进来的
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
if (dy > 0) {//需要回收当前屏幕,上越界的View
if (getDecoratedBottom(child) - dy < topOffset) {
removeAndRecycleView(child, recycler);
mFirstVisiPos++;
continue;
}
} else if (dy < 0) {//回收当前屏幕,下越界的View
if (getDecoratedTop(child) - dy > getHeight() - getPaddingBottom()) {
removeAndRecycleView(child, recycler);
mLastVisiPos--;
continue;
}
}
}
//detachAndScrapAttachedViews(recycler);
}

int leftOffset = getPaddingLeft();
int lineMaxHeight = 0;
//布局子View阶段
if (dy >= 0) {
int minPos = mFirstVisiPos;
mLastVisiPos = getItemCount() - 1;
if (getChildCount() > 0) {
View lastView = getChildAt(getChildCount() - 1);
minPos = getPosition(lastView) + 1;//从最后一个View+1开始吧
topOffset = getDecoratedTop(lastView);
leftOffset = getDecoratedRight(lastView);
lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(lastView));
}
//顺序addChildView
for (int i = minPos; i <= mLastVisiPos; i++) {
//找recycler要一个childItemView,我们不管它是从scrap里取,还是从RecyclerViewPool里取,亦或是onCreateViewHolder里拿。
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
//计算宽度 包括margin
if (leftOffset + getDecoratedMeasurementHorizontal(child) <= getHorizontalSpace()) {//当前行还排列的下
layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));

//保存Rect供逆序layout用
Rect rect = new Rect(leftOffset, topOffset + mVerticalOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child) + mVerticalOffset);
mItemRects.put(i, rect);

//改变 left  lineHeight
leftOffset += getDecoratedMeasurementHorizontal(child);
lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
} else {//当前行排列不下
//改变top  left  lineHeight
leftOffset = getPaddingLeft();
topOffset += lineMaxHeight;
lineMaxHeight = 0;

//新起一行的时候要判断一下边界
if (topOffset - dy > getHeight() - getPaddingBottom()) {
//越界了 就回收
removeAndRecycleView(child, recycler);
mLastVisiPos = i - 1;
} else {
layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));

//保存Rect供逆序layout用
Rect rect = new Rect(leftOffset, topOffset + mVerticalOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child) + mVerticalOffset);
mItemRects.put(i, rect);

//改变 left  lineHeight
leftOffset += getDecoratedMeasurementHorizontal(child);
lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
}
}
}
//添加完后,判断是否已经没有更多的ItemView,并且此时屏幕仍有空白,则需要修正dy
View lastChild = getChildAt(getChildCount() - 1);
if (getPosition(lastChild) == getItemCount() - 1) {
int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(lastChild);
if (gap > 0) {
dy -= gap;
}

}

} else {
/**
* ##  利用Rect保存子View边界
正序排列时,保存每个子View的Rect,逆序时,直接拿出来layout。
*/
int maxPos = getItemCount() - 1;
mFirstVisiPos = 0;
if (getChildCount() > 0) {
View firstView = getChildAt(0);
maxPos = getPosition(firstView) - 1;
}
for (int i = maxPos; i >= mFirstVisiPos; i--) {
Rect rect = mItemRects.get(i);

if (rect.bottom - mVerticalOffset - dy < getPaddingTop()) {
mFirstVisiPos = i + 1;
break;
} else {
View child = recycler.getViewForPosition(i);
addView(child, 0);//将View添加至RecyclerView中,childIndex为1,但是View的位置还是由layout的位置决定
measureChildWithMargins(child, 0, 0);

layoutDecoratedWithMargins(child, rect.left, rect.top - mVerticalOffset, rect.right, rect.bottom - mVerticalOffset);
}
}
}

Log.d("TAG", "count= [" + getChildCount() + "]" + ",[recycler.getScrapList().size():" + recycler.getScrapList().size() + ", dy:" + dy + ",  mVerticalOffset" + mVerticalOffset+", ");

return dy;
}
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
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

思路已经在前面讲解过,代码里也配上了注释,计算坐标等都是数学问题,略饶人,需要用笔在纸上写一写,或者运行调试调试。没啥好办法。 

值得一提的是,可以通过
getChildCount()
recycler.getScrapList().size()
 查看当前屏幕上的Item数量
和 scrapCache缓存区域的Item数量,合格的LayoutManager,childCount数量不应大于屏幕上显示的Item数量,而scrapCache缓存区域的Item数量应该是0. 
官方的LayoutManager都是达标的,本例也是达标的,网上大部分文章的Demo,都是不合格的。。 
原因在系列开篇也提过,不再赘述。

至此我们的自定义LayoutManager已经可以用了,使用的效果就和文首的两张图一模一样。

下面再提及一些其他注意点和适配事项:


五 适配notifyDataSetChanged()

此时会回调onLayoutChildren()函数。因为我们流式布局的特殊性,每个Item的宽度不一致,所以化简处理,每次这里归零。
//初始化区域
mVerticalOffset = 0;
mFirstVisiPos = 0;
mLastVisiPos = getItemCount();
1
2
3
4
1
2
3
4

如果每个Item的大小都一样,逆序顺序layoutChild都比较好处理,则应该在此判断,getChildCount(),大于0说明是DatasetChanged()操作,(初始化的第二次也会childCount>0)。根据当前记录的position和位移信息去fill视图即可。


六 适配 Adapter的替换。

我根据24.2.1源码,发现网上的资料对这里的处理其实是不必要的。


一 资料中的做法如下:

当对RecyclerView设置一个新的Adapter时,
onAdapterChanged()
方法会被回调,一般的做法是在这里remove掉所有的View。此时
onLayoutChildren()
方法会被再次调用,一个新的轮回开始。
@Override
public void onAdapterChanged(final RecyclerView.Adapter oldAdapter, final RecyclerView.Adapter newAdapter) {
removeAllViews();
}
1
2
3
4
1
2
3
4


二 我的新观点:

通过查看源码+打断点跟踪分析,调用RecyclerView.setAdapter后,调用顺序依次为


1 Recycler.setAdapter():

public void setAdapter(Adapter adapter) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, false, true); //张旭童注:注意第三个参数是true
requestLayout();
}
1
2
3
4
5
6
1
2
3
4
5
6

那么我们查看
setAdapterInternal()
方法:
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
...
//张旭童注:removeAndRecycleViews 参数此时为ture
if (!compatibleWithPrevious || removeAndRecycleViews) {
...
if (mLayout != null) {
//张旭童注: 所以如果我们更换Adapter时,mLayout不为空,会先执行如下操作,
mLayout.removeAndRecycleAllViews(mRecycler);
mLayout.removeAndRecycleScrapInt(mRecycler);
}
// we should clear it here before adapters are swapped to ensure correct callbacks.
//张旭童注:而且还会清空Recycler的缓存
mRecycler.clear();
}
...
if (mLayout != null) {
//张旭童注:这里才调用的LayoutManager的方法
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
//张旭童注:这里调用Recycler的方法
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

也就是说 更换Adapter一开始,还没有执行到
LayoutManager.onAdapterChanged()
,界面上的View都已经被remove掉了,我们的操作属于多余的。


2 LayoutManager.onAdapterChanged()

空实现:也没必要实现了
public void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter) {
}
1
2
1
2


3 Recycler.onAdapterChanged():

该方法先清空scapCache区域(貌似也是多余,一开始被清空过了),然后调用
RecyclerViewPool.onAdapterChanged()
 


void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
boolean compatibleWithPrevious) {
clear();
getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, compatibleWithPrevious);
}

public void clear() {
mAttachedScrap.clear();
recycleAndClearCachedViews();
}
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12


4 RecyclerViewPool.onAdapterChanged()

如果没有别的Adapter在用这个RecyclerViewPool,会清空RecyclerViewPool的缓存。
void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
boolean compatibleWithPrevious) {
if (oldAdapter != null) {
detach();
}
if (!compatibleWithPrevious && mAttachCount == 0) {
clear();
}
if (newAdapter != null) {
attach(newAdapter);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12


5 LayoutManager.onLayoutChildren()

新的布局开始。


七 总结:

引用一段话

They are also extremely complex, and hard to get right. For every amount of effort RecyclerView requires of you, it is doing 10x more behind the scenes.

本文Demo仍有很大完善空间,有些需要完善的细节非常复杂,需要经过多次试验才能得到正确的结果(这里我更加敬佩Google提供的三个LM)。每一个我们想要实现的需求,可能要花费比我们想象的时间*10倍的时间。 

上篇也提及到的,不要过度优化,达成需求就好。

可以通过
getChildCount()
recycler.getScrapList().size()
 查看当前屏幕上的Item数量
和 scrapCache缓存区域的Item数量,合格的LayoutManager,childCount数量不应大于屏幕上显示的Item数量,而scrapCache缓存区域的Item数量应该是0. 
官方的LayoutManager都是达标的,本例也是达标的,网上大部分文章的Demo,都是不合格的。。

感兴趣的同学可以对网上的各个Demo打印他们onCreateViewHolder执行的次数,以及上述两个参数的值,和官方的LayoutManager比较,这三个参数先达标,才算是及格的LayoutManager,但后续优化之路仍很长。

本系列文章相关代码传送门: 
自定义LayoutManager实现的流式布局 

欢迎star,pr,issue。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: