RecycerView实现不需要复用的View

做android开发的小伙伴,应该都知道,之前用ListView

  • 可以添加HeaderView
  • 可以添加FooterView
    这些直接不需要复用的view到listview中。

现在RecycerView替换ListView之后,貌似这两方法都没有了。

  • RecyclerView可以实现这两种方法么?
  • 等等,还有假如我不添加在头部及尾部,还可以添加到其他位置么?
    带着这两个疑问,我们一起去探索吧!

前言

RecycleView绑定数据主要代码在Adapter中。
重写Adapter我们需要实现如下几个方法:

  • onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder 创建ViewHolder,即:提供需要复用布局的方法(必须重写)
  • onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<*>?) 绑定ViewHolder数据的方法(必须重写)
  • getItemCount(): Int 提供需要创建ViewHolder个数的方法(必须重写)
  • getItemViewType(position: Int): Int 根据position指定ViewHolder的类型(可不必要重写,因为默认只有一个ViewHolder)

理出这些方法有什么用呢?
既然我们不要复用布局,那我们只加载一次View,然后每次创建ViewHolder的时候照样给它之前存在的View应该就能不复用了吧。

可能大家还担心,RecycleView不是滑动不可见的View满足条件之后会回收。
这里 系统还给ViewHolder提供了以下方法:

  • setIsRecyclable(false) false为不复用布局,不回收

嗯,貌似没有需要担心的了,试一下吧!

自定义全局View绑定ViewHolder测试

  • 这里用了上篇博客的RecyclerView绑定代码
  • 在创建ViewHolder代码中给一个一直存在的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
    class MainAdpater(context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>(){

    private var txt: TextView = TextView(context)
    init{
    txt.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300)
    txt.text = "我是自定义的不复用地View"
    txt.setTextColor(Color.WHITE)
    txt.gravity = Gravity.CENTER
    txt.setBackgroundColor(Color.GREEN)
    }

    override fun onNewCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    if (viewType == 0) {//其他正常的绑定
    return Title1ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.title_type1, parent, false))
    }
    //...其他代码略
    else{
    var vh = object : ViewHolder(txt) {}
    vh.setIsRecyclable(false)//这里设置不需要复用
    return vh
    }
    }

    //...其他代码略
    数据绑定及测试之后,完美运行,多次滑动也没有出现布局被回收的情况。

不过,你有没有发现这样写感觉不大雅观啊!有没有什么省代码的优雅写法呢?

嗯,对!需要封装一下。不过话说,一般就HeaderView 和 FooterView是大家比较常用的。
如果需要把View放到自定义位置的话,这样对于绑定的数据,做增删改查,可能会导致一定的数据混乱。
那这里就只封装HeaderView 和 FooterView吧!

让RecyclerView支持HeaderView 和 FooterView (封装后的HeaderAndFooterAdapter)

这里就直接贴代码了,代码加些注释应该大家都能明白原理的。嗯,对了。这是用kotlin写的

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
228
229
230
231
package top.goluck.recyclerview_2017_9_24

import android.support.v4.util.SparseArrayCompat
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup

/**
* Created by luck on 2017/9/24.
*/
abstract class HeaderAndFooterAdapter<T> : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

//以下代码用于操作绑定的data

protected var data=ArrayList<T>()

/**
* 获取指定position对应的T

* @param p
* *
* @return
*/
fun getDataT(p: Int): T? {
if (p > data.size || p < 0) {
return null
}
return data[p]
}

/**
* 设置指定positioin对应的T

* @param p
* *
* @param t
*/
fun setDataT(p: Int, t: T) {
data[p] = t
notifyItemChanged(p)
}

/**
* 清空数据但不通知刷新界面
*/
fun clearNotNotify() {
if (data != null) {
data.clear()
}
}

private var off = false

/**
* 设置添加数据是否清空之前的数据

* @param off
*/
fun clearNotNotify(off: Boolean) {
this.off = off
}

/**
* 清空数据并刷新界面
*/
fun clear() {
if (data != null) {
data.clear()
notifyDataSetChanged()
}
}

/**
* 添加集合数据并通知刷新界面 (注: 如果off设置为true,会先清空之前的数据)

* @param datas
*/
fun addData(datas: List<T>?) {
if (datas != null) {
if (off) {
data.clear()
off = false
}
data.addAll(datas)
notifyDataSetChanged()
}
}

/**
* 添加单个数据但不通知刷新界面

* @param mData
*/

fun addData(mData: T) {
if (data != null) {
data.add(mData)
}
}

/**
* 添加集合数据但不通知刷新界面

* @param datas
*/
fun addDataNotNotify(datas: List<T>?) {
if (datas != null) {
data.addAll(datas)
}
}

/**
* 重置数据并刷新通知

* @param data
*/
fun setData(data: List<T>) {
if (data != null) {
this.data = data as ArrayList<T>
} else {
this.data.clear()
}
if (off) {
off = false
}
notifyDataSetChanged()
}

//以上代码用于操作绑定的data

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {//拦截是Header或这Footer,然后创建一直存在的View的ViewHoder用于绑定
if (mHeaderViews.get(viewType) != null) {
val holder = object : RecyclerView.ViewHolder(mHeaderViews.get(viewType)) {
}
holder.setIsRecyclable(false)
return holder
} else if (mFootViews.get(viewType) != null) {
val holder = object : RecyclerView.ViewHolder(mFootViews.get(viewType)) {
}
holder.setIsRecyclable(false)
return holder
}
return onNewCreateViewHolder(parent, viewType)
}

private fun isHeaderViewPos(position: Int): Boolean {//用于判断是否是header
return position < headersCount
}

private fun isFooterViewPos(position: Int): Boolean {//用于判断是否是footer
return position >= headersCount + newItemCount
}

fun addHeaderView(view: View) {//添加header的View
mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view)
}

fun addFooterView(view: View) {//添加footer的View
mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view)
}

val headersCount: Int//header总个数
get() = mHeaderViews.size()

val footersCount: Int//footer总个数
get() = mFootViews.size()

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {//该方法只重写,缺少payloads参数,我们不用它

}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<*>?) {//拦截header footer的绑定
if (isHeaderViewPos(position)) {
return
}
if (isFooterViewPos(position)) {
return
}

onNewBindViewHolder(holder, position - headersCount, payloads)
}

override fun getItemCount(): Int {//总的需要展示的count,head个数 + footer个数 + 重写adapter真实数据的个数
return headersCount + footersCount + newItemCount
}

override fun getItemViewType(position: Int): Int {//添加时候对应的key用于当ItemView的type
if (isHeaderViewPos(position)) {
return mHeaderViews.keyAt(position)
} else if (isFooterViewPos(position)) {
return mFootViews.keyAt(position - headersCount - newItemCount)
}
return getNewItemViewType(position - headersCount)
}

/**
* 要显示的item
* @param parent
* @param viewType 对应的viewType
* *
* @return
*/
abstract fun onNewCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder

/**
* 绑定item
* @param holder 对应viewType的holder
* @param position list的下标
*/
abstract fun onNewBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<*>?)

/**
* 要展示的item个数
* @return
*/
abstract val newItemCount: Int

/**
* 设置要展示的ItemView对应的type
* @return
*/
abstract fun getNewItemViewType(position: Int): Int


private val mHeaderViews = SparseArrayCompat<View>()
private val mFootViews = SparseArrayCompat<View>()
companion object {//定义不会出现异常用于指定是Header或Footer的type值
const private val BASE_ITEM_TYPE_HEADER = 1000000
const private val BASE_ITEM_TYPE_FOOTER = 2000000
}
}

使用封装后的HeaderAndFooterAdapter

实现适配器

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
class MainAdpater(context: Context) : HeaderAndFooterAdapter<Data>() {//继承HeaderAndFooterAdapter

private var txt: TextView = TextView(context)//用于不复用的自定义位置View
init{
txt.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300)
txt.text = "我是自定义的不复用地View"
txt.setTextColor(Color.WHITE)
txt.gravity = Gravity.CENTER
txt.setBackgroundColor(Color.GREEN)
}

override fun onNewBindViewHolder(holder: ViewHolder, position: Int, payloads: List<*>?) {
if (holder is Title1ViewHolder) {
holder.bindDataandListener(position, data[position])
} else if (holder is ContextViewHolder) {
holder.bindDataandListener(position, data[position])
}
}

override val newItemCount: Int
get() = data.size

override fun onNewCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
if (viewType == 0) {
return Title1ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.title_type1, parent, false))//正常的ViewHolder
} else if (viewType == 1) {
return ContextViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.context, parent, false))//正常的ViewHolder
} else {
var vh = object : ViewHolder(txt) {}//不复用的自定义位置View
vh.setIsRecyclable(false)
return vh
}
}

override fun getNewItemViewType(position: Int): Int {
return data[position].type
}
}

添加各种不复用的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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
var data = getData()

var itemdata = Data()
itemdata.type = 2
data.add(15,itemdata)//添加自定义位置的不复用View

mMainAdpater.setData(data)

var txt1 = TextView(this)
txt1.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,400)
txt1.text = "我是添加的HeadView"
txt1.gravity = Gravity.CENTER
txt1.setTextColor(Color.GREEN)
txt1.setBackgroundColor(Color.YELLOW)
mMainAdpater.addHeaderView(txt1)//添加一个HeaderView


var txt2 = TextView(this)
txt2.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,200)
txt2.text = "我是添加的FootView"
txt2.setTextColor(Color.RED)
txt2.gravity = Gravity.CENTER
txt2.setBackgroundColor(Color.BLUE)
mMainAdpater.addFooterView(txt2)//添加一个FootView

main_reyclerview.initAdapter(mMainAdpater)
}

fun getData(): ArrayList<Data> {
var datas = ArrayList<Data>()
var position = 0
for (i in 0..3) {
for (j in 0..9) {
var data = Data()
data.id = i * 9 + j
if (j == 0) {
data.type = 0
data.position = position
data.context = "我是type=0数据,我是标题:" + (i + 1)
} else {
data.type = 1
data.position = position
data.context = "我是type=1数据,我是第" + (i + 1) + "组的数据,内容数据:" + (j + 1)
}
datas.add(data)
}
position = (i + 1) * 10
}
return datas
}
//其他代码略...
}

时间有限,就不贴效果图了,可自行查阅项目源码。
真实效果文字叙述:

  • 正常达到效果
  • 头部一个view,尾部一个view
  • 设置对应type的位置的地方,展示了一个指定位置不复用的View。

总结

  • 原理很简单,不要每次都重新创建View,同时记得要设置不需要回收
  • 特别是自定义不需要复用的指定位置的view时,逻辑代码一定要写好,不要照成数据修改有异常。

本篇完

(注意:转载文章请注明来源[RecycerView实现不需要复用的View](http://goluck.top/2017/09/24/RecycerView%E5%AE%9E%E7%8E%B0%E4%B8%8D%E9%9C%80%E8%A6%81%E5%A4%8D%E7%94%A8%E7%9A%84View/))