RecycleView入门

RecycleView入门

八月 13, 2018

RecycleView出来很长一段时间了,我也终于熟练使用了,所以决定写一个日志,用来总结并分享一下自己的理解。因为个人能力有限,写的比较浅显,所以命名为”入门“。如果有错误,欢迎指正。

组件

RecycleView 作为一个容器类的View,并不像 TextView 或者 ImageView 一样是独立个体。RecycleView 更像是一套框架。所以这个段落我命名为组件。
  从功能和角色分工上,我把 RecycleView 分为4个部分,(仅代表个人观点,如果与官方文档有出入,请以官方文档为主)。这四个部分分别是: RecycleViewAdapterLayoutManagerViewHolder。 他们有着明确的分工:

组件 工作
RecycleView 主体,负责ViewHolder的回收和复用,Adapter与LayoutManager的协调调度
Adapter 适配器,负责ViewHolder的创建与数据绑定
LayoutManager 布局管理器,负责排版与交互
ViewHolder Item个体,负责单个ItemView的维护与状态管理
  • RecycleView 是主体,并且只负责回收复用Item,根据AdapterLayoutManager的反馈,协调几个组件,不包含任何业务逻辑。那么我们可以得到一个结论:RecycleView不会干涉展示效果,也不会限制展示效果,只是处理复用逻辑。

  • Adapter 是适配器,只负责创建ViewHolder数据绑定。那么我们可以得到一个结论:Adapter不会干涉展示效果,但是会干涉数据内容、展示数据,决定创建的ViewHolder类型。

  • LayoutManager 是布局管理器,只负责ItemView的排版,不关联AdapterViewHolder。那么我们可以得到一个结论:LayoutManager决定最终显示效果,Child 的布局方式,甚至交互效果;但是不能干涉View的创建以及数据内容,不能决定View的类型,也不能“看见”ViewHolder

  • ViewHolder 是ItemView的包装体,包含并记录View的状态、类型等属性,可以在其他三大组件间传递。我们可以总结为:它是Item个体的体现,负责对View进行数据填充,以及细节逻辑处理。

    从上面的整理,可以理解为:RecycleView是指挥者,负责协调调度;Adapter是生产者,负责产出有效的“产品”(RecycleView复用的ViewHolde仍然需要重新经过Adapter的数据绑定,直接展示的情况是产品可用,没有被回收);LayoutManager是消费者,负责使用产出成品和抛弃无用产品;ViewHolder是产品,或者理解为包裹,就像快递包裹一样,统一的包装,标明里面的内容,方便保存,传递,分类,使用。

优点

  那么这么做的优点是什么呢?因为很多初学者朋友都在说,还没有ListView简单,好复杂啊。
  其实并不这样的,可以片面的理解他为ListView的升级版,将ListView和GridView合并起来了,大家可曾还记得那些奇奇怪怪的需求?

  • 横向滚动的列表
  • 横向滚动的宫格
  • 横向瀑布流
  • 纵向瀑布流
  • Banner图
  • 卡片堆叠
  • 画廊

  谷歌肯定也遇到了这些需求,他们前期也有为这些需求做过一些偏门的实现,有兴趣的小伙伴可以去搜索一下。不过都因为扩展性太差而被冷落了。那么有没有办法一劳永逸呢?有的,所以他们做出了RecycleView。通过第一节的分析,想必很多小伙伴已经看出来了,或者对ListViewRecycleView都比较熟悉的小伙伴也看出来了。RecycleView其实是对ListView结构的进一步抽象。

  • Adapter其实没变,只是从原本的View,增加了ViewHower的封装。
  • LayoutManager只是把ListViewGridView原本的排版工作独立起来。
  • ViewHolder其实就是原本Adapter创建的View

因为抽象出来了,所以干脆把原本Adapter里面的getItemType方法强化了一下。所以就改头换面了,然而其实并没有变,还是熟悉的味道,熟悉的配方。

使用

下面我们就使用RecyclerView来找找ListView的感觉:

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
//这个方法写在Activity中,假设我们已经findViewById找到RecyclerView了
private void initView(RecyclerView recyclerView){
//绑定系统提供的线性布局,也就是ListView的模式
recyclerView.setLayoutManager(new LinearLayoutManager(this));
//数据集合,为了简化代码,我这里直接new了
List<Bean> data = new ArrayList<>();
//new出Adapter,为了查看方便,我也不声明为成员变量了
MyAdapter adapter = new MyAdapter(data);
//绑定Adapter
recyclerView.setAdapter(adapter);

//初始化
adapter.notifyDataSetChanged();
}

//熟悉的Adapter写法,继承,只是需要指定一个ViewHolder泛型
private class MyAdapter extends RecyclerView.Adapter<MyHolder>{
//保留集合对象
private List<Bean> data;
//熟悉的构造器写法
public MyAdapter(List<Bean> data){
this.data = data;
}

//这是创建Holder,我们可以理解为getView()中,view为null的情况
@NonNull
@Override
public MyHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
//直接返回Holder,构造器传入参数,可以理解为给原来的View加了壳
return new MyHolder(
LayoutInflater.from(parent.getContext()).inflate(R.layout.xxxxx,parent,false));
}

//把Holder和数据绑定一下,可以理解为getView()中View不为null的情况了
@Override
public void onBindViewHolder(@NonNull MyHolder holder, int position) {
//根据序号取到数据
Bean bean = data.get(position);
//把数据绑定到相应的View上
((TextView)holder.itemView.findViewById(R.id.xxxx)).setText(bean.name);
((TextView)holder.itemView.findViewById(R.id.xxxx)).setText(bean.age);
}

//返回Iten数量的方法,我记得以前经常有人返回一个Integer.MAX_VALUE来做无限循环
@Override
public int getItemCount() {
return data.size();
}
}
//假设这就是我们的bean
private class Bean{
String name = "";
String age = "";
}

//Holder的类,我们可以什么都不写
private class MyHolder extends RecyclerView.ViewHolder {
public MyHolder(View itemView) {
super(itemView);
}
}

  以上,就是一个ListView风格的写法,是不是很熟悉,虽然看起来很乱,但是这确实可以用,也确实是ListView的写法,不是吗?而且很多入门Demo的写法也都是这样写的。
  但是作为一个比较作的人,觉得不够优雅,一定要改改,所以我这样写,我称为“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
//使用时候基本没区别
private void initView(RecyclerView recyclerView){

recyclerView.setLayoutManager(new LinearLayoutManager(this));
List<Bean> data = new ArrayList<>();
MyAdapter adapter = new MyAdapter(this,data);
recyclerView.setAdapter(adapter);

adapter.notifyDataSetChanged();

}

//还是上面的Adapter
private class MyAdapter extends RecyclerView.Adapter<MyHolder>{

private List<Bean> data;
//保存的成员变量的LayoutInflater
private LayoutInflater inflater;

//构造器传入Context来创建LayoutInflater,也可以直接传入一个LayoutInflater
public MyAdapter(Context context,List<Bean> data){
this.data = data;
this.inflater = LayoutInflater.from(context);
}

//这里代码变少了,直接使用MyHolder的静态构造方法
@NonNull
@Override
public MyHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return MyHolder.create(inflater,parent);
}

//这里也变少了,使用Holder提供的绑定方法
@Override
public void onBindViewHolder(@NonNull MyHolder holder, int position) {
holder.onBind(data.get(position));
}

//这里没变
@Override
public int getItemCount() {
return data.size();
}
}

//还是那个bean
private class Bean{
String name = "";
String age = "";
}

//变化最大的地方就是这里了
private static class MyHolder extends RecyclerView.ViewHolder {

//静态构造方法,传入环境参数,内部构造,封装起来,防止传入不合法的布局id
static MyHolder create(LayoutInflater inflater,ViewGroup parent){
return MyHolder(inflater.inflate(R.layout.xxxx,parent,false));
}

//保存自己的View引用
private TextView nameView;
private TextView ageView;

//构造器
private MyHolder(View itemView) {
super(itemView);
//既然构造器已经传入了View,为什么不顺便把需要用到的View保存一份呢?
nameView = itemView.findViewById(R.id.xxxx);
ageView = itemView.findViewById(R.id.xxxx);
}

//申明一个数据绑定方法,只要求外部传入bean,绑定方式由自己决定
public void onBind(Bean bean){
nameView.setText(bean.name);
ageView.setText(bean.age);
}
}

看到这里,有些比较较真的朋友就一定会有很多问题,我来一组QA吧!

问:这有区别吗?
答:没区别啊!
问:优点是什么?
答:好看!
问:性能有优化吗?
答:没有!
问:那么这有写,最大的好处是什么?
答:好看而已,没别的了。

是的,这套写法仅仅是为了好看,让自己看了舒服,让别人看了也舒服。你可能会说:也没好看到哪里去啊!那我把业务复杂一下,就可以看出哪里好看了。因为代码比较多了,所以很多地方我能简略的都尽量简略。

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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//首先我们假设要求一个列表有3种布局,类似于手机淘宝的首页那样。
//那么你的代码是什么样呢?
//如果我们使用上面的样式,是这样的:

//首先是看不出啥变化的初始化方法,嗯,这次我加了个点击监听
private void initView(RecyclerView recyclerView){

recyclerView.setLayoutManager(new LinearLayoutManager(this));
List<BaseBean> data = new ArrayList<>();
MyAdapter adapter = new MyAdapter(this,data);
recyclerView.setAdapter(adapter);

//设置一个列表的点击监听事件
adapter.addClickListener(new OnHolderClickListener() {
@Override
public void onHolderClick(BaseHolder holder, View view) {
//从Holder身上是可以拿到序号的
int postion = holder.getAdapterPosition();
//然后我们可以判断到底是哪种类型的Holder,当然也可以去对比Data集合的bean的类型
if(holder instanceof MyHolder1){
//我们还可以知道是这个item的哪个View
if(view == holder.nameView){
//第postion个Item的NameView被点击了
}else if(view == holder.ageView){
//第postion个Item的AgeView被点击了
}

}else if(holder instanceof MyHolder2){
// ....
}else if(holder instanceof MyHolder3){
// ....
}
}
});

adapter.notifyDataSetChanged();

}

//功能增加了,要求变得已经非常接近现实的产品需求了,所以Adapter也理所当然的变长了
private class MyAdapter extends RecyclerView.Adapter<BaseHolder> implements OnHolderClickListener{

//数据集合,LayoutInflater
private List<BaseBean> data;
private LayoutInflater inflater;
//增加了点击事件的集合,学Google的,适应多个监听者的场景
private ArrayList<OnHolderClickListener> listeners = new ArrayList<>();

//构造器没变
public MyAdapter(Context context,List<BaseBean> data){
this.data = data;
this.inflater = LayoutInflater.from(context);
}

//给外部留出添加监听的方法,其实就是添加到集合
public void addClickListener(OnHolderClickListener listener){
this.listeners.add(listener);
}

//移除监听的方法,也就是集合本身的方法而已
public void removeClickListener(OnHolderClickListener listener){
this.listeners.remove(listener);
}

//这里就变多了,不过个人认为并没有随着业务逻辑复杂而增加太多代码,增加一种类型,这里也就增加4行而已,不至于一个上千行的Adapter
@NonNull
@Override
public BaseHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {

BaseHolder holder;
//判断类型,调用相应Holder的构造方法
switch (viewType){
case 1: {
holder = MyHolder1.create(inflater,parent);
break;
}
case 2: {
holder = MyHolder2.create(inflater,parent);
break;
}
case 3: {
holder = MyHolder3.create(inflater,parent);
break;
}
default: {
holder = null;
}
}
//如果类型未定义,那么肯定要抛出异常的
if(holder == null){
throw new RuntimeException("未定义的viewType");
}
//给holder设置监听,之所以在创建时候绑定,因为对象Holder是复用的
holder.setClickListener(this);
return holder;
}

//哈哈,我们的数据绑定方法不用变哦,如果是最开始那个写法会怎么样?想象一下。
@Override
public void onBindViewHolder(@NonNull BaseHolder holder, int position) {
holder.onBind(data.get(position));
}

//这里也没动
@Override
public int getItemCount() {
return data.size();
}

//我们这里也很简单的返回Bean的type,根据实际情况,可以用不同的实现,比如header形式,那么position==0是一种type,position!=0是另一种type
@Override
public int getItemViewType(int position) {
return data.get(position).type;
}

//Adapter最上面,我们实现了OnHolderClickListener的接口,仅仅是为了在这里把点击事件分发出去而已
@Override
public void onHolderClick(BaseHolder holder, View view) {
for(OnHolderClickListener listener : listeners){
if(listener != null){
listener.onHolderClick(holder, view);
}
}
}
}

//为了上面的效果,我们需要一个Base的数据Bean
private class BaseBean{
int type = 0;
}

//这是第一种数据类型
private class Bean1 extends BaseBean{
public Bean1(){
type = 1;
}
}

//这是第二种Bean了
private class Bean2 extends BaseBean{
public Bean2(){
type = 2;
}
}

//这是第三种Bean
private class Bean3 extends BaseBean{
public Bean3(){
type = 3;
}
}

//BaseHolder,我们这里封装一些通用的方法,点击事件我们一开始就做好监听
private static class BaseHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

//监听事件的回调函数
private OnHolderClickListener clickListener;

//点击事件回调函数的设置方法
public void setClickListener(OnHolderClickListener listener){
this.clickListener = listener;
}

//构造器意思一下就好了
private BaseHolder(View itemView) { super(itemView); }

//这里我们一定要申明一个标准,这样Adapter就可以一股脑的塞进去了
public void onBind(BaseBean bean){ }

//点击事件的方法,直接给回调就好了
@Override
public void onClick(View v) {
if(this.clickListener != null){
this.clickListener.onHolderClick(this,v);
}
}
}

//假设我们有很多Item,Holder一号
private static class MyHolder1 extends BaseHolder {

//提供构造方法,Adapter就不用再关心布局id之类的了
static MyHolder1 create(LayoutInflater inflater,ViewGroup parent){
return MyHolder1(inflater.inflate(R.layout.xxxx,parent,false));
}

//这里我们可以记录有需要的View,就像上一个demo,同时为需要的View绑定点击事件,
//直接view.setOnClickListener(this)就好了
private MyHolder1(View itemView) { super(itemView); }

//转换为自己的Bean,就可以为所欲为了
@Override
public void onBind(BaseBean bean){
Bean1 data = (Bean1)bean;
}
}

//和上面一样,只是为了保证demo的完整
private static class MyHolder2 extends BaseHolder {

static MyHolder2 create(LayoutInflater inflater,ViewGroup parent){
return MyHolder2(inflater.inflate(R.layout.xxxx,parent,false));
}

private MyHolder2(View itemView) { super(itemView); }

@Override
public void onBind(BaseBean bean){ }
}

//不多说,一样的
private static class MyHolder3 extends BaseHolder {

static MyHolder3 create(LayoutInflater inflater,ViewGroup parent){
return MyHolder3(inflater.inflate(R.layout.xxxx,parent,false));
}

private MyHolder3(View itemView) { super(itemView); }

@Override
public void onBind(BaseBean bean){ }
}

//最后,就是一个点击事件的监听了。
interface OnHolderClickListener{
void onHolderClick(BaseHolder holder,View view);
}

  以上,就是一个相对完整的demo了。而最后一个demo,基本上也算是实际项目中经常遇到的需求,通过这种模式,可以实现HeaderFooter的效果,不要再去找什么支持addHeaderRecyclerView了,他们的实现方式其实和我上面写的是一样的,都是多布局实现的。
  你可能会说:好多类啊,好复杂啊。但是你想过没有,我们这样写过之后,代码会更加专注。HolderA里面只需要专注的处理他自己相关的逻辑,比如Bean过来了,我怎么绑定,哪些需要点击事件,我有几张图片需要加载,里面进度条走到多少,文本怎么排版。甚至里面可以是一个Banner图,比如这个项目
  这样代码就很纯粹,业务代码的牵连就很少,代码量并没有减少什么,但是整体逻辑的梳理和代码的整洁,就会好很多了。
  
  以上,就是我对于RecyclerView的一些简单理解了,希望可以帮到你。