RecyclerView
多樣式布局的框架有很多,大家熟悉已久的BaseRecyclerViewAdapterHelper
摔吏,vlayout
等等,包括google在新版本RecyclerView
中推出的MergeAdapter
物喷,ConcatAdapter
等起宽。既然有這么多現(xiàn)成的框架,為什么還要去自己編寫一個呢?很多時候這些框架考慮的都是常見通用性場景瘫俊,在某些奇葩的產(chǎn)品設(shè)計需求中也許并不適用蛛芥,這也是這篇文章出現(xiàn)的原因。目前多樣式適配器框架總體的設(shè)計方案有兩種军援,一種是
BaseRecyclerViewAdapterHelper
這種極度簡化開發(fā)者編寫的代碼量仅淑,用最簡潔的方式去實現(xiàn)多樣式。另外一種就是vlayout
這種胸哥,將每種樣式設(shè)計為單獨模塊(子適配器)涯竟,視圖創(chuàng)建,數(shù)據(jù)綁定都在該模塊內(nèi)部處理,再用一個主適配器將這些子適配器進(jìn)行包裝關(guān)聯(lián)庐船。本文采用的是第二種設(shè)計方式银酬。
由于代碼量極少,下面就不多說了筐钟,直接貼代碼:
- 視圖構(gòu)建器揩瞪,保持原生api命名方式
<ViewTypeCreator.kt>
abstract class ViewTypeCreator<T, VH : ViewHolder> {
abstract fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): VH
abstract fun onBindViewHolder(holder: VH, data: T)
abstract fun match(data: T): Boolean
open fun getItemId(position: Int) = RecyclerView.NO_ID
}
- 數(shù)據(jù)適配器
<MultiTypeAdapter.kt>
abstract class MultiTypeAdapter : RecyclerView.Adapter<ViewHolder>() {
private val dataCache: ArrayList<Class<*>> = ArrayList()
private val creatorCache: SparseArray<SparseArray<ViewTypeCreator<Any, *>>> = SparseArray()
private val viewTypeCache: SparseArray<ViewTypeCreator<Any, *>> = SparseArray()
abstract fun getData(position: Int): Any
inline fun <reified T : Any> registerCreator(creator: ViewTypeCreator<T, *>) {
registerCreatorInner(T::class.java, creator)
}
fun registerCreatorInner(clazz: Class<*>, creator: ViewTypeCreator<*, *>) {
var index = dataCache.indexOf(clazz)
if (index == -1) {
dataCache.add(clazz)
index = dataCache.size - 1
}
var cache = creatorCache[index]
if (cache == null) {
cache = SparseArray()
}
val id = System.identityHashCode(creator)
@Suppress("UNCHECKED_CAST")
cache.put(id, creator as ViewTypeCreator<Any, *>)
creatorCache.put(index, cache)
}
override fun getItemViewType(position: Int): Int {
val data = getData(position)
val viewType = getCreatorViewType(data)
return if (viewType != -1) {
viewType
} else
super.getItemViewType(position)
}
override fun getItemId(position: Int): Long {
val itemViewType = getItemViewType(position)
val viewCreator = getViewCreatorByViewType(itemViewType)
return viewCreator.getItemId(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val viewCreator: ViewTypeCreator<*, *> = getViewCreatorByViewType(viewType)
return viewCreator.onCreateViewHolder(LayoutInflater.from(parent.context), parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val data = getData(position)
@Suppress("UNCHECKED_CAST")
val viewCreator: ViewTypeCreator<Any, ViewHolder> =
getViewCreatorByViewType(getItemViewType(position)) as ViewTypeCreator<Any, ViewHolder>
viewCreator.onBindViewHolder(holder, data)
}
private fun getCreatorViewType(data: Any): Int {
val clazz: Class<*> = data::class.java
var viewType: Int
val index = dataCache.indexOf(clazz)
if (dataCache.size > 0 && index != -1) {
val creators: SparseArray<ViewTypeCreator<Any, *>> = creatorCache[index]
// The Data bind more than one viewTypeCreator.
if (creators.size() > 1) {
creators.forEach { id, viewCreator ->
if (viewCreator.match(data)) {
viewType = id
if (viewTypeCache.indexOfKey(viewType) < 0) {
viewTypeCache.put(viewType, viewCreator)
}
return viewType
}
}
}
// The Data only bind one viewTypeCreator.
else if (creators.size() == 1) {
viewType = creators.keyAt(0)
if (viewTypeCache.indexOfKey(viewType) < 0) {
viewTypeCache.put(viewType, creators.valueAt(0))
}
return viewType
}
}
throw RuntimeException("Current dataType [$clazz] is not found in DataTypeCache:\n$dataCache \nPlease check the Type of data for your custom creator.")
}
private fun getViewCreatorByViewType(viewType: Int): ViewTypeCreator<Any, *> {
return viewTypeCache[viewType]
}
}
好了,代碼就這么多篓冲,下面簡單介紹下原理:
- 視圖構(gòu)建器不過多介紹李破,主要就是抽象出視圖構(gòu)建的方法,這里只重點說下
match
這個方法:
fun match(data: T): Boolean
- 這里面接收一個數(shù)據(jù)類型參數(shù)壹将,需要返回一個
Boolean
值嗤攻,通常的產(chǎn)品設(shè)計常見一種數(shù)據(jù)類型應(yīng)該是對應(yīng)一種視圖類型,但是就是存在這么奇葩的設(shè)計诽俯,比如返回一個Person
數(shù)據(jù)類型妇菱,如果sex
為男需要展示一種樣式(左右布局:頭像在左邊,右邊顯示簡介
)暴区,如果sex
為女則需要展示另一種樣式(上下布局:頭像在中間闯团,下面顯示簡介
),這樣的場景就可以這樣定義兩種視圖:
<ManCreator.kt>
class ManCreator : ViewTypeCreator<Person, ManCreator.Holder>() {
...
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
return Holder(inflater.inflate(R.layout.view_type_man, parent, false))
}
// 當(dāng)person為男性的時候會通過該Creator創(chuàng)建視圖
override fun match(data: Person) = data.sex == Sex.MAN
}
<WomanCreator.kt>
class WomanCreator : ViewTypeCreator<Person, WomanCreator.Holder>() {
...
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
return Holder(inflater.inflate(R.layout.view_type_female, parent, false))
}
// 當(dāng)person為女性的時候會通過該Creator創(chuàng)建視圖
override fun match(data: Person) = data.sex == Sex.WOMAN
}
- 適配器仙粱,當(dāng)然是繼承
RecyclerView.Adapter
- 先介紹一下3個緩存:
<MultiTypeAdapter.kt>
abstract class MultiTypeAdapter : RecyclerView.Adapter<ViewHolder>() {
// 按數(shù)據(jù)順序房交,存儲數(shù)據(jù)類型
private val dataCache: ArrayList<Class<*>> = ArrayList()// [DataType]
// 存儲ViewTypeCreator,索引為該數(shù)據(jù)類型在緩存的下標(biāo)缰盏,由于一個數(shù)據(jù)類型可對應(yīng)多個Creator涌萤,
// 因此使用集合存儲
private val creatorCache: SparseArray<SparseArray<ViewTypeCreator<Any, *>>> = SparseArray()// DataTypeIndex - [ViewTypeCreators]
// 存儲ViewTypeCreator,索引為對應(yīng)的viewType口猜,一對一的關(guān)系负溪,該緩存是為了快速查找
private val viewTypeCache: SparseArray<ViewTypeCreator<Any, *>> = SparseArray()// ViewType - ViewTypeCreator
}
- 接下來看下注冊視圖構(gòu)建器的方法:
<MultiTypeAdapter.kt>
// 自動讀取泛型Data數(shù)據(jù)的類型進(jìn)行存儲
inline fun <reified T : Any> registerCreator(creator: ViewTypeCreator<T, *>) {
registerCreatorInner(T::class.java, creator)
}
fun registerCreatorInner(clazz: Class<*>, creator: ViewTypeCreator<*, *>) {
var index = dataCache.indexOf(clazz)
if (index == -1) {
// 如果沒有存儲過進(jìn)行緩存
dataCache.add(clazz)
index = dataCache.size - 1
}
// 初始化該Data數(shù)據(jù)類型對應(yīng)的ViewTypeCreator集合
var cache = creatorCache[index]
if (cache == null) {
cache = SparseArray()
}
// 構(gòu)造唯一標(biāo)識作為索引
val id = System.identityHashCode(creator)
@Suppress("UNCHECKED_CAST")
cache.put(id, creator as ViewTypeCreator<Any, *>)
creatorCache.put(index, cache)
}
- 最后按照
RecyclerView.Adapter
調(diào)用流程分析下原理:
<MultiTypeAdapter.kt>
// 獲取當(dāng)前索引的數(shù)據(jù)
abstract fun getData(position: Int): Any
override fun getItemViewType(position: Int): Int {
val data = getData(position)// 1.獲取當(dāng)前索引對應(yīng)的數(shù)據(jù)
val viewType = getCreatorViewType(data)// 2.根據(jù)當(dāng)前數(shù)據(jù)獲取ViewType
return if (viewType != -1) {
viewType
} else
super.getItemViewType(position)
}
private fun getCreatorViewType(data: Any): Int {
val clazz: Class<*> = data::class.java// 獲取data數(shù)據(jù)的class類型
var viewType: Int
val index = dataCache.indexOf(clazz)// 查找出該data在緩存中的索引
if (dataCache.size > 0 && index != -1) {// 判斷是否注冊過該data對應(yīng)的ViewTypeCreator
val creators: SparseArray<ViewTypeCreator<Any, *>> = creatorCache[index]// 獲取該data對應(yīng)的ViewTypeCreator集合
// The Data bind more than one viewTypeCreator.
if (creators.size() > 1) {// 一個data數(shù)據(jù)對應(yīng)多種viewType
creators.forEach { id, viewCreator ->
if (viewCreator.match(data)) {// 遍歷ViewTypeCreator,并且判斷是否符合條件济炎,也就是上面說的match匹配方法
viewType = id// viewType就是上面根據(jù)ViewTypeCreator實例獲取到的唯一id:System.identityHashCode
if (viewTypeCache.indexOfKey(viewType) < 0) {// 如果viewTypeCache沒有緩存過川抡,則添加入緩存
viewTypeCache.put(viewType, viewCreator)// 這一級緩存是為了能夠快速根據(jù)viewType查找對應(yīng)的ViewTypeCreator
}
return viewType
}
}
}
// The Data only bind one viewTypeCreator.
else if (creators.size() == 1) {// 一個data數(shù)據(jù)對應(yīng)一種viewType
viewType = creators.keyAt(0)
if (viewTypeCache.indexOfKey(viewType) < 0) {
viewTypeCache.put(viewType, creators.valueAt(0))
}
return viewType
}
}
throw RuntimeException("Current dataType [$clazz] is not found in DataTypeCache:\n$dataCache \nPlease check the Type of data for your custom creator.")
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// 根據(jù)viewType查找到對應(yīng)的ViewTypeCreator,并且通過onCreateViewHolder構(gòu)建Holer視圖
val viewCreator: ViewTypeCreator<*, *> = getViewCreatorByViewType(viewType)
return viewCreator.onCreateViewHolder(LayoutInflater.from(parent.context), parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// 根據(jù)viewType查找到對應(yīng)的ViewTypeCreator崖堤,并且通過onBindViewHolder綁定數(shù)據(jù)
val data = getData(position)
@Suppress("UNCHECKED_CAST")
val viewCreator: ViewTypeCreator<Any, ViewHolder> =
getViewCreatorByViewType(getItemViewType(position)) as ViewTypeCreator<Any, ViewHolder>
viewCreator.onBindViewHolder(holder, data)
}
// 根據(jù)viewType查找到對應(yīng)的ViewTypeCreator
private fun getViewCreatorByViewType(viewType: Int): ViewTypeCreator<Any, *> {
return viewTypeCache[viewType]
}
以上就是MultiTypeAdapter
的所有代碼
- 下面舉個使用案例:
- 先定義一個適配器,繼承
MultiTypeAdapter
class SampleAdapter : MultiTypeAdapter() {
val data = mutableListOf<Any>()
override fun getData(position: Int) = data[position]
override fun getItemCount() = data.size
}
- 定義兩種展示文字和一種展示圖片的
viewType
// 文字?jǐn)?shù)據(jù)類型定義:包含主標(biāo)題和副標(biāo)題
data class Title(val mainTitle: String = "", val subTitle: String = "")
// 文字樣式1
class MainTitleCreator : ViewTypeCreator<Title, MainTitleCreator.Holder>() {
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val title: TextView = itemView.findViewById(R.id.main_title)
}
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
return Holder(inflater.inflate(R.layout.view_type_main_title, parent, false))
}
override fun onBindViewHolder(holder: Holder, data: Title) {
holder.title.text = data.mainTitle
}
override fun match(data: Title): Boolean {
return !TextUtils.isEmpty(data.mainTitle) && TextUtils.isEmpty(data.subTitle)
}
}
// 文字樣式2
class SubTitleCreator : ViewTypeCreator<Title, SubTitleCreator.Holder>() {
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val title: TextView = itemView.findViewById(R.id.sub_title)
}
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
return Holder(inflater.inflate(R.layout.view_type_sub_title, parent, false))
}
override fun onBindViewHolder(holder: Holder, data: Title) {
holder.title.text = data.subTitle
}
override fun match(data: Title): Boolean {
return !TextUtils.isEmpty(data.subTitle) && TextUtils.isEmpty(data.mainTitle)
}
}
// 圖片樣式
class ImageCreator : ViewTypeCreator<Int, ImageCreator.Holder>() {
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val image: ImageView = itemView.findViewById(R.id.image)
}
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
return Holder(inflater.inflate(R.layout.view_type_image, parent, false))
}
override fun onBindViewHolder(holder: Holder, data: Int) {
holder.image.setImageResource(data)
}
override fun match(data: Int): Boolean {
return false
}
}
- 注冊
viewTypeCreator
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recycler_view.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
val adapter = SampleAdapter()
// image type
adapter.registerCreator(ImageCreator())
// the same bean but different view type
adapter.registerCreator(MainTitleCreator())
adapter.registerCreator(SubTitleCreator())
for (i in 0..10) {
adapter.data.add(R.drawable.test)
adapter.data.add("I am string")
adapter.data.add(Title("I am MainTitle"))
adapter.data.add(Title("", "I am SubTitle"))
}
recycler_view.adapter = adapter
}
最終展示效果如下:
如果后續(xù)產(chǎn)品設(shè)計新增了樣式木柬,只需要定義新的ViewTypeCreator淹办,再注冊到MultiTypeAdapter中,然后在數(shù)據(jù)集中添加對應(yīng)類型的數(shù)據(jù)即可速挑,適配器和數(shù)據(jù)梗摇,視圖構(gòu)建完全解耦。
可以直接遠(yuǎn)程依賴引入流纹,項目地址:https://github.com/seagazer/multitype