创建好item布局item_page.xml后,会自动生成ItemPageBinding文件
使用BindingAdapter构造方法创建Adapter,在绑定器中使用itemBinding来访问和更新Item内容
//in XxxActivity.ky
val adapter = BindingAdapter<ItemBean, ItemBinding>(ItemBinding::inflate) { position, item ->
//在绑定器内,配置对ui元素和bean属性之间的绑定,设置点击事件等。
itemBinding.title.text = item.title
itemBinding.title.setOnClickListener {
}
}
使用带有payloads的方式,只需增加payloads参数,编译器会自动切换到对应重载方法。
val adapter =
BindingAdapter<ItemBean, ItemBinding>(ItemBinding::inflate) { position, item, payloads ->
}
在构造时指定初始数据
val adapter = BindingAdapter(ItemBinding::inflate, listOf("xx")) { position, item, payloads ->
}
注:如果使用DataBinding 记得在绑定器内调用executePendingBindings()方法
提供了3种方式创建多类型Adapter
适合于多个布局对应的Item类型是不同的情况。
- buildMultiTypeAdapterByType 通过 Class 构建多类型Adapter
sealed class DataType(val text: String) {
class TitleData(text: String) : DataType(text)
class NormalData(text: String) : DataType(text)
}
//1.通过Class类型决定ItemType
val adapter =
buildMultiTypeAdapterByType {
//配置item到布局类型的映射,以下2种效果是一样的,只是指定泛型方式不同
layout(ItemSimpleTitleBinding::inflate) { _, item: DataType.TitleData ->
itemBinding.title.text = item.text
}
layout<DataType.NormalData, ItemSimpleBinding>(ItemSimpleBinding::inflate) { _, item ->
itemBinding.title.text = item.text
}
}
当无法按Class来区分布局类型的时候可以使用该方式
- buildMultiTypeAdapterByIndex 自定义匹配类型构建多类型Adapter
- builder.layout 定义布局,返回布局类型id
- builder.extractItemViewType 指定Item所对于的布局类型id
//2.自定义ItemType
val adapter = buildMultiTypeAdapterByIndex<DataType> {
val typeTitle = layout(ItemSimpleTitleBinding::inflate) { _, item: DataType.TitleData ->
itemBinding.title.text = item.text
}
val typeNormal = layout(ItemSimpleBinding::inflate) { _, item: DataType.NormalData ->
itemBinding.title.text = item.text
}
extractItemViewType { position, item -> if (position % 10 == 0) typeTitle else typeNormal }
}
类似于原生的方式,需要自己维护id,不推荐
- buildMultiTypeAdapterByMap 自定义匹配类型构建多类型Adapter
- builder.layout 定义布局,使用固定的布局类型id
- builder.extractItemViewType 指定Item所对于的布局类型id
//3.自定义ItemType
val adapter = buildMultiTypeAdapterByMap<DataType> {
val typeTitle = 0
val typeNormal = 1
layout(typeTitle, ItemSimpleTitleBinding::inflate) { _, item: DataType.TitleData ->
itemBinding.title.text = item.text
}
layout(typeNormal, ItemSimpleBinding::inflate) { _, item: DataType.NormalData ->
itemBinding.title.text = item.text
}
extractItemViewType { _, item -> if (item is DataType.TitleData) typeTitle else typeNormal }
}
该库没有包含Header/Footer的任何实现,而是使用了RecyclerView 库中自带的ConcatAdapter来实现。
得益于ConcatAdapter的灵活性,我们可以直接使用+
拼接多个Adapter
来实现Header/Footer的效果,拼接顺序即Header、内容、Footer的相对位置。
SingleViewBindingAdapter 是BindingAdapter的一个子类,它只有一个元素,可以很方便生成 Header/Footer 的Adapter
+
依次连接多个Adapter- adapter.copy() 可以拷贝一个Adapter,相当于拷贝了其onCreateViewHolder和onBindViewHolder,并使用其数据作为初始数据,后续的数据变更是相互独立的,且状态不共享。
val header = SingleViewBindingAdapter(HeaderSimpleBinding::inflate)
val footer = SingleViewBindingAdapter(FooterSimpleBinding::inflate)
binding.list.adapter = header + adapter + footer
binding.list.adapter = header + adapter + header.copy() + adapter.copy() + footer //也可以任意拼接
- singleViewBindingAdapter.update() 更新布局状态
header.update {
itemBinding.tips.text = select
}
由于可以通过ConcatAdapter 来实现多个Adapter的连接,那么对其中一些Adapter的元素进行显示和隐藏切换就十分有用,也是后续许多拓展模块的基础。
- adapter.isVisible 控制Adapter显示和隐藏
adapter.isVisible = false //隐藏
由于使用itemView.visibility = View.GONE 控制item隐藏仍然占用布局空间,因此提供了isGone拓展属性来替代需要隐藏item的情况
- itemBinding.isGone 控制Item显示和隐藏
val adapter = BindingAdapter<ItemBean, ItemBinding>(ItemBinding::inflate) { position, item ->
//itemBinding.itemView.visibility = View.GONE 此方式仍然占用空间
itemBinding.isGone = item.title.isEmpty()
}
大约有3种变更方式
操作进行数据修改,然后调用adapter.notifyXxxx()
- adapter.data 获取当前数据List
adapter.data.addAll(data)
adapter.notifyDataSetChanged()
内置了一些常用的拓展方案,满足大多数情况。如需要更多也可以自定义
-
adapter.appendData() 末尾追加数据
-
adapter.replaceData() 替换数据
adapter.appendData(data)//追加数据
adapter.replaceData(data)//替换数据
- adapter.changeDataList() 替换Adapter内部List实现,实现特殊功能时使用
adapter.changeDataList(LinkedList())
通过替换列表实现类,我们可以拓展一些模块:
-
InfiniteListWrapper 将List数据重复,模拟无限数据
-
adapter.setupInfiniteDataModule() 设置无限数据模块,一般Banner无限滚动中使用
adapter.setupInfiniteDataModule() //数据变成无限循环的
-
ObservableListWrapper 将List变更通知到Adapter
-
adapter.setupAutoNotifyModule() 设置自动刷新模块,自动将List的变更同步到Adapter,无需notify
adapter.setupAutoNotifyModule()
adapter.data.add(data)
adapter.data.add(0, data)
-
LazyListWrapper 动态的将List的代理到不同的实现
-
adapter.setupDiffModule() 替换实现类为 AsyncListDiffer内部的List 来使用Differ模块,避免继承ListAdapter 或者 修改Adapter类
-
diffModule.submitList() 提供新数据,差量更新模块会计算旧数据和新数据的变更,并应用到Adapter上
val diffModule = adapter.setupDiffModule(object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem
})
lifecycleScope.launchWhenCreated {
viewModel.dataFlow.collect {
diffModule.submitList(it)
}
}
本模块参考了Paging3库中的源码,进行了一些简化,主要有以下特点:
-
相比于Paging, 无需继承PagingDataAdapter,完全解耦,无需更改Adapter/RecyclerView,简化了加载状态
相比于大多数分页监听,BRVAH BRV 不同,本模块将数据和ui分离,支持MVVM/MVP/MVC。
- 分页数据纯Kotlin实现,不依赖ViewModel/LiveData,分页Ui逻辑单独实现,切换到Compose 时无需更改数据
LoadMoreData类似于Pager,提供数据加载和存储功能。
使用时建议根据项目分页逻辑封装一下,大多数是pageIndex,pageSize 的模式,可按照以下封装:
- PageProgress 默认分页进度,pageIndex,pageSize 的模式
fun <T> ViewModel.PageLoadMoreData(fetcher: LoadMoreDataFetcher<T, PageProgress>) =
LoadMoreData(viewModelScope, PageProgress(), fetcher)
在ViewModel中定义加载:
-
loadMoreData.fetchDistance 设置下拉加载距离,0表示不会触发下拉加载
-
loadMoreData.reload() 重新加载数据
-
loadMoreData.retry() 重试加载
-
loadMoreData.loadMore() 加载下一页
-
loadMoreData.source 数据源
val projects = PageLoadMoreData<ProjectBean> {
ProjectRepository.getProjects(it.pageIndex, it.pageSize).data.datas
}
fun reload() {
projects.reload()
}
在非MVVM架构中,只需把CoroutineScope使用LifecycleScope即可
主要实现了数据的显示逻辑和检测数据需要加载更多的逻辑
-
adapter.setupLoadMoreModule() 拓展方法添加分页模块
-
loadMoreModule.setDataSource() 设置数据源
-
loadMoreModule.reload() 重新加载
-
loadMoreModule.retry() 错误重试
-
loadMoreModule.loadStatus 获取当前加载状态
-
loadMoreModule.addLoadMoreStatusListener 监听加载状态
val loadMoreModule = dataAdapter.setupLoadMoreModule()
loadMoreModule.setDataSource(lifecycleScope, vm.projects.source)
loadMoreModule.reload()//启动加载第一页
加载状态有4种:
-
LoadMoreStatus.Idle 空闲
-
LoadMoreStatus.NoMore 无更多数据
-
LoadMoreStatus.Fail 加载失败
-
LoadMoreStatus.Loading 加载中
加载状态布局可以用一个SingleViewBindingAdapter 来完成,然后监听状态变更刷新内容,本库也提供了拓展方法来简化创建
- loadMoreModule.createLoadMoreStatusAdapter() 创建一个Adapter,同时根据LoadMoreStatus去更新Footer内容 。
module.createLoadMoreStatusAdapter(FooterProjectBinding::inflate) {
when (val status = module.loadStatus) {
is LoadMoreStatus.Fail -> {
isGone = false
//加载失败,显示加载失败、
itemBinding.root.showChildOnly { it == itemBinding.loadError }
itemBinding.loadError.setOnClickListener {
//点击重试
module.retry()
itemBinding.root.showChildOnly { it == itemBinding.progressBar }
//手动显示加载中
}
}
LoadMoreStatus.Idle -> {
//空闲,隐藏所有view
isGone = true
}
is LoadMoreStatus.Loading -> {
isGone = false
//加载中,显示加载中
itemBinding.root.showChildOnly { it == itemBinding.progressBar }
}
is LoadMoreStatus.NoMore -> {
if (status.isReload) {
isGone = true
//第一页就没有数据,我们有空布局了,所以不需要显示[没有更多数据了]。
return@createLoadStateAdapter
}
isGone = false
//没有更多数据了,显示没有更多数据
itemBinding.root.showChildOnly { it == itemBinding.reachEnd }
}
}
}
空布局本质上也是createLoadStateAdapter去实现的,当数据为空时,且加载状态为空闲时显示此布局
- loadMoreModule.createEmptyStateAdapter() 可以快速创建一个空布局。
需要注意,空布局和加载布局同时使用时,加载布局需要特殊处理一下判断是第一页就不需要显示了,以免 "空布局" 和 "没有更多数据了" 同时出现
loadMoreAdapterModule.createEmptyStateAdapter(LayoutEmptyBinding::inflate) {}
加载状态布局和空布局一般在项目中一般是统一的,可以封装成项目通用Adapter
fun CommonAdapter(loadMoreAdapterModule: LoadMoreAdapterModule<*, *>) =
ImageHeaderAdapter() +
loadMoreAdapterModule.adapter +
ProjectLoadMoreState(loadMoreAdapterModule) +
ProjectEmptyAdapter(loadMoreAdapterModule)
在需要特殊的Adapter时,重新组合成新的即可
fun SpecialAdapter(loadMoreAdapterModule: LoadMoreAdapterModule<*, *>) =
ImageHeaderAdapter() +
ProjectHeaderAdapter() +
loadMoreAdapterModule.adapter +
ProjectLoadMoreState(loadMoreAdapterModule) +
ProjectEmptyAdapter(loadMoreAdapterModule) +
ProjectFooterAdapter()
GridLayoutManager 本身提供了SpanSizeLookup来精确设置其跨度
- gridLayoutManager.configFullSpan() 配置某个position 是否需要单独占用一行的情况
需要注意的是RecyclerView/LayoutManager 中的position是全局的,在使用了ConcatAdapter时,提供了一些拓展方法便于转换
-
concatAdapter.getAdapterByItemPosition(position) Adapter 可以获取该position所属的Adapter
-
concatAdapter.findLocalPositionAt(adapter,globalPosition) 把全局位置转换为子Adapter的相对位置
binding.list.layoutManager = GridLayoutManager(this, 3).apply {
configFullSpan { concatAdapter.getAdapterByItemPosition(it) != dataAdapter } //除了数据外,其他Adapter单独占用1行
}
StaggeredGridLayoutManager 中无法精确设置跨度,而是通过StaggeredGridLayoutManager.LayoutParams的isFullSpan属性去设置满跨度
-
concatAdapter.adapters 获取ConcatAdapter的所有子Adapter
-
adapter.setFullSpan() 设置adapter中的所有item/部分item为满跨度
binding.list.adapter = LoadMoreAdapters.CommonAdapter(loadMoreModule).apply {
adapters.filterIsInstance<MultiTypeBindingAdapter<Any, ViewBinding>>()
.filterNot { it == dataAdapter }//除了数据外,其他Adapter单独占用1行
.forEach { it.setFullSpan() }
}
binding.list.layoutManager = StaggeredGridLayoutManager(3, RecyclerView.VERTICAL)
和动态加载模块基本一致,但是需要继承PagingBindingAdapter
选择模块一般有2种实现,
实现1:标记到Item中增加属性来标记是否选中,这种方式访问速度快,但是需要修改Item类,内存占用较高,侵入性强
实现2:通过使用HashMap记录选中下标,这种方式访问较慢,但无需修改Item类侵入性弱,本库采用此方式实现。
同时,不管是实现1还是实现2:对数据变更的兼容新都不太好。因此增加了通过Item id来保存选中状态。
- adapter.setupSingleSelectModule() 单选模块
- selectModule.clearSelected() 清空选择
- selectModule.selectIndex 设置/获取选择下标
- selectModule.doOnSelectChange() 监听选择变化,包括用户点击和代码设置导致的变化
- selectModule.doOnUserSelect() 监听/拦截用户点击选择事件
val selectModule = dataAdapter.setupSingleSelectModule()
selectModule.clearSelected()//清空选择
selectModule.selectIndex = 1 //设置选择下标
selectModule.selectedItem //获取当前选择item
selectModule.doOnSelectChange {
//监听选择变化
}
selectModule.doOnUserSelect { position, currentSelected -> true }//监听/拦截用户点击选择
- adapter.setupSingleSelectModuleByKey() 单选模块,以Item的id来标记选择,来使得数据变化后避免丢失Item
val selectModule = dataAdapter.setupSingleSelectModuleByKey { it.id }
- dataAdapter.setupMultiSelectModule() 多选模块
- selectModule.clearSelected() 清空选中
- selectModule.selectAll() 全选
- selectModule.invertSelected() 反选
- dataAdapter.setupMultiSelectModuleByKey() 多选模块,以Item的id来标记选择,来使得数据变化后避免丢失Item
val selectModule = dataAdapter.setupMultiSelectModule()
val selectModule = dataAdapter.setupMultiSelectModuleByKey { it.id }
如果需要对选中的item进行ui上的变更,在adapter中使用isItemSelected (单选和多选都是)
- viewHolder.isItemSelected 当前Item是否选中
private val adapter = BindingAdapter(ItemTestBinding::inflate, TestData.stringList()) { _, item ->
itemBinding.tips.setTextColor(if (isItemSelected) Color.BLUE else Color.BLACK)
}
- recyclerView.setupStickItemModule() Item 悬浮模块,配置需要悬浮的position
recyclerView.setupStickItemModule() {
val position = concatAdapter.findLocalPositionAt(adapter1, it)//设置adapter1中的数据每隔10个进行悬浮
position != RecyclerView.NO_POSITION && (position) % 10 == 0
}
注意: 对Item进行悬浮,因为悬浮的Item和列表的Item不是同一个view,因此需要规范绑定数据(将状态存储在Item中,而不是View中),以免同步不到悬浮的View
adapter 需使用ConcatAdapter(原因不明。。。) , 需要悬浮的Item 需实现StickyHeader接口
binding.list.layoutManager = StickyLayoutManager(this) { adapter.data }
某些情况头部布局比较复杂,且并不属于RecyclerView的Item,本库单独实现了一个StickContainerLayout 来帮助此类。
- 添加StickContainerLayout 到xml 布局中
-
app:stick_scroll_mode=""
控制滚动头部相比较于内容的优先级(before_scroll_up、before_scroll_down、after_scroll_up、after_scroll_down的组合) -
app:stick_mode="sticking_all"
配置多个粘性头部的模式(sticking_all:所有的粘性布局依次显示,sticking_latest:最后一个粘性将上一个顶上去) -
stickContainerLayout.stickMode 配置多个粘性头部的模式,StickingLatest:最后一个粘性将上一个顶上去,StickingAll 所有的粘性布局依次显示
-
stickContainerLayout.stickScrollMode 滚动顺序配置:
CONSUME_BEFORE_CONTENT_SCROLL_UP 向上滑动content前,先隐藏header CONSUME_BEFORE_CONTENT_SCROLL_DOWN 向下滑动content前,先显示header CONSUME_AFTER_CONTENT_SCROLL_UP 向上滑动content后,再隐藏header CONSUME_AFTER_CONTENT_SCROLL_DOWN 向下滑动content后,再显示header CONSUME_PREFER_CONTENT_SHOW 总是优先显示内容,向上时,先隐藏header,向下时,等内容滚动往再显示header(默认) CONSUME_BEFORE_CONTENT 总是优先操作header
- 配置子布局
-
app:stick_type="header_scroll"
需要滚动的头部布局 -
app:stick_type="header_stick"
需要悬浮的头部布局 -
app:stick_type="content"
内容布局(必须)
<me.lwb.adapter.sticklayout.StickContainerLayout
app:stick_scroll_mode="before_scroll_up|after_scroll_down" app:stick_mode="sticking_all">
<TextView app:stick_type="header_scroll" />
<CheckBox app:stick_type="header_stick" />
<ImageView app:stick_type="header_scroll" />
<com.google.android.material.tabs.TabLayout app:stick_type="header_stick" />
<androidx.viewpager2.widget.ViewPager2 app:stick_type="content" />
</me.lwb.adapter.sticklayout.StickContainerLayout>
支持ViewPager2和PagerSnapHelper 模式
直接设置到adapter中即可
viewPager2.adapter = adapter
如果项目中原来是RecyclerView 需要改造成无限滚动,且自动在activity resume时滚动的ViewPager,可按如下:
利用PagerSnapHelper 实现的 ViewPagerModule
-
recyclerView.setupViewPagerModule() 设置recyclerView设置为ViewPager模式
-
viewPagerModule.setupAutoScrollModule() 添加自动滚动模块
-
autoScrollModule.bindLifecycle() 设置自动滚动模块绑定生命周期
recyclerView.adapter = infiniteAdapter
infiniteAdapter.setupInfiniteDataModule() //adapter设置为无限数据
recyclerView.scrollToPosition(infiniteAdapter.itemCount / 2)//滚动到中间
val viewPagerModule = recyclerView.setupViewPagerModule()
val autoScrollModule = viewPagerModule.setupAutoScrollModule()//设置ViewPager添加自动滚动
autoScrollModule.bindLifecycle(this)//设置自动滚动模块绑定生命周期
绝大多数的WheelView对自定义布局的支持性都不好。 本库利用RecyclerView实现的WheelView能通过Adapter配置其布局
-
recyclerView.setupWheelModule() 设置滚轮模块
-
wheelModule.offset 滚轮上下留白的Item数量,(具体宽度以Adapter的第一个Item为主)
-
wheelModule.orientation 方向横向或竖向,RecyclerWheelViewModule.HORIZONTAL/RecyclerWheelViewModule.VERTICAL
-
wheelModule.setWheelDecoration() 设置分割线
-
wheelModule.flingVelocityFactor fling速度控制
-
wheelModule.selectedPosition 获取/设置当前选中
-
wheelModule.onScrollingSelectListener 监听当前滚动变化
-
wheelModule.onSelectChangeListener 监听当前选中变化
val wheelModule = recyclerView.setupWheelModule()
wheelModule.apply {
offset = 2 //上下偏移多少
orientation = RecyclerWheelViewModule.HORIZONTAL//方向
setWheelDecoration(DefaultWheelDecoration(10.dp, 10.dp, 2.dp, "#dddddd".toColorInt()))//分割线
}
如果需要对选中的item进行ui上的变更,在adapter中使用isWheelItemSelected
- viewHolder.isWheelItemSelected 当前Item是否选中
private val adapter =
BindingAdapter<String, ItemWheelVerticalBinding>(ItemWheelVerticalBinding::inflate) { _, item ->
itemBinding.text.setTextColor(
if (isWheelItemSelected) Color.BLACK else Color.GRAY
)
}
LinkageWheelView 帮助多个WheelView进行联动
- linkageWheelView.setAdapterFactory() 设置Adapter工厂,决定了WheelView的样式(必须)
- linkageWheelView.setData() 设置数据,决定了WheelView的数据来源(必须)
- linkageWheelView.currentPositions 获取/设置当前所有滚轮选中位置
- linkageWheelView.currentItems 获取当前所有滚轮选中内容
- linkageWheelView.onSelectListener 当前滚轮选中变化回调
- linkageWheelView.wheelOffset 滚轮上下留白的Item数量
- linkageWheelView.wheelDecoration 设置分割线
linkageWheelView.setAdapterFactory { WheelAdapter() }
//设置每一级的数据加载策略
linkageWheelView.setData {
provideData { provinces.map { it.name } }
provideData { provinces[it[0].selectedPosition].cities.map { it.name } }
provideData { provinces[it[0].selectedPosition].cities[it[1].selectedPosition].counties }
}
其中标注(无侵入)
的属于独立拓展模块,一般在代码中,以xxx.setupXxxModule(),来插入该模块。
独立拓展模块 依赖BindingAdapter的公开方法,独立拓展模块之间也无相互依赖关系,也不需要调用其私有方法,因此需要拓展时,拷贝一份到自己项目中不会有报错, 也无需修改BindingAdapter内部实现。