安卓模仿微信选择昵称备注效果

最近对一个交互效果感兴趣,就是有人加你微信时输入了备注信息,你可以直接在备注信息中选择词语成为这个人的昵称备注。虽然微信给用户喂屎,但这个交互效果是值得肯定的。

首先,这是我们要实现的预期效果:

实现一个标签容器效果

我们把每个字视作一个标签,那么微信这个效果可以看作是多个标签逐行摆放,其中部分被选中。幸运的是MD中有这样的效果ChipGroup,这样我们可以节省不少工作。

源代码:ChipGroup.java

文章介绍:Android修行手册 - ChipGroup

然后我们用代码添加一些标签,并在点击时进行反选:

kotlin 复制代码
        binding.contentMain.chipGroup.apply {
            isSelectionRequired
            isSingleSelection = false
            words.forEach { word ->
                addView(Chip(context).also { chip ->
                    chip.text = word.trimIndent()
                    chip.setOnClickListener {
                        it.isSelected = !it.isSelected
                    }
                })
            }
        }

涂抹选择文本

显然要实现涂抹选中,我们得自定义View,拦截触摸事件定制逻辑。既然是"涂抹",那么要拦截的就是 ev?.action == MotionEvent.ACTION_MOVE

回顾Android触摸事件传递机制,复写onInterceptTouchEvent方法。

kotlin 复制代码
class SwipeSelectLayout @JvmOverloads constructor(context: Context, defStyleAttr: AttributeSet? = null, defStyleRes: Int = 0) : ChipGroup(context, defStyleAttr, defStyleRes) {
    private val childrenSelectionOnMoveStart = mutableMapOf<View, Boolean>()
    private var closestViewIndex = -1

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        if (ev.action == MotionEvent.ACTION_DOWN) {
            handleMotionEventDown(ev)
        }
        if (ev.action == MotionEvent.ACTION_MOVE) {
            return true
        }
        return super.onInterceptTouchEvent(ev)
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (!children.any()) {
            return super.onTouchEvent(event)
        }
        when(event.action) {
            MotionEvent.ACTION_DOWN -> {
                // 拖动起始点不在一个child上,也选择一个最近的View
                // handleMotionEventDown(event)
                // return true
            }
            MotionEvent.ACTION_MOVE -> {
                handleMotionEventMove(event)
                return true
            }
            MotionEvent.ACTION_UP -> {
                childrenSelectionOnMoveStart.clear()
                return true
            }
            MotionEvent.ACTION_CANCEL -> {
                childrenSelectionOnMoveStart.clear()
                return true
            }
        }
        return super.onTouchEvent(event)
    }

    ...
}

而"涂抹"的过程实际上是反选了起始点和终止点之间的所有View。因此我们需要

  1. 手指按下时记录所有View的选择状态,以及距离按下坐标最近的View
  2. 手指移动时找到当前点View,修改所有View的选中状态
    • 不在选中范围的View,重置为初始选中状态
    • 在选中范围的View,反选
  3. 手指抬起时清除记录

计算点到矩形的距离

首先思考点到线段的距离,

设A、B、C三点位置分别为 x A x_A xA、 x B x_B xB、 x C x_C xC,那么点A到线段BC的距离有
x = { x B − x A x A < x B 0 x B < x A < x C x A − x C x A > x C x=\begin{cases} x_B - x_A & x_A < x_B \\\\ 0 & xB < x_A < x_C \\\\ x_A - x_C & x_A > x_C \end{cases} x=⎩ ⎨ ⎧xB−xA0xA−xCxA<xBxB<xA<xCxA>xC

推广到点到矩形的距离,得到距离计算方式

kotlin 复制代码
    private fun getDistanceBetweenRectAndPoint(rect: Rect, x: Int, y: Int): Int {
        val xDistance = if (x in rect.left..rect.right) {
            0
        } else (
            min(abs(x - rect.left), abs(x - rect.right))
        )
        val yDistance = if (y in rect.top..rect.bottom) {
            0
        } else {
            min(abs(y - rect.top), abs(y - rect.bottom))
        }
        return sqrt((xDistance * xDistance + yDistance * yDistance).toFloat()).roundToInt()
    }

这样,在手指按下时记录选中状态和最近的View

kotlin 复制代码
    private fun handleMotionEventDown(event: MotionEvent) {
        children.forEachIndexed { index, view ->
            // 记录所有Chip的选中状态
            childrenSelectionOnMoveStart[view] = view.isSelected
            // 查找距离起始点最近的View
            closestViewIndex = findClosestViewIndex(event.x.roundToInt(), event.y.roundToInt())
        }
    }

手指移动过程中反选最近View与起始点之间的View。

kotlin 复制代码
    private fun handleMotionEventMove(event: MotionEvent) {
        val startViewIndex = closestViewIndex
        val endViewIndex = findClosestViewIndex(event.x.roundToInt(), event.y.roundToInt())

        val range = min(startViewIndex, endViewIndex)..max(startViewIndex, endViewIndex)
        for (i in 0 until childCount) {
            val view = getChildAt(i)
            val originSelection = childrenSelectionOnMoveStart[view] == true
            view.isSelected = if (i in range) {
                // 反选拖动区域所有View的选中状态
                !originSelection
            } else {
                // 不在拖动区域,还原到初始选择状态
                originSelection
            }
        }
    }

大功告成。我们最终实现的效果:

代码:Gist

相关推荐
mmsx6 分钟前
osmdroid 自定义地图视图:继承 MapView 的正确姿势与生命周期管理
android·osmdroid
ii_best1 小时前
移动开发工具按键精灵安卓版 OcrEx 识别总是漏字错字?改这一个参数,精度直接翻倍
android·ai编程·按键精灵
chjif2 小时前
Android 设备管控开发实战:自定义 Launcher 如何只显示指定 App?
android·kotlin
Dovis(誓平步青云)2 小时前
模拟器横评:电脑上看小说、追短剧用什么模拟器?MuMu、雷电、腾讯手游助手实测
android·java·服务器·前端·javascript·电脑
非凡ghost3 小时前
用 Kotlin 和 Jetpack Compose 重写的安卓视频播放器,原生体验有多流畅?
android·java·kotlin·音视频·软件需求
峥嵘life3 小时前
Android16 系统 APEX 模块调试总结
android·大数据·开发语言
亘元有量-流量变现3 小时前
2026安卓ASO增量攻略:摆脱iOS思维依赖,吃透多商店自然免费流量
android·aso优化·亘元有量·方糖试玩
一航jason4 小时前
安卓车机端 AIOS 技术生态全景
android·人工智能·ai·ai编程·ai-native
深念Y4 小时前
ZTE B860AV1.1-T2 机顶盒改造记录:从 Android 到 Linux 服务器
android·linux·运维·服务器·boot·cma·机顶盒