前语
需求很简略也很常见,比方有一个数据列表RecyclerView,需求用户去点击挑选一个或多个数据。
完成单选的时分往往简略下标记载完事,完成多选的时分就略微杂乱去处理调集和选中。跟着项目选中需求增多,不同的当地有了不同的完成,难以保护。
因而本文规划和完成了简略的挑选模块去处理此类需求。
本文完成的挑选模块主要有以下特色:
- 不需求改动
Adapter
,ViewHolder
,Item
,低耦合 - 单选,可监听挑选改变,手动设置挑选方位,支撑装备再次点击撤销挑选
- 多选,支撑全选,反选等
- 支撑数据改变后记载原挑选
项目地址 BindingAdapter
效果
import me.lwb.adapter.select.isItemSelected
class XxxActivity {
private val dataAdapter =
BindingAdapter(ItemTestBinding::inflate, TestData.stringList()) { _, item ->
itemBinding.tips.text = item
itemBinding.tips.setTextColor(if (isItemSelected) Color.BLUE else Color.BLACK)
}
fun onCreate() {
val selectModule = dataAdapter.setupSingleSelectModule()//单选
val selectModule = dataAdapter.setupMultiSelectModule()//多选
selectModule.doOnSelectChange {
}
//...全选,反选等
}
}
原理
单选
单选的特色:
- 用户点击能够选中列表的一个元素 。
- 当挑选另1个数据会自动撤销当时现已选中的,也便是最多选中1个。
- 再次点击现已选中的元素撤销选中(可装备)。
依据记载选中数据的不同,能够分为下标形式和标识形式,他们各有优缺点。
下标形式
通常状况咱们都会这样完成。运用一个记载选中下标的变量selectIndex
去标识当时挑选,selectIndex=-1
表明没有选中任何元素。
原理尽管简略,那么问题来了,变量selectIndex
应该放在哪里呢? Adapter
?Fragment
?Activity
?
往往许多人都会挑选放在Adapter
,觉得数据选中和数据放一同嘛。
完成是完成了,可是往往有更多问题:
- 给一个列表增加数据挑选功用,需求改造
Adapter
,侵入性强。 - 我要给别的一个列表增加数据挑选功用,需求再完成一遍,难复用。
- 去除数据挑选功用,又需求再改动
Adapter
,耦合重。
总结起来其实这样完成是不符合单一职责的准则,selectIndex
是数据挑选功用的数据,Adapter
是绑定UI数据的。放在一同改动一方就得牵扯到别的一方。
处理办法便是,独自抽离出挑选模块,依赖于Adapter
的接口而不是放在Adapter
中完成。
得益于BindingAdapter
供给的接口,咱们首先经过doBeforeBindViewHolder
在绑守时增加Item
点击事件的监听,然后切换selectIndex
。
咱们将需求保存的挑选数据和行为,独自放在一个模块:
class SingleSelectModule {
var selectIndex: Int
var enableUnselect: Boolean
init {
adapter.doBeforeBindViewHolder { holder, position ->
holder.itemView.setOnClickListener {
toogleSelect(position)
}
}
}
fun toggleSelect(selectedKey: Int) {
selectIndex = if (enableUnselect) {
if (isSelected(selectedKey)) {
INDEX_UNSELECTED //撤销挑选
} else {
selectedKey //切换挑选
}
} else {
selectedKey //切换挑选
}
}
//...
}
往往咱们需求在onBindViewHolder
时判别当时Item
是否选中,从而对选中和未选中的Item
显现不同的款式。
简略的完成的话能够保存SingleSelectModule
引用,然后再onBindViewHolder
中获取。
class XxActivity {
var selectModule: SingleSelectModule
val adapter =
BindingAdapter(ItemTestBinding::inflate, TestData.stringList()) { pos, item ->
val isItemSelected = selectModule.isSelected(pos)
itemBinding.tips.text = item
itemBinding.tips.setTextColor(if (isItemSelected) Color.BLUE else Color.BLACK)
}
}
但缺点便是,它又和SingleSelectModule
产生了耦合,实际上咱们只需求关怀当时Item
是否选中即可,要是能给Item
加个isItemSelected
特点就好了。
许多的挑选方案确实是这么完成的,给Item
增加特点,或许运用Pair<Boolean,Item>
去包装,这些方案又造成了必定的侵入性。
咱们从别的一个角度,不从Item
下手,而是从ViewHolder
中去改造,比方这样:
class BindingViewHolder {
var isItemSelected: Boolean
}
给ViewHolder
加特点比Item
更加通用,起码不用每个需求支撑挑选的列表都去改造Item
。
可是逻辑上需求留意:真实选中的是Item
,而不是ViewHolder
,因为ViewHolder
可能会在不同的机遇绑定到不同的Item
。
所以实际上BindingViewHolder.isItemSelected
起到一个桥接效果,
原本的onBindViewHolder
内容,是经过val isItemSelected = selectModule.isSelected(pos)
获取当时Item
是否选中,然后再去运用isItemSelected
现在咱们将变量加到ViewHolder
后,就不用每次去界说变量了。
val adapter =
BindingAdapter(ItemTestBinding::inflate, TestData.stringList()) { pos, item ->
this.isItemSelected = selectModule.isSelected(pos)
itemBinding.tips.text = item
itemBinding.tips.setTextColor(if (isItemSelected) Color.BLUE else Color.BLACK)
}
一起再把赋值isItemSelected = selectModule.isSelected(pos)
也放入到挑选模块中
class SingleSelectModule {
init {
adapter.doBeforeBindViewHolder { holder, position ->
holder.isItemSelected = this.isSelected(pos)
holder.itemView.setOnClickListener {
toogleSelect(position)
}
}
}
}
doBeforeBindViewHolder 能够在监听Adapter的onBindViewHolder,并在其前面执行
最终这儿就剩余一个问题了,给BindingViewHolder
增加isItemSelected
不是又得改ViewHolder
吗。还是造成了侵入性,
后续咱们还得增加其他模块,总不能每增加一个模块就改一次ViewHolder
吧。
那么如何动态的增加特点?
这儿咱们直接就想到了经过view.setTag/view.getTag
(本质上是SparseArray
)不就能完成动态增加特点吗,
一起运用上Kotlin的拓宽特点,那么它就成了真的”拓宽特点”了:
var BindingViewHolder<*>.isItemSelected: Boolean
set(value) {
itemView.setTag(R.id.binding_adapter_view_holder_tag_selected, value)
}
get() = itemView.getTag(R.id.binding_adapter_view_holder_tag_selected) == true
然后经过引进这个拓宽特点import me.lwb.adapter.select.isItemSelected
就能直接在Adapter中访问了,
同理你能够增加任意个拓宽特点,并经过doBeforeBindViewHolder
来在它们被运用前赋值,这些都不需求改动Adapter
或许ViewHolder
。
import me.lwb.adapter.select.isItemSelected
import me.lwb.adapter.select.isItemSelected2
import me.lwb.adapter.select.isItemSelected3
class XxActivity {
private val dataAdapter =
BindingAdapter(ItemTestBinding::inflate, TestData.stringList()) { _, item ->
//运用isItemSelected isItemSelected2 isItemSelected3
itemBinding.tips.text = item++
itemBinding.tips.setTextColor(if (isItemSelected) Color.BLUE else Color.BLACK)
}
}
下标形式非常易用,只需一行代码即可setupSingleSelectModule
,可是也有必定局限性,便是用户选中的数据是运用下标来记载的,
假如数据下标对应的数据是改变了,就往往不是咱们预期的效果,比方[A,B,C,D]
,用户挑选B
,此时selectIndex=1
,用户改写数据变成了[D,C,B,A]
,这时因为selectIndex=1
,尽管挑选的都是第2个,可是数据改变了,就变成了挑选了C
往往那么常常就只能清空挑选了。
标识形式
下标形式适用于数据不变,或许改变后清空选中的状况。
标识形式便是记载数据的仅有标识,能够在数据改变后依然选中对应的数据,一般Item
都会有一个仅有Id能够用作标识。
完成和下标形式挨近,可是需求完成获取标识的方法,而且判别选中是依据标识是否相同。
class SingleSelectModuleByKey<I : Any> internal constructor(
val adapter: MultiTypeBindingAdapter<I, *>,
val selector: I.() -> Any,
){
fun isSelected(selectedKey: I?): Boolean {
val select = selectedItem
return selectedKey != ITEM_UNSELECTED && select != ITEM_UNSELECTED && selectedKey.selector() == select.selector()
}
}
运用时指定Item
的标识:
adapter.setupSingleSelectModuleByKey { it.id }
多选
多选也分为下标形式和标识形式,原理和单选相似
下标形式
存储选中状况从下标变成了下标调集
class MultiSelectModule<I : Any> internal constructor(
val adapter: MultiTypeBindingAdapter<I, *>,
) {
private val mutableSelectedIndexes: MutableSet<Int> = HashSet();
override fun isSelected(selectKey: Int): Boolean {
return selectedIndexes.contains(selectKey)
}
override fun selectItem(selectKey: Int, choose: Boolean) {
if (choose) {
mutableSelectedIndexes.add(selectKey)
} else {
mutableSelectedIndexes.remove(selectKey)
}
notifyItemsChanged()
}
//全选
override fun selectAll() {
mutableSelectedIndexes.clear()
//增加一切索引
for (i in 0 until adapter.itemCount) {
mutableSelectedIndexes.add(i)
}
notifyItemsChanged()
}
//反选
override fun invertSelected() {
val selectStates = BooleanArray(adapter.itemCount) { false }
mutableSelectedIndexes.forEach {
selectStates[it] = true
}
mutableSelectedIndexes.clear()
selectStates.forEachIndexed { index, select ->
if (!select) {
mutableSelectedIndexes.add(index)
}
}
notifyItemsChanged()
}
}
标识形式
存储选中状况从标识变成了标识调集
class SingleSelectModuleByKey<I : Any> internal constructor(
override val adapter: MultiTypeBindingAdapter<I, *>,
val selector: I.() -> Any,
) {
private val mutableSelectedItems: MutableMap<Any, IndexedValue<I>> = HashMap()
override fun isSelected(selectKey: I): Boolean {
return mutableSelectedItems.containsKey(selectKey.selector())
}
override fun selectItem(selectKey: I, choose: Boolean) {
val id = selectKey.selector()
if (choose) {
mutableSelectedItems[id] = IndexedValue(selectKey)
} else {
mutableSelectedItems.remove(id)
}
notifyItemsChanged()
}
//全选
override fun selectAll() {
mutableSelectedItems.clear()
mutableSelectedItems.putAll(adapter.data.mapIndexed { index, it ->
it.selector() to IndexedValue(it, index)
})
notifyItemsChanged()
}
//反选
override fun invertSelected() {
val other = adapter.data
.asSequence()
.filter { it !in mutableSelectedItems }
.mapIndexed { index, it -> it.selector() to IndexedValue(it, index) }
.toList()
mutableSelectedItems.clear()
mutableSelectedItems.putAll(other)
notifyItemsChanged()
}
}
运用上也是相似的
val selectModule = dataAdapter.setupMultiSelectModule()
val selectModule = dataAdapter.setupMultiSelectModuleByKey()
总结
本文完成了在RecyclerView
中运用的独立的单选,多选模块,有下标形式和标识形式基本能满足项目中的需求。
运用BindingAdapter
供给的接口,使得增加挑选模块几乎是拔插式的。
一起,因为RadioGroup
、TabLayout
更新数据麻烦,需求重写remove
,add
。因而许多状况下RecyclerView
也能够替代RadioGroup
、TabLayout
运用
本文的完成和Demo均可在项目中找到。