Android 简单实现联系人列表+字母索引联动效果

embedded/2024/12/22 18:44:11/

请添加图片描述
效果如上图。

Main Ideas

  1. 左右两个列表
  2. 左列表展示人员数据,含有姓氏首字母的 header item
  3. 右列表是一个全由姓氏首字母组成的索引列表,点击某个item,展示一个气泡组件(它会自动延时关闭), 左列表滚动并显示与点击的索引列表item 相同的 header
  4. 搜索动作后,匹配人员名称中是否包含搜索字符串,或搜索字符串为单一字符时,是否能匹配到某个首字母;而且滚动后,左右列表都能滚动至对应 Header 或索引处。

Steps

S1. 汉字拼音转换

先找到了 Pinyin4J 这个库;后来发现没有对多音字姓氏 的处理;之后找到 TinyPinyin ,它可以自建字典,指明多音汉字(作为姓氏时)的指定读音。

fun initChinaNamesDictMap() {// 增加 多音字 姓氏拼音词典Pinyin.init(Pinyin.newConfig().with(object : PinyinMapDict() {override fun mapping(): MutableMap<String, Array<String>> {val map = hashMapOf<String, Array<String>>()map["解"] = arrayOf("XIE")map["单"] = arrayOf("SHAN")map["仇"] = arrayOf("QIU")map["区"] = arrayOf("OU")map["查"] = arrayOf("ZHA")map["曾"] = arrayOf("ZENG")map["尉"] = arrayOf("YU")map["折"] = arrayOf("SHE")map["盖"] = arrayOf("GE")map["乐"] = arrayOf("YUE")map["种"] = arrayOf("CHONG")map["员"] = arrayOf("YUN")map["繁"] = arrayOf("PO")map["句"] = arrayOf("GOU")map["牟"] = arrayOf("MU") // mù、móu、mūmap["覃"] = arrayOf("QIN")map["翟"] = arrayOf("ZHAI")return map}}))
}

// Pinyin.toPinyin(char) 方法不使用自定义字典
而使用 Pinyin.toPinyin(nameText.first().toString(), ",").first() // 将 nameText 的首字符,转为拼音,并取拼音首字母


S2. 数据bean 和 item view

对原有数据bean 增加 属性:

data class DriverInfo(var Name: String?, // 人名var isHeader: Boolean, // 是否是 header itemvar headerPinyinText: String? // header item view 的拼音首字母
)

左列表的 item view,当数据是 header时,仅显示 header textView (下图红色的文字),否则仅显示 item textView (下图黑色的文字):
在这里插入图片描述
右列表的 item view,更简单了,就只含一个 TextView 。


S3. 处理数据源

这一步要做的是:转拼音;拼音排序;设置 isHeader、headerPinyinText 属性;构建新的数据源集合 …

// 返回新的数据源
fun getPinyinHeaderList(list: List<DriverInfo>): List<DriverInfo> {list.forEachIndexed { index, driverInfo ->if (driverInfo.Name.isNullOrEmpty()) return@forEachIndexed// Pinyin.toPinyin(char) 方法不使用自定义字典val header = Pinyin.toPinyin(driverInfo.Name!!.first().toString(), ",").first()driverInfo.headerPinyinText = header.toString()}// 以拼音首字母排序(list as MutableList).sortBy { it.headerPinyinText }val newer = mutableListOf<DriverInfo>()list.forEachIndexed { index, driverInfo ->val newHeader = index == 0 || driverInfo.headerPinyinText != list[index - 1].headerPinyinTextif (newHeader) {newer.add(DriverInfo(null, true, driverInfo.headerPinyinText))}newer.add(driverInfo)}return newer
}

当左侧列表有了数据源之后,那右侧的也就可以有了:将所有 header item 的 headerPinyinText 取出,并转为 新的 集合。

val indexList = driverList?.filter { it.isHeader }?.map { it.headerPinyinText ?: ""} ?: arrayListOf()
indexAdapter.updateData(indexList)

S4. Adapter 的点击事件

这里省略设置 adapter 、LinearLayoutManager 等 样板代码 …

设定:左侧的适配器名为 adapter, 右侧字母索引名为 indexAdapter;左侧 RV 名为 recyclerView,右侧的名为了 rvIndex。

  1. 左侧的点击事件,不会触发右侧的联动
override fun onItemClick(position: Int, data: DriverInfo) {if (data.isHeader) return// 如果是点击 item, 做自己的业务
}
  1. 右侧点击事件,会触发左侧的滚动;还可以触发气泡 view 的显示,甚至自身的滚动
override fun onItemClick(position: Int, data: DriverInfo) {val item = adapter.dataset.first { it.isHeader && it.headerPinyinText == data }val index = adapter.dataset.indexOf(item)
//  mBind.recyclerView.scrollToPosition(index)(mBind.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(index, 0)//  mBind.rvIndex.scrollToPosition(position)val rightIndex = indexAdapter.dataset.indexOf(item.headerPinyinText)(mBind.rvIndex.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(rightIndex, 0)showBubbleView(position, data)
}

一开始用 rv#scrollToPosition(),发现也能滚动。但是呢,指定 position 之后还有其它内容时,且该位置之前也有很多的内容;点击后,仅将 该位置 item ,显示在页面可见项的最后一个位置。 改成 LinearLayoutManager#scrollToPositionWithOffset()后,更符合预期。


S5. 气泡 view

<widget.BubbleViewandroid:id="@+id/bubbleView"android:layout_width="100dp"android:layout_height="100dp"android:visibility="invisible"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="@id/rv_index" />

设置 文本;获取点击的 item view;根据 item view 的位置 进行显示设置;延迟1秒 隐藏气泡:

private fun showBubbleView(position: Int, data: String) {lifecycleScope.launch {mBind.bubbleView.setText(data)val itemView = mBind.rvIndex.findViewHolderForAdapterPosition(position)?.itemView ?: return@launchmBind.bubbleView.showAtLocation(itemView)delay(1000)mBind.bubbleView.visibility = View.GONE}
}

自定义 气泡 view:

/*** desc:    指定view左侧显示的气泡view* author:  stone* email:   aa86799@163.com* time:    2024/9/27 18:22*/
class BubbleView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {  private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {  color = resources.getColor(R.color.syscolor)style = Paint.Style.FILL  }  private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {  color = Color.WHITE  textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50f, resources.displayMetrics)textAlign = Paint.Align.CENTER  }  private val path = Path()  private var text: String = ""  fun setText(text: String) {  this.text = text  invalidate()  }  override fun onDraw(canvas: Canvas) {  super.onDraw(canvas)  // 绘制贝塞尔曲线气泡  path.reset()  path.moveTo(width / 2f, height.toFloat())  path.quadTo(width.toFloat(), height.toFloat(), width.toFloat(), height / 2f)  path.quadTo(width.toFloat(), 0f, width / 2f, 0f)  path.quadTo(0f, 0f, 0f, height / 2f)  path.quadTo(0f, height.toFloat(), width / 2f, height.toFloat())  path.close()  canvas.drawPath(path, paint)  // 绘制文本  canvas.drawText(text, width / 2f, height / 2f + textPaint.textSize / 3, textPaint)}fun showAtLocation(view: View) {view.let {val location = IntArray(2)it.getLocationOnScreen(location)// 设置气泡的位置x = location[0] - width.toFloat() - 10y = location[1] - abs(height - it.height) / 2f - getStatusBarHeight()visibility = View.VISIBLE}}private fun getStatusBarHeight(): Int {var result = 0val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")if (resourceId > 0) {result = resources.getDimensionPixelSize(resourceId)}return result}
}

S6. 搜索实现

  • 空白输入字符时,左侧返回全数据源;右侧列表跟随左侧变化。
  • 有输入时,根据全数据源,获取 匹配的子数据源;右侧列表跟随左侧变化。
fun filterTextToNewHeaderList(list: List<DriverInfo>?, text: String): List<DriverInfo>? {// 如果item 的拼音和 查询字符 相同;或者,非 header 时,名称包含查询字符val filterList = list?.filter { it.headerPinyinText?.equals(text, true) == true|| !it.isHeader && it.Name?.contains(text, ignoreCase = true) == true }if (filterList.isNullOrEmpty()) {return null}val newer = mutableListOf<DriverInfo>()filterList.forEachIndexed { index, driverInfo ->val newHeader = (index == 0 || driverInfo.headerPinyinText != filterList[index - 1].headerPinyinText) && !driverInfo.isHeaderif (newHeader) {newer.add(DriverInfo(null, true, driverInfo.headerPinyinText))}newer.add(driverInfo)}return newer
}// 搜索点击
mBind.tvSearch.setOnClickListener {val beanList: List<DriverInfo>? = adapter.filterTextToNewHeaderList(driverList, text)adapter.updateData(beanList)val indexList = beanList.filter { it.isHeader }.map { it.headerPinyinText ?: ""}indexAdapter.updateData(indexList)
}

整体核心实现都贴出来了,如果有什么bug,欢迎回复


http://www.ppmy.cn/embedded/121207.html

相关文章

SpringCloud学习路线

目录 基础篇 Spring Cloud核心组件 进阶篇 实战与优化 基础篇 1.理解微服务概念 微服务架构的基本理念、优缺点、适用场景。与传统单体架构的对比分析。 2.Spring Boot基础 掌握Spring Boot的基本使用&#xff0c;包括自动配置、Starter依赖、Actuator监控等。实战一个简单…

【理论科学与实践技术】数学与经济管理中的学科与实用算法

在现代商业环境中&#xff0c;数学与经济管理的结合为企业提供了强大的决策支持。包含一些主要学科&#xff0c;包括数学基础、经济学模型、管理学及风险管理&#xff0c;相关的实用算法和这些算法在中国及全球知名企业中的实际应用。 一、数学基础 1). 发现人及著名学者 发…

[大语言模型-论文精读] 阿里巴巴-通过多阶段对比学习实现通用文本嵌入

[大语言模型-论文精读] 阿里巴巴达摩院-GTE-通过多阶段对比学习实现通用文本嵌入 1. 论文信息 这篇论文《Towards General Text Embeddings with Multi-stage Contrastive Learning》介绍了一种新的文本嵌入模型&#xff0c;名为GTE&#xff08;General-purpose Text Embeddin…

过滤器 Filter 详解

想象一下&#xff0c;你正在搭建一个网站&#xff0c;用户需要登录才能访问某些页面。你肯定不希望未经授权的用户能够随意浏览或修改重要信息&#xff0c;这时&#xff0c;你就需要一个“安全门”来保护你的网站&#xff0c;而 Java Filter 就是扮演这个角色的最佳人选&#x…

MFC工控项目实例二十一型号选择界面删除参数按钮禁用切换

承接专栏《MFC工控项目实例二十手动测试界面模拟量输入实时显示》 对于禁止使用的删除、参数按钮&#xff0c;在选中列表控件选项时切换为能够使用。 1、在TypDlg.h文件中添加代码 #include "ShadeButtonST.h" #include "BtnST.h" class CTypDlg : publi…

RabbitMQ 实验入门

使用 spring-amqp 实验 发布订阅模型 fanoutExchange 实验 实验步骤&#xff1a; 编写定义 队列 和 交换机 绑定关系的代码创建接口&#xff0c;模拟生产者&#xff0c;方便调试&#xff08;接受参数 队列名、路由键、[消息]&#xff09;定义消费者 代码示例&#xff1a; C…

[leetcode刷题]面试经典150题之9python哈希表详解(知识点+题合集)

为了方便理解哈希表&#xff0c;我们先从python中的字典讲起。 字典 (Dictionary) 字典是 Python 中一种内置的数据结构&#xff0c;它是一种 键值对&#xff08;key-value pair&#xff09;存储形式。每个键&#xff08;key&#xff09;都有一个对应的值&#xff08;value&a…

XPath基础知识点讲解——用于在XML中查找信息的语言

1. 什么是XPath&#xff1f; XPath&#xff08;XML Path Language&#xff09;是用于在XML&#xff08;Extensible Markup Language&#xff09;文档中查找信息的语言。它可以通过路径表达式来选择XML文档中的节点&#xff0c;类似于如何在文件系统中使用路径查找文件。XPath是…