前语

需求很简略也很常见,比方有一个数据列表RecyclerView,需求用户去点击挑选一个或多个数据。

完成单选的时分往往简略下标记载完事,完成多选的时分就略微杂乱去处理调集和选中。跟着项目选中需求增多,不同的当地有了不同的完成,难以保护。

因而本文规划和完成了简略的挑选模块去处理此类需求。

本文完成的挑选模块主要有以下特色:

  • 不需求改动Adapter,ViewHolder,Item,低耦合
  • 单选,可监听挑选改变,手动设置挑选方位,支撑装备再次点击撤销挑选
  • 多选,支撑全选,反选等
  • 支撑数据改变后记载原挑选

项目地址 BindingAdapter

效果

RecyclerView 低耦合单选、多选模块实现
RecyclerView 低耦合单选、多选模块实现
RecyclerView 低耦合单选、多选模块实现
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. 用户点击能够选中列表的一个元素 。
  2. 当挑选另1个数据会自动撤销当时现已选中的,也便是最多选中1个。
  3. 再次点击现已选中的元素撤销选中(可装备)。

依据记载选中数据的不同,能够分为下标形式和标识形式,他们各有优缺点。

下标形式

通常状况咱们都会这样完成。运用一个记载选中下标的变量selectIndex去标识当时挑选,selectIndex=-1表明没有选中任何元素。

原理尽管简略,那么问题来了,变量selectIndex应该放在哪里呢? Adapter?Fragment?Activity?

往往许多人都会挑选放在Adapter,觉得数据选中和数据放一同嘛。

完成是完成了,可是往往有更多问题:

  1. 给一个列表增加数据挑选功用,需求改造Adapter,侵入性强。
  2. 我要给别的一个列表增加数据挑选功用,需求再完成一遍,难复用。
  3. 去除数据挑选功用,又需求再改动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供给的接口,使得增加挑选模块几乎是拔插式的。 一起,因为RadioGroupTabLayout更新数据麻烦,需求重写removeadd。因而许多状况下RecyclerView也能够替代RadioGroupTabLayout运用

本文的完成和Demo均可在项目中找到。

项目地址 BindingAdapter