-
RecyclerView variety are many, the framework of layout of familiar BaseRecyclerViewAdapterHelper, vlayout, etc. Including Google in the new version of RecyclerView launched MergeAdapter(later renamed ConcatAdapter) and so on. Why write your own frameworks when there are so many out there? Most of the time these frameworks consider common generic scenarios, which may not be applicable to some bizarre product design requirements, which is why this article appears.
-
At present variety adapter framework for the overall design scheme has two kinds, one kind is BaseRecyclerViewAdapterHelper this extremely simplified developers to write code, use the most concise way to achieve the material. The other is a ConcatAdapter, where each style is designed as a separate module (sub-adapter), view creation, and data binding are handled within that module, and a main adapter wraps and ties those sub-adapters together. This paper adopts the second design method.
Because the amount of code is very small, the following will not say, directly paste code:
- View builder, keeping the native API naming
<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
}
Copy the code
- Data adapter
<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]
}
}
Copy the code
Ok, so much code, here is a brief introduction to the principle:
- View builders don’t go into much detail here, but rather abstract the way views are built
match
This method:
fun match(data: T): Boolean
Copy the code
- It receives a datatype parameter and needs to return one
Boolean
Value, common product design common data type should correspond to a view type, but there is such a bizarre design, such as returning aPerson
Data type, ifsex
Show a style for men (Left and right layout: the profile picture is on the left, and the introduction is on the right
), ifsex
Women need to show a different style (Up and down layout: head in the middle, with a brief introduction below
), such a scenario can define two views as follows:
<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))}// The Creator creates the view when the Person is male
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))}// The view is created through this Creator when the Person is female
override fun match(data: Person) = data.sex == Sex.WOMAN
}
Copy the code
- Adapters, of course, are inheritance
RecyclerView.Adapter
- Let’s start with three caches:
<MultiTypeAdapter.kt>
abstract class MultiTypeAdapter : RecyclerView.Adapter<ViewHolder>() {
// Store data types in data order
private val dataCache: ArrayList<Class<*>> = ArrayList()// [DataType]
// Store ViewTypeCreator, index is the subscript of the data type in the cache, since a data type can correspond to multiple creators,
// Therefore use collection storage
private val creatorCache: SparseArray<SparseArray<ViewTypeCreator<Any, *>>> = SparseArray()// DataTypeIndex - [ViewTypeCreators]
// Store ViewTypeCreator, indexed to the corresponding viewType, one-to-one relationship, this cache is for quick lookup
private val viewTypeCache: SparseArray<ViewTypeCreator<Any, *>> = SparseArray()// ViewType - ViewTypeCreator
}
Copy the code
- Here’s how to register a view builder:
<MultiTypeAdapter.kt>
// Automatically read the type of generic Data for storage
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) {
// If not, cache it
dataCache.add(clazz)
index = dataCache.size - 1
}
// Initializes the ViewTypeCreator collection corresponding to the Data type
var cache = creatorCache[index]
if (cache == null) {
cache = SparseArray()
}
// Construct a unique identifier as an index
val id = System.identityHashCode(creator)
@Suppress("UNCHECKED_CAST")
cache.put(id, creator as ViewTypeCreator<Any, *>)
creatorCache.put(index, cache)
}
Copy the code
- Finally, in accordance with the
RecyclerView.Adapter
Principle of call process analysis:
<MultiTypeAdapter.kt>
// Get the current index data
abstract fun getData(position: Int): Any
override fun getItemViewType(position: Int): Int {
val data = getData(position)// 1. Obtain the data corresponding to the current index
val viewType = getCreatorViewType(data)// 2. Get ViewType based on current data
return if(viewType ! = -1) {
viewType
} else
super.getItemViewType(position)
}
private fun getCreatorViewType(data: Any): Int {
val clazz: Class<*> = data: :class.java// Get the class type of data
var viewType: Int
val index = dataCache.indexOf(clazz)// Find the index of this data in the cache
if (dataCache.size > 0&& index ! = -1) {// Check whether the ViewTypeCreator corresponding to this data is registered
val creators: SparseArray<ViewTypeCreator<Any, *>> = creatorCache[index]// Get the ViewTypeCreator set corresponding to this data
// The Data bind more than one viewTypeCreator.
if (creators.size() > 1) {// A data corresponds to multiple viewtypes
creators.forEach { id, viewCreator ->
if (viewCreator.match(data)) {// Iterate over the ViewTypeCreator and determine if it matches the condition
viewType = id// viewType is the unique id obtained from the ViewTypeCreator instance above: system. identityHashCode
if (viewTypeCache.indexOfKey(viewType) < 0) {// If viewTypeCache is not cached, add it to the cache
viewTypeCache.put(viewType, viewCreator)// This level of cache is used to quickly find the corresponding ViewTypeCreator based on the viewType
}
return viewType
}
}
}
// The Data only bind one viewTypeCreator.
else if (creators.size() == 1) {// A data corresponds to a 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 {
// Find the corresponding ViewTypeCreator according to the viewType, and build the Holer view with onCreateViewHolder
val viewCreator: ViewTypeCreator<*, *> = getViewCreatorByViewType(viewType)
return viewCreator.onCreateViewHolder(LayoutInflater.from(parent.context), parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// Find the corresponding ViewTypeCreator according to the viewType and bind the data via onBindViewHolder
val data = getData(position)
@Suppress("UNCHECKED_CAST")
val viewCreator: ViewTypeCreator<Any, ViewHolder> =
getViewCreatorByViewType(getItemViewType(position)) as ViewTypeCreator<Any, ViewHolder>
viewCreator.onBindViewHolder(holder, data)}// Find the corresponding ViewTypeCreator according to viewType
private fun getViewCreatorByViewType(viewType: Int): ViewTypeCreator<Any, *> {
return viewTypeCache[viewType]
}
Copy the code
The above isMultiTypeAdapter
All the code of
- Here’s a use case:
- Start by defining an adapter, inheritance
MultiTypeAdapter
class SampleAdapter : MultiTypeAdapter() {
val data = mutableListOf<Any>()
override fun getData(position: Int) = data[position]
override fun getItemCount(a) = data.size
}
Copy the code
- Define two display text and one display picture
viewType
// Literal data type definition: contains the main title and subtitle
data class Title(val mainTitle: String = "".val subTitle: String = "")
// Text style 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)
}
}
// Text style 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)
}
}
// Image style
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}}Copy the code
- registered
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().apply {
// image type
registerCreator(ImageCreator())
// text type
registerCreator(TextCreator())
// the same bean but different view type
registerCreator(MainTitleCreator())
registerCreator(SubTitleCreator())
}
fillData(adapter)
recycler_view.adapter = adapter
}
private fun fillData(adapter: SampleAdapter) {
for (i in 0.10.) {
adapter.data.run {
add(R.drawable.test)
add("I am string")
add(Title("I am MainTitle"))
add(Title(""."I am SubTitle"))}}}Copy the code
The final display effect is as follows:
If a new style is added later in the product design, you just need to define a new ViewTypeCreator, register it with a MultiTypeAdapter, and then add the corresponding type of data to the data set. Adapter and data, view build decouple.
Project address: github.com/seagazer/mu…