RecyclerView 低耦合单选、多选模块实现

news2024/11/18 13:33:47

作者:丨小夕

前言

需求很简单也很常见,比如有一个数据列表RecyclerView,需要用户去点击选择一个或多个数据。

实现单选的时候往往简单下标记录了事,实现多选的时候就稍微复杂去处理集合和选中。随着项目选中需求增多,不同的地方有了不同的实现,难以维护。

因此本文设计和实现了简单的选择模块去解决此类需求。

本文实现的选择模块主要有以下特点:

  • 不需要改动Adapter,ViewHolder,Item,低耦合
  • 单选,可监听选择变化,手动设置选择位置,支持配置再次点击取消选择
  • 多选,支持全选,反选等
  • 支持数据变化后记录原选择

效果

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使用

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/670274.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

k8s的部署

二进制搭建 Kubernetes v1.20 k8s集群master01&#xff1a;192.168.92.30 kube-apiserver kube-controller-manager kube-scheduler etcd k8s集群master02&#xff1a;192.168.92.21 k8s集群node01&#xff1a;192.168.92.40 kubelet kube-proxy docker k8s集群node02…

阿里云热修复打补丁包注意事件

1、每次发布app到应用市场前&#xff0c;注意保存没有加固前的apk文件和mapping.txt 2、修复好bug&#xff0c;打包app前&#xff0c;要做的事情 &#xff08;1)先把有问题的apk的mapping.txt文件复制到/app路径下 (2)修改混淆配置&#xff1a;将-printmapping mapping.txt使…

Android蓝牙协议知识汇总

蓝牙协议下载 蓝牙技术联盟网址&#xff1a;https://www.bluetooth.com/ 在这个网址搜索&#xff0c;比如&#xff1a; 在搜索结果中找到蓝牙协议规范&#xff1a; 点击上面网址&#xff1a; 蓝牙手册里包含了部分核心协议&#xff0c;比如L2CAP、SDP、ATT、GATT&#x…

Python 100%解析svg-captcha验证码

前言 前段时间接到一个需求&#xff0c;登陆某一个网站&#xff0c;然后录入数据&#xff1b;本来以为是一个很简单的需求&#xff0c;结果遇到几个难点&#xff1a; 登陆的时候需要有验证码验证码是一个请求路径&#xff0c;每请求一次验证码都不一样 本来一开始以为是常用的…

探究 CoreData 使用索引(Index)机制加速查表究竟如何实现?

问题现象 在  App 的开发中,CoreData 到底能不能用索引机制(Index)来加速查表?如果可以,又该如何创建和使用索引呢? 这是一个连  官方文档都模棱两可,Stackoverflow 里诸多大神都闪烁其词的话题。 在本篇博文中,您将学到如下内容: 什么是 CoreData 索引(Index…

SpringBoot + Ant Design Vue实现数据导出功能

SpringBoot Ant Design Vue实现数据导出功能 一、需求二、前端代码实现2.1 显示实现2.2 代码逻辑 三、后端代码实现3.1 实体类3.2 接收参数和打印模板3.3 正式的逻辑3.4 Contorller 一、需求 以xlsx格式导出所选表格中的内容要求进行分级设置表头颜色。 二、前端代码实现 2…

20230524 taro+vue3+webpack5+pdfjs时打包pdfjs进不来的问题

关闭taro的terser就可以了 terser:{enable:false }

UE中创建异步任务编辑器工具(Editor Utility Tasks)

在UE中我们往往需要执行一些编辑器下的异步任务&#xff0c;例如批量生成AO贴图、批量合并静态模型等&#xff0c;又不想阻碍主线程&#xff0c;因此可以使用Editor Utility Tasks直接创建UE编辑器下的异步任务。 如果你不太了解UE编辑器工具&#xff0c;可以参考这篇文章&…

Spring Boot 中自定义数据校验注解

Spring Boot 中自定义数据校验注解 在 Spring Boot 中&#xff0c;我们可以使用 JSR-303 数据校验规范来校验表单数据的合法性。JSR-303 提供了一些常用的数据校验注解&#xff0c;例如 NotNull、NotBlank、Size 等。但是&#xff0c;在实际开发中&#xff0c;我们可能需要自定…

2023年6月24日(星期六):骑行明郎

2023年6月24日(星期六)&#xff1a;骑行明郎&#xff0c;早8:30到9:00&#xff0c; 大观公园门囗集合&#xff0c;9:30点准时出发 【因迟到者&#xff0c;骑行速度快者&#xff0c;可自行追赶偶遇。】 偶遇地点: 大观公园门囗集合&#xff0c;家住南&#xff0c;东&#xff0c…

(二叉树) 100. 相同的树 ——【Leetcode每日一题】

❓100. 相同的树 难度&#xff1a;简单 给你两棵二叉树的根节点 p 和 q&#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 示例 1&#xff1a; 输入&#xff1a;p [1,2,3], q …

使用代理ip做网页抓取需要注意什么

现在&#xff0c;很多公司为达成目标&#xff0c;都需要抓取大量数据。企业需要根据数据来作出重大决定&#xff0c;因此掌握准确信息至关重要。互联网上有许多宝贵的公共数据。问题是如何轻松采集这些数据&#xff0c;而无需让团队整天手动复制粘贴所需信息?网页抓取的定义越…

Qt学习11:Dialog对话框操作总结

文章目录 QDialogQDialogButtonBoxQMessageBoxQFileDialogQFontDialogQColorDialogQInputDialogQProgressDialog 文章首发于我的个人博客&#xff1a;欢迎大佬们来逛逛 QDialog Qt中使用QDialog来实现对话框&#xff0c;QDialog继承自QWidget&#xff0c;对话框分为**三种**&…

尿的唰唰和笑的哈哈

很多人说看不懂&#xff0c;不知道哪个是真哪个是假。我说都是真的。不同心不同理。全球并不同炎凉。窦唯有句歌词&#xff1a;天堂地狱皆在人间。何勇有句歌词&#xff1a;有人减肥&#xff0c;有人饿死没粮。&#xff08;1&#xff09;产业我过去说过顶天立地。立地&#xff…

专利背后的故事 | 一种异常信息检测方法和装置

Part01 专利发明的初衷 用户和实体行为分析&#xff08;UEBA&#xff09;在2018年入选Gartner为安全团队建议的十大新项目。UEBA近几年一直受到国内安全厂商的热捧。但是对于UEBA的理解&#xff0c;以及具体落实的产品方案&#xff0c;各厂商虽然明显不同&#xff0c;但在对账…

Go应用性能优化的8个最佳实践,快速提升资源利用效率!

作者&#xff5c;Ifedayo Adesiyan 翻译&#xff5c;Seal软件 链接&#xff5c;https://earthly.dev/blog/optimize-golang-for-kubernetes/ 优化服务器负载对于确保运行在 Kubernetes 上的 Golang 应用程序的高性能和可扩展性至关重要。随着企业越来越多地采用容器化的方式和 …

HOOPS Native Platform 2023 cRACK

将高级 3D 工作流程添加到桌面和移动应用程序 HOOPS 原生平台集成了三种用于桌面和移动应用程序开发的先进 HOOPS 技术&#xff0c;包括高性能图形 SDK、CAD 数据访问工具包和 3D 数据发布 API。 ​ ​ 构建 3D 原生应用 借助桌面和移动设备上的 HOOPS 原生平台&#xff0c;快…

一个初级程序员该在哪接项目练手?

作为一个初级程序员&#xff0c;想要通过兼职接单赚钱&#xff0c;离不开项目练手。但不得不说&#xff0c;初级程序员想要通过接私活获取收入还是相对比较困难的&#xff0c;如果对接私活比较感兴趣的朋友&#xff0c;可以参考这条路径&#xff1a; 在GitHub上学习大佬的项目…

【WebLogic】WebLogic 10.3.6.0部署应用包后报错

问题背景&#xff1a; WebLogic 10.3.6.0部署应用包后出现报错【posted content exceeds max post size】&#xff0c;此报错会导致应用部署的目标服务实例无法成功启动。 报错信息截图如下所示&#xff1a; 根据报错信息&#xff0c;查询相关MOS文档&#xff0c;发现问题原因是…

网络能成为AI加速器吗

网络能成为AI加速器吗 摘要 人工神经网络&#xff08;NNs&#xff09;在许多服务和应用中扮演越来越重要的角色&#xff0c;并对计算基础设施的工作负载做出了重要贡献。在用于延迟敏感的服务时&#xff0c;NNs通常由CPU处理&#xff0c;因为使用外部专用硬件加速器会效率低下…