多类型RecyclerView方案
需要加入jitpack仓库:
maven { url 'https://jitpack.io' }
依赖引入
implementation 'com.github.ToryCrox.ModuleAdapter:module_adapter:0.2.0'
- 创建Adapter,直接使用NormalModuleAdapter即可 private val listAdapter = NormalModuleAdapter()
- 创建Model,可以是任意模型类。但不能是基本类型,Map,List这种集合类
data class ItemOneModel(
val index: Int
)
- 创建自定义View, 注意必须实现IModuleView,并且要与上一步定义的Model进行一一对应,这里可继承已实现IModuleView部分方法的AbsModuleView,。在自定义View专注于实现自己的功能,使组件拆分出来,也利于以后的复用。
class ItemOneView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : AbsModuleView<ItemOneModel>(context, attrs) {
private val textView = AppCompatTextView(context)
init {
addView(textView, LayoutParams.MATCH_PARENT, 40.dp())
textView.setBackgroundColor(Color.GREEN)
textView.gravity = Gravity.CENTER
textView.setTextColor(Color.WHITE)
textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14f)
}
/**
* Adapter的onBindViewHolder时执行,
*/
override fun update(model: ItemOneModel) {
super.update(model)
}
/**
* 数据有变化时执行
*/
override fun onChanged(model: ItemOneModel) {
super.onChanged(model)
textView.text = model.index.toString() + "-" + groupPosition
}
}
- 将自定义View注册到Adapter中
listAdapter.register {
ItemOneView(it.context)
}
- 接口数据返回后,更新Adapter数据就完成了
private fun handleData(list: List<Any>) {
listAdapter.setItems(list)
}
在自定义view之前,我们要先有一个明确的概念,就是我们目前定义的是业务相关的组件,与通用的UI控件widget是不同,我们称之为业务组件,是有明确的业务场景的,所以千万不要轻易复用
- 自定义View都需要继承IModuleView,但是为了方便大家使用,原则上应该使用AbsModuleView,例如:
class AddCommentAnonymousView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AbsModuleView<AnonymousModel>(context, attrs, defStyleAttr)
- 继承IModuleView或者AbsModuleView都需要对应的泛型,表示该View所需要的数据模型类型,该类型一定要使用自定义的类型,方便以后修改更新,具有明确的象征意义,例如商详: PmTitleModel(标题模型) --> PmTitleView,标题组件,这样让我们的数据与组件的对应关系更加明确
- 不管是IModuleView还是AbsModuleView,它们更新数据的方法都是update方法,只不过在AbsModuleView中,会对数据做一次对比,数据有变化时才调用onChange
override fun update(model: T) {
val isChanged = data != model
data = model
if (isChanged) {
onChanged(model)
}
subModuleViewhHelper?.update(model)
}
所以在使用AbsModuleView时要注意一下几点:
- AbsModuleView中会存储有数据对象data,所以不需要再加上一个成员记录数据
- 可以看到,在update中,是对model的内容进行对比,再决定是否调用onChanged的,所以建议将model定义为data class
- 不管是RecyclerView自动回调,还是我们手动更新数据,记得一定要使用update而不是onChanged
- 在某些情况下,即使我们不使用RecyclerView+ModuleAdapter来实现页面,而是直接将自定义View写到布局中,也建议直接继承AbsModuleView,主要有一下原因:
- View与Model有明确的对应关系,体现数据驱动的**
- 更新数据统一使用update方法,方法比较统一,数据需要变动时修改量较少,不会出现以下更新入参混乱的情况
orderCustomerServiceView.render(model.customerServiceProcessItem,
orderNum,
model.statusInfo?.statusValue
?: 0)
orderShippingView.render(model.trackInfo)
register方法有很多参数,但是默认只有最后一个参数是必须的,例如:
listAdapter.register(
gridSize = 5,
poolSize = 10,
itemSpace = ItemSpace(spaceH = 4.dp(), spaceV = 5.dp(), edgeH = 10.dp())
) {
ItemOneView(it.context)
}
大括号里面的是一个高阶函数,当RecyclerView需要创建一个新的Item的时候会回调,创建一个新的View,有一下几点需要了解和注意的点:
- ModuleAdapter是通过创建的IModuleView对应的泛型来进行类型匹配的,对应的泛型必须是确定类型,不能是Int, Long,String这种毫无意义的简单类型,也不能是List, Map这种不确定的类型
- regisger方法调用必须在adapter设置给RecyclerView之前,否则一些设置会不生效,并被强制crash。
- register方法里面一定要新创建View,不要将里面的View进行复用,全局变量的引用等操作
- 同一种类型不允许重复注册,即同一个Modle类型不要对应多个View,如果有遇到这种情况,可以有一下两种方案
将Model进行包装成不同类型进行映射,如BannerView和Banner2View都需要使用到BannerModel,但我们可以将它们分别包装成不同的Model使用
val list: List<String> = emptyList()
)
class BannerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : AbsModuleView<BannerModel>(context, attrs) {
override fun getLayoutId(): Int = R.layout.layout_mock_banner_view
}
data class Banner2Model(
val item: BannerModel
)
class Banner2View @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : AbsModuleView<BannerModel>(context, attrs) {
override fun getLayoutId(): Int = R.layout.layout_mock_banner2_view
}
- 第二种方式是通过register的modelKey来区分,这个之后再来说明
接下来说一下register的其他参数
gridSize表示一行有几列,如商品流一行有两个,则传2,金刚位一行5个则传6, 默认值为1。
注意,使用NormalAdapter时需要自己给RecyclerView设置LayoutManager,需要使用NormalAdapter.getGridLayoutManager(context)
例如: gridSize=5, 那么最终显示的是一行5个
listAdapter.register(
gridSize = 5,
poolSize = 10,
itemSpace = ItemSpace(spaceH = 4.dp(), spaceV = 5.dp(), edgeH = 10.dp())
) {
ItemOneView(it.context)
}
这个主要用来解决同一个Model类型映射不同View的情景
例如: 两种不同类型的View公用同一个模型:
listAdapter.registerModelKeyGetter<ItemTwoModel> { it.type }
val itemSpace = ItemSpace(spaceH = 4.dp(), spaceV = 5.dp(), edgeH = 10.dp())
listAdapter.register(
gridSize = 3, poolSize = 20, groupType = "list", modelKey = ItemType.ONE,
itemSpace = itemSpace
) {
ItemTwoView(it.context)
}
listAdapter.register(
gridSize = 3, poolSize = 20, groupType = "list", modelKey = ItemType.TWO,
itemSpace = itemSpace
) {
ItemTwoExtraView(it.context)
}