mBinding.recycler.linear().divider()
.set {
addLayout(R.layout.layout_ordinary_item)
}.setList(getList())
如果我要说,除了数据和布局之外,以上的几行代码,就实现了一个列表加载,有老铁会相信吗?
可以很负责人的告诉大家,经过Kotlin的DSL语言和扩展函数的使用,实现一个列表加载就是如此的简单,有的老铁可能会很懵,怎么没有看到适配器啊,也没有看到设置布局管理器啊,这就是此次封装的隐秘之处,弱化adapter的存在,当然了只是使用上弱化,其内部依然用到了adapter。
经过代码上不断的完善,功能上不断的拓展,目前的封装,已经满足绝大部分的常见需求,决定开源出来,希望可以给RecyclerView的使用上,带来一丝变化,当然了,此项目也会不定期更新,也欢迎大家在使用上提出自己的见解和问题,也希望点个小星星。
目前已开发功能:
本篇文章的概述如下
1、封装库快速使用
2、具体功能一一概述
3、开源地址
4、使用总结
5、后续规划
目前封装的库,已经上传到远程Maven,大家可以按照如下的步骤进行使用:
1、在你的根项目下的build.gradle文件下,引入maven。
allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}
2、在你需要使用的Module中build.gradle文件下,引入依赖。
dependencies {
implementation 'com.vip:relist:1.0.5'//一个包含了列表加载和下拉刷新、上拉加载的库,它包含了下面的两个库,使用它,下面的两个就不要引用了。
implementation 'com.vip:list:1.0.4'//列表加载库,如果使用了relist,这个不要再引用
implementation 'com.vip:refresh:1.0.0'//下拉刷新、上拉加载库,如果使用了relist,这个不要再引用
}
需要注意的是,目前拆分了三个依赖,大家一定看清楚后,进行选择使用。refresh依赖只是对SmartRefreshLayout包了一层,没有做过多的扩展,如果大家项目中已经有了刷新库,其实只用list这个依赖即可。list库是纯RecyclerView封装库,没有用到任何的第三方,大家可以放心使用。
依赖 |
概述 |
版本号 |
集成 |
relist |
一个包含了列表加载和下拉刷新、上拉加载的库 |
1.0.5 |
implementation 'com.vip:relist:1.0.5' |
list |
只包含列表加载(添加头尾、缺省页、侧滑删除、吸顶效果、分割线、DataBinding等) |
1.0.4 |
implementation 'com.vip:list:1.0.4' |
refresh |
只包含下拉刷新、上拉加载 |
1.0.0 |
implementation 'com.vip:refresh:1.0.0' |
关于功能上的使用,大家可以直接看源码,或者访问github地址后看使用说明也可以,当然了,在这里我也罗列一下。
普通的列表加载,就是RecyclerView原始的用法,创建适配器Adapter,给RecyclerView设置布局管理器,给RecyclerView设置适配器,虽然说,Adapter已经抽取封装了,但考虑到还有一部分小伙伴具有怀旧意识,也很喜欢使用这种方式加载列表,于是呢,这种方式就保留了。
具体使用,举例如下:
即便是普通列表加载,这里也是建议继承父类BaseAdapter,因为直接继承RecyclerView.Adapter的方式, 还得要重写一大堆方法,实在是冗余。
BaseAdapter对RecyclerView.Adapter做了一层封装,只需要传递要加载的layout和数据对象泛型即可,这两个就不贴源码了,一个是xml布局,一个自己定义的对象,没啥好说的。
数据绑定和逻辑处理,实现dataOperation方法即可,当然,这个也是强制要实现的方法。
class OrdinaryListAdapter : BaseAdapter(R.layout.layout_ordinary_item) {
override fun dataOperation(holder: BaseViewHolder, t: OrdinaryListBean?, position: Int) {
t?.title?.let {
holder.setText(R.id.tv_title, it)
}
t?.desc?.let {
holder.setText(R.id.tv_desc, it)
}
val ivPic = holder.findView(R.id.iv_pic)
t?.icon?.let {
ivPic.setImageResource(it)
}
}
}
这些代码就比较熟悉了吧,都是RecyclerView的原始用法,点击事件调用adapter中的setOnItemClickListener方法即可。
因为使用了ViewDataBinding,这里的mBinding.recycler指的是RecyclerView控件。
val manger = LinearLayoutManager(requireContext())
manger.orientation = LinearLayoutManager.VERTICAL
//设置布局管理器
mBinding.recycler.layoutManager = manger
val adapter = OrdinaryListAdapter()
//设置适配器
mBinding.recycler.adapter = adapter
//设置分割线
mBinding.recycler.addItemDecoration(
ItemDivider(
Color.parseColor("#cccccc"),
RecyclerView.VERTICAL, 0
)
)
//设置数据
adapter.setList(getList())
adapter.setOnItemClickListener {
//条目点击事件
Toast.makeText(requireContext(), "当前点击条目为:$it", Toast.LENGTH_SHORT).show()
}
看到上面的代码,还是觉得有些冗余,普通列表加载,也是建议大家尽量使用简洁用法。
val adapter = OrdinaryListAdapter()
mBinding.recycler.linear()//设置布局管理器
.divider()//设置分割线
.adapter=adapter//设置适配器
//设置数据
adapter.setList(getList())
adapter.setOnItemClickListener {
//条目点击事件
Toast.makeText(requireContext(), "当前点击条目为:$it", Toast.LENGTH_SHORT).show()
}
布局管理器调用如下:
垂直列表:linear()
横向列表:linear(RecyclerView.HORIZONTAL)
网格列表:grid(2),一列展示几个,传递数值即可
瀑布流列表:staggered(2),一列展示几个,传递数值即可
至于其他的列表展示,如,网格和瀑布流形式,只需要更改布局管理器即可。
封装之后的列表加载,是推荐使用的,相对于普通的列表加载,完全弱化了适配器的存在,只需要考虑数据处理即可,非常的简单。
mBinding.recycler.linear()
.divider()
.set {
addLayout(R.layout.layout_ordinary_item)
bindData {
//获取DataBinding
val binding = getDataBinding()
//获取Model
val model = getModel(adapterPosition)
binding?.apply {
tvTitle.text = model.title
tvDesc.text = model.desc
ivPic.setImageResource(model.icon!!)
}
setOnItemClickListener {
//条目点击事件
Toast.makeText(requireContext(), "当前点击条目为:$it", Toast.LENGTH_SHORT)
.show()
}
}
}.setList(getList())
相关方法说明:
属性或方法 |
概述 |
mBinding.recycler |
因为使用了ViewDataBinding,这里的mBinding.recycler指的是RecyclerView控件。 |
linear |
布局管理器,可调用方法如下: 垂直列表:linear() 横向列表:linear(RecyclerView.HORIZONTAL) 网格列表:grid(2),一列展示几个,传递数值即可 瀑布流列表:staggered(2),一列展示几个,传递数值即可 |
divider |
分割线 |
set |
扩展函数,泛型为加载的对象 |
addLayout |
添加列表Item展示的布局 |
bindData |
绑定数据和处理逻辑 |
getDataBinding |
获取ViewDataBinding |
getModel |
获取数据对象 |
setOnItemClickListener |
条目点击事件 |
setList |
设置列表数据 |
有的老铁可能会问了,封装之后简单是简单了,但是如果遇到复杂列表,都写到一个类里,代码量实在是太多了,哎!这确实是 个问题,但是呢,又不是问题,如果bindData里的逻辑比较多,你完成可以抽取到其他地方,比如ViewModel里,在ViewModel定义 方法后,调用即可,又或者呢,使用后边的DataBinding。
效果图可以忽略,重在功能哈~,多条目加载也封装了,有适配器和无适配方式,在实际的业务开发中,大家可以选择性进行使用。
也就是和传统的多条目保持一致,都在适配器里进行数据的渲染和逻辑处理。
还是那句话,多条目的适配器也抽取了基类,既然都有基类了,为了代码上的简洁,建议还是继承基类比较好。
如何添加多条目?
直接在构造方法里,调用addLayout即可,有几个多条目就添加几个,是不是非常的方便,泛型为数据对象,参数为多条目xml布局。
如何数据渲染和逻辑处理?
实现bindOperation方法即可,通过holder.itemViewType来却分多条目的类型,当然了这个必须和数据对象里的类型保持一致。
区分多条目类型
数据对象实现BaseMultipleItem,重写itemViewType属性,itemViewType就是用来区分多条目类型的,可以随意设置,或者是接口的某个参数,或者是对应的layout。
class OrdinaryMultipleItemAdapter : BaseMultipleItemAdapter {
constructor() {
//添加多条目类型以及绑定的数据对象
addLayout(R.layout.layout_ordinary_multiple_01)
addLayout(R.layout.layout_ordinary_multiple_02)
addLayout(R.layout.layout_ordinary_multiple_03)
}
override fun bindOperation(holder: BaseViewHolder, t: BaseMultipleItem?, position: Int) {
when (holder.itemViewType) {
1 -> {
val bean = t as MultipleItem01Bean
holder.setText(R.id.tv_title, bean.title!!)
holder.setText(R.id.tv_desc, bean.desc!!)
val ivPic = holder.findView(R.id.iv_pic)
ivPic.setImageDrawable(bean.icon)
}
2 -> {
val bean = t as MultipleItem02Bean
val ivPic01 = holder.findView(R.id.iv_01)
val ivPic02 = holder.findView(R.id.iv_02)
val ivPic03 = holder.findView(R.id.iv_03)
ivPic01.setImageDrawable(bean.icon01)
ivPic02.setImageDrawable(bean.icon02)
ivPic03.setImageDrawable(bean.icon03)
}
3 -> {
val bean = t as MultipleItem03Bean
holder.setText(R.id.tv_content, bean.content!!)
}
}
}
}
设置布局管理器和适配器
val adapter = OrdinaryMultipleItemAdapter()
mBinding.recycler.linear()
.divider().adapter = adapter
adapter.setList(getMoreList())
无适配器模式,更加的简单,只需要调用setMore方法即可,有多少条目就调用addLayout几次,在bindData里进行数据渲染和逻辑处理,当然了,也可以使用DataBinding形式,需要追加BR。
mBinding.recycler.linear()
.divider()
.setMore {
addLayout(R.layout.layout_multiple_01, BR.multiple1)
addLayout(R.layout.layout_multiple_02, BR.multiple2)
addLayout(R.layout.layout_multiple_03, BR.multiple3)
bindData {
}
}.setList(getMoreList())
DataBinding的出现,使得数据绑定更加的简单化,大大减少了代码的书写,大家可以采用提供的两种方式进行使用,一种是针对Item的绑定,一种是对RecyclerView自身的绑定。
第一步,addLayout,增加和xml绑定的BR
mBinding.recycler.linear().divider()
.set {
addLayout(R.layout.layout_ordinary_bind_item, BR.ordinary)
setOnItemViewClickListener { view, position ->
//条目点击事件
Toast.makeText(requireContext(), "当前点击条目为:$position", Toast.LENGTH_SHORT).show()
}
}.setList(getList())
第二步,xml数据绑定
第一步,xml中RecyclerView数据绑定
第二步、对应model中提供数据
class RecyclerViewBindViewModel : BaseViewModel() {
val layoutId = R.layout.layout_ordinary_bind_item//item列表
/**
* AUTHOR:AbnerMing
* INTRODUCE:获取视图绑定的name
*/
fun getListVariableName(): Int {
return BR.ordinary
}
/**
* AUTHOR:AbnerMing
* INTRODUCE:模拟数据
*/
fun getList(): MutableList {
return mutableListOf().apply {
for (a in 0..20) {
val bean = OrdinaryListBean()
bean.title = "我是标题$a"
bean.desc = "我是描述信息$a"
bean.icon = R.mipmap.vip_list_logo
add(bean)
}
}
}
}
属性一览
属性 |
类型 |
概述 |
listManager |
Int |
布局管理器:默认为纵向的普通列表。 0:普通列表 1:网格 2:瀑布流 |
listOrientation |
Int |
设置列表方向,默认纵向。 横向:RecyclerView.HORIZONTAL |
listSpanCount |
Int |
展示几列,适用于网格和瀑布流 |
isDivider |
Boolean |
是否展示分割线 |
dividerDrawable |
Int |
分割线样式 |
listLayout |
Int |
Item布局 |
listData |
MutableList |
数据 |
listVariableName |
Int |
绑定的BR |
listAdapter |
OnAdapterListener |
返回适配器,可以通过这里实现,适配器中的逻辑处理 |
isMultiple |
Boolean |
是否是多条目 |
multipleAdapter |
OnAdapterMultipleListener |
返回多条目适配器 |
multipleData |
MutableList |
多条目数据 |
multipleLayout |
MutableList |
多条目布局 |
multipleLayoutBindData |
MutableList |
layout绑定的数据对象 |
multipleVariableName |
MutableList |
xml绑定的对应的VariableName |
两种绑定方式,各有优点,大家可以根据实际的业务进行选择使用。
调用divider方法即可。
mBinding.recycler.linear()
.divider()
.set {
addLayout(R.layout.layout_ordinary_bind_item, BR.ordinary)
}.setList(getList())
divider可选参数如下:
参数 |
类型 |
概述 |
color |
Int |
分割线颜色,默认是#cccccc |
orientation |
Int |
分割线方向,默认和纵向保持一致,RecyclerView.VERTICAL |
lineWidth |
Int |
分割线的高度 |
margin |
Int |
分割线距离左右或者距离上下的边距 |
hideLast |
Boolean |
是否隐藏最后一条分割线 |
itemType |
Int |
默认为0,不为0时,则绘制横向线条,适用于网格列表分割线 |
头和尾的添加支持layout和View两种添加方式,代码如下所示:
mAdapter?.addHead(R.layout.layout_head)//初始 添加头 不用刷新
mAdapter?.addFoot(R.layout.layout_foot)//初始 添加尾 不用刷新
后续如果动态添加头尾,需要更新适配器。
mAdapter?.addHead(view, true)//追加头,需要刷新
mAdapter?.addFoot(view, true)//追加尾,需要刷新
动态删除头尾操作,支持按照索引进行删除。
mAdapter?.removeHead()
mAdapter?.removeFooter()
mAdapter?.setList(getList())
mAdapter?.addData()
mAdapter?.removeData(0)
缺省页面没什么好说的,数据为空或者数据加载错误的时候,设置一张占位View。
空页面,调用addEmptyView即可,支持layout和View两种模式,错误页面,调用addErrorView,和空页面使用方式一致。
mAdapter = mBinding.recycler.linear()
.divider()
.set {
addEmptyView(R.layout.layout_empty)//初始化 空页面
addErrorView(R.layout.layout_error)//初始化 错误页面
addLayout(R.layout.layout_item, BR.str)
}
//初始添加数据
mAdapter?.setList(getList())
功能 |
实现 |
显示空 |
mAdapter?.showEmptyView() |
隐藏空 |
mAdapter?.hintEmptyView() |
显示错误 |
mAdapter?.showErrorView() |
隐藏错误 |
mAdapter?.hintErrorView() |
调用drag方法即可实现拖拽排序功能。
mBinding.recycler.linear()
.drag()//支持拖拽
.set {
addLayout(R.layout.layout_drag_item, BR.drag)
}.setList(getList())
注意事项
传递的数据对象必须实现BaseNoDragBean,需要重写isDrag属性,false为禁止拖拽,true为允许拖拽。
class DragBean : BaseNoDragBean {
override var isDrag = false
var content = ""
}
调用slideDelete方法即可,支持左右侧滑删除,传递不同的值即可。
mBinding.recycler
.linear()
.slideDelete()//支持侧滑删除 默认是左滑删除 0是右边 1是左右两边
.set {
addLayout(R.layout.layout_main_item, BR.str)
}.setList(getList())
由原来的set方法改为setSlide方法即可。
mBinding.recycler
.linear()
.divider()
.setSlide {//如果要显示按钮 使用 setSlide
addLayout(R.layout.layout_item)
bindData {
val model = getModel(adapterPosition)
setText(R.id.tv_content, model)
}
}.setList(getList())
有的老铁可能会问,我想展示多个按钮,或者展示自定义的View,如何实现呢?在setSlide调用addSlideLayout,传入自己的xml布局即可。
实现吸顶就调用stick方法即可。
mBinding.recycler.linear()
.stick()
.set {
addLayout(R.layout.layout_stick_item, BR.stick)
}.setList(getList())
需要注意,数据对象需要实现StickHeaderBean,重写分组标识。
class StickHeaderBean : BaseStickHeaderBean {
override var stickGroup: String = ""//分组标识
var name = ""//普通内容
}
当然了这都是业务层的逻辑,按理来说,没必要在封装,但是考虑到代码的简洁性,单选和多选的判断逻辑就封装了一下,大家如有用到此功能,可按照如下的方式进行操作即可。
主要的就两部分:
1、开启单选刷新
mNotifySingleChoice = true
2、判断单选
adapterPosition == mCheckPosition,可以利用mCheckPosition来判断,进而更新UI。
mBinding.recycler.linear().divider().set {
mNotifySingleChoice = true//开启单选刷新
addLayout(R.layout.layout_single_choice_list_item, BR.single)
bindData {
val binding = this.getDataBinding()
//判断单选,直接判断 adapterPosition == mCheckPosition 即可
binding?.checkbox?.isChecked = adapterPosition == mCheckPosition
setOnItemViewClickListener { view, position ->
//条目点击
val singleBean = getList()[position]//单选 选择的对象
mViewModel.name.set(singleBean.name)
}
}
}.setList(getList())
多选
和单选一样,也是两部分
1、设置多选更新
mNotifyMultipleChoice = true
2、多选回调监听
setOnMultipleChoiceListener{}
mBinding.recycler.linear().divider().set {
mNotifyMultipleChoice = true//多选更新
addLayout(R.layout.layout_multiple_choice_list_item, BR.multiple)
bindData {
//多选回调监听
setOnMultipleChoiceListener {
var allPrice = 0.0f
it.forEach {
allPrice += it.price
}
mViewModel.commodityNumber.set("选择商品数量为:" + it.size)
mViewModel.allPrice.set("总价格为:$allPrice")
}
}
}.setList(getList())
刷新和加载使用的是SmartRefreshLayout这个开源框架,毕竟已经有很优秀的框架了,没必要再重新封装一个,具体的用法,大家可以按照SmartRefreshLayout的文档去操作使用就行。
支持全局设置下拉和上拉展示View,只需要在Application里初始化即可。
//上拉加载和下拉刷新,初始化头和尾
ListConfig.apply {
addRefreshHeader {
ClassicsHeader(it)
}
addRefreshFooter {
ClassicsFooter(it)
}
}
这里也简单举个例子:
1、xml中引入
2、代码简单使用
mSmartRefreshLayout?.setOnRefreshLoadMoreListener(object : OnRefreshLoadMoreListener {
override fun onRefresh(refreshLayout: RefreshLayout) {
}
override fun onLoadMore(refreshLayout: RefreshLayout) {
}
})
当然了,为了更好的拓展使用,针对SmartRefreshLayout,又简单做了一层包装,别无他意,就是为了让使用更加的简单。
1、xml里引入PageRefreshLayout
2、代码里使用
mBinding.refresh.getRecycler().linear()
.divider()
.set {
addLayout(R.layout.layout_item, BR.str)
}
//刷新和加载
mBinding.refresh.refresh { isRefresh, refreshLayout ->
mViewModel.doHttp {
addData(it)
}
}.autoRefresh()
直接调用refresh方法即可,isRefresh为true是下拉,否则就是上拉,非常的方便,而addData方法,则实现了分页加载数据,上拉和下拉直接调用addData即可。
PageRefreshLayout可调用方法如下
方法 |
参数 |
概述 |
autoRefresh |
无参 |
自动刷新操作 |
refresh |
无参 |
静默刷新(不带下拉刷新动画) |
addData |
Collection |
添加数据,分页会自动追加数据,下拉和上拉会自动关闭 |
setEnableRefresh |
Boolean |
是否禁止下拉 |
setEnableLoadMore |
Boolean |
是否禁止上拉 |
finishRefresh |
无参 |
关闭下拉刷新 |
finishLoadMore |
无参 |
关闭上拉刷新 |
getPager |
无参 |
获取当前页码 |
refresh |
回调函数 |
刷新和加载方法 |
getSmartRefresh |
无参 |
获取SmartRefreshLayout |
getRecycler |
无参 |
获取RecyclerView |
addEmptyView |
Int/View |
添加空的布局,支持layout和View |
addErrorView |
Int/View |
添加错误的布局,支持layout和View |
showEmptyView |
无参 |
显示空布局 |
showErrorView |
无参 |
显示错误布局 |
hintEmptyView |
无参 |
隐藏空布局 |
hintErrorView |
无参 |
隐藏错误布局 |
setHeightWrapContent |
无参 |
设置整体的列表由充满改为包裹内容。 |
关于大家在使用上的问题以及后续的优化,或者功能新增,都会时长更新,方便的话,给个小星星呗~
开源地址:https://github.com/AbnerMing888/VipList
1、无适配器的模式,在逻辑相对复杂的页面,建议大家可以抽取到ViewModel中实现,当然,也可以采用DataBinding的形式。
2、很多使用方式,或者常见的业务开发场景,在文档中或者源码中,都会详细的备注,大家可以细心的查看即可。
3、条目点击事件,给出了两个,大家可以选择性使用。
只返回索引
setOnItemClickListener {
}
返回当前点击的View和索引
setOnItemViewClickListener { view, position ->
}
此库的封装,除了刷新加载库使用了SmartRefreshLayout,其他的都是自己从0到1的开发,目前,自己已经在项目中使用,暂时没有出现任何问题,当然了,后续,也会不断的对其进行优化,增加一些其他的功能,希望有需要的小伙伴,长期关注。
关于维护上,不断的优化和解决一些大家所提的问题。
关于源码上,后续会逐一剖析其实现方式,确确实实,里面用到了一些另类技术,当然了,这都是后话了。