当前位置: 首页 > article >正文

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

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

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.kler.cn/news/331948.html

相关文章:

  • VUE 开发——Node.js学习(一)
  • Chapter02
  • 检查jar冲突,查找存在相同class的jar
  • Xcode16适配
  • 【MySQL】SQL介绍+基础+DDL+数据备份+还原
  • Linux增加一个回收站功能(实用功能)
  • 腾讯云技术深度解析:从微服务到AI编程助手的应用实践
  • vector 的使用(上)
  • SAP_FI_科目组,科目表,科目之间的关系
  • 算法日记-链表翻转
  • 【优选算法】(第十九篇)
  • 算法训练营打卡Day19
  • 在Robot Framework中Run Keyword If的用法
  • stm32f103调试,程序与定时器同步设置
  • 基于Spring Boot+Unipp的中考体测训练小程序(协同过滤算法、图形化分析)【原创】
  • 前缀和算法详解
  • Acwing 背包问题
  • UE5.4.3 录屏回放系统ReplaySystem蓝图版
  • 无水印短视频素材下载网站有哪些?十个高清无水印视频素材网站分享
  • Angular ng-state script 元素的生成机制介绍