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

news2024/9/30 12:49:12

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

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 item
    var 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].headerPinyinText
        if (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.BubbleView
      android: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@launch
        mBind.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() - 10
            y = location[1] - abs(height - it.height) / 2f - getStatusBarHeight()
            visibility = View.VISIBLE
        }
    }

    private fun getStatusBarHeight(): Int {
        var result = 0
        val 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.isHeader
        if (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.coloradmin.cn/o/2179660.html

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

相关文章

Solidity智能合约调用其他合约的三种主要方式

在 Solidity 中&#xff0c;智能合约之间的交互非常重要。调用其他合约的功能可以增强合约的灵活性&#xff0c;使其能够执行跨合约操作&#xff0c;比如获取数据、转移资金或触发其他合约的功能。本文将详细介绍 Solidity 中调用其他合约的不同方式及其应用场景。 1. 合约间调…

【Unity踩坑】Unity更新Google Play结算库

一、问题描述&#xff1a; 在Google Play上提交了app bundle后&#xff0c;提示如下错误。 我使用的是Unity 2022.01.20f1&#xff0c;看来用的Play结算库版本是4.0 查了一下文档&#xff0c;Google Play结算库的维护周期是两年。现在需要更新到至少6.0。 二、更新过程 1. 下…

JAVA内存模型!=JVM内存模型

文章目录 前言JVM内存模型JAVA内存模型JAVA内存模型解释的问题可见性问题一致性问题 总结 前言 有很多JAVA开发人员&#xff0c;在被问起&#xff1a;“你知道Java内存模型吗&#xff1f;”&#xff0c;都会回答&#xff1a;“知道&#xff0c;JAVA内存模型分为方法区、堆、……

悟透自己、悟透生活、悟透人生(此文无价)

很多人都会有这样的疑问&#xff1a;“为什么听了很多道理&#xff0c;却依然没有过好这一生&#xff1f;” 古人给出了这样的回答。 王阳明曾说&#xff1a;“知行合一。” 老子则言&#xff1a;“知人者智&#xff0c;自知者明。” 可见&#xff0c;一切问题的根源都出在了我…

CSDN 的 GIt 是没东西吗

虽然说吧 CSDN 的博客也就那样&#xff0c;记得去年的时候 CSDN 出了一个 Git 代码库&#xff0c;被骂得要死&#xff0c;基本上是从外面搬了一堆代码回来。 这回 CSDN 又玩了个新东西&#xff0c;干脆你可以把你的博客文章同步到你在 CSDN 开的代码库上了。 如何同步 在 CS…

数据增强之imgaug的使用

包的导入 path = r"D:\\" # sometimes = lambda aug: iaa.Sometimes(0.5, aug) img = cv2.imread("D:\\photo\\test.jpg") img = cv2.resize(img,(128,128)) # img = cv2.cvtColor(img,cv2.COLOR_RGBA2GRAY) cv2.imwrite(path+"img.jpg",img)随…

python并发编程实战

python并发编程有三种 多线程Thread多进程Process多协程Coroutine cpu密集型计算 cpu密集型也叫计算密集型&#xff0c;是指I/O在很短的时间就可以完成&#xff0c;cpu需要大量的计算处理&#xff0c;特点是cpu占用率相当高 例如&#xff1a;压缩解压缩、加密解密、正则表达…

【Qt】开发环境与下载

这里写目录标题 1 Qt的开发工具概述2 Qt的下载2.1 下载Qt SDK 3. 认识SDK中的重要工具 1 Qt的开发工具概述 Qt支持持多种开发工具&#xff0c;其中⽐较常⽤的开发工具有&#xff1a;Qt Creator、Visual Studio、Eclipse. (1) QtCreator Qt Creator 是⼀个轻量级的跨平台集成…

iot网关是什么?iot网关在工业领域的应用-天拓四方

一、IoT网关的定义 IoT网关&#xff0c;即物联网网关&#xff0c;是物联网&#xff08;IoT&#xff09;系统中的重要组成部分。它主要实现感知网络与通信网络&#xff0c;以及不同类型感知网络之间的协议转换&#xff0c;既能够支持广域互联&#xff0c;也能满足局域互联的需求…

windows系统下Telnet工具的安装步骤

通过控制面板启用Telnet客户端 点击“确定”按钮&#xff0c;按照系统提示完成安装。 打开cmd&#xff0c;输入telnet就可以了

APISIX 联动雷池 WAF 实现 Web 安全防护

Apache APISIX 是一个动态、实时、高性能的云原生 API 网关&#xff0c;提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。 雷池是由长亭科技开发的 WAF 系统&#xff0c;提供对 HTTP 请求的安全请求&#xff0c;提供完整的 API 管理和…

【盘一盘】加密软件有哪些?10款电脑文件加密软件超好用推荐!让您的数据更安全!

在信息洪流中&#xff0c;数据安全如古战场上的坚固堡垒&#xff0c;至关重要。 古人云&#xff1a;"机密深藏&#xff0c;方能安身立命。" 为此&#xff0c;我特意搜罗了10款电脑文件加密软件&#xff0c;它们如同现代版的"八卦阵"&#xff0c;既能保护…

华为/海思 Hi3516CV610 4K@20,6M@30 分辨率,1T 算力 NPU

总体介绍 Hi3516CV610 是一颗应用在安防市场的 IPC SoC 。在开放操作系统、新一代视频编解码标准、 网络安全和隐私保护、人工智能方面引领行业发 展&#xff0c;主要面向室内外场景下的枪机、球机、半球 机、海螺机、枪球一体机、双目长短焦机等产品 形态&#xff0c;打…

Spring - @Import注解

文章目录 基本用法源码分析ConfigurationClassPostProcessorConfigurationClass SourceClassgetImportsprocessImports处理 ImportSelectorImportSelector 接口DeferredImportSelector 处理 ImportBeanDefinitionRegistrarImportBeanDefinitionRegistrar 接口 处理Configuratio…

全同态加密算法概览

我们前面有谈到《Paillier半同态加密算法》&#xff0c;半同态加密算法除了支持密文加法运算的 Paillier 算法&#xff0c;还有支持密文乘法计算的 RSA 算法&#xff0c;早期的PSI(隐私求交)和PIR(匿踪查询)都有使用基于RSA盲签名技术来实现。今天我们来谈谈能够有效支持任意函…

【Git原理与使用】分支管理

分支管理 1.理解分支2.创建分支2.1创建分支2.2切换分支2.3合并分支 3.删除分支4.合并冲突4.分支管理策略5.分支策略6.bug分支7.删除临时分支8.小结 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&…

美客多自养号测评的常见问题与解决方案,从零开始的技术指南

美客多(MercadoLibre)主要专注于拉丁美洲市场。涵盖了多个国家&#xff0c;包括阿根廷、巴西、墨西哥、智利、哥伦比亚等&#xff0c;在这些国家占据了重要份额。对于卖家来说&#xff0c;要充分了解平台的特点和市场需求&#xff0c;制定合理的营销策略&#xff0c;不断提升自…

vue使用高德地图,点标记+轨迹

<template><!-- 轨迹--><divv-if"visible"ref"pageTotal"v-loading"loading"class"page-total"><div><divref"pageHead"class"page-head"><div class"head-title" /&…

《最高人民法院关于审理民间借贷案件适用法律若干问题的规定》(最新)民间借贷司法解释全文

原文地址 编辑于&#xff1a; 贵格律师事务所 2024年06月11日 16:04 上海 “ 为正确审理民间借贷纠纷案件&#xff0c;根据《中华人民共和国民法典》《中华人民共和国民事诉讼法》《中华人民共和国刑事诉讼法》等相关法律之规定&#xff0c;结合审判实践&#xff0c;制定本规定…

AI生成垃圾内容对互联网的冲击与应对:一场持续扩展的危机

引言 随着生成式AI技术的迅猛发展&#xff0c;人工智能在内容生产上的应用已深刻改变了互联网的内容生态。越来越多由AI生成的低质量内容正在淹没搜索引擎、内容社区、甚至学术领域。通过自动化工具&#xff0c;创建大规模虚假账号矩阵、批量生成无价值信息、甚至操纵搜索引擎…