android EditText设置后缀

news2024/9/28 5:26:22

有两种实现方案。
方案一:是自己写一个TextWatcher。
方案二:是重写TextView的getOffsetForPosition方法,返回一个计算好的offset。

我在工作时,使用的是方案一。在离职之后,我还是对这个问题耿耿于怀,所以才去看源码,最后想到了方案二。

但实际测试时,发现方案二存在一些问题,而且看了找了好久的源码,都不知道要重写哪个方法来解决,所以只能提供另一个半成品的方案二出来。

先看看两种方案的gif吧。
方案一
在这里插入图片描述
方案二
在这里插入图片描述
从方案一的gif图片可以看到,在输入文本之后,会在文本后面追加suffix text。如果在suffix text的范围内输入会删除,则会将这些操作传递到已输入的文本上。如果已输入的最后一个文本被删除,则会删除掉所有文本。

方案二则相对简单,输入后依然有suffix text,但suffix 的范围是不可以点击的。咋一看,这个方案好像很好,但我在实际测试时发现了一些问题,而且还不知道要怎么解决。

  • 长按EditText会出现全选的dialog,并且点击全选将选择suffix text。这个操作我认为是有问题的。既然suffix text没办法被selection,那全选就不应该将它包含进来
  • 双击suffix text,也会选择suffix text

我找了很久很久的源码,不断的debug,尝试定位到第一个问题和第二个问题调用的源码,并看看能否重写某些方法来改变其逻辑,但最终都无功而返。
在上面我也提到了,我是重写getOffsetForPosition方法实现了这个功能,所以我也尝试在这个方法上动手脚,但最终的效果不是很好。在模拟器上,如果是用了全选,会出现,先全选,再选回suffix text前面的文本。对于用户来说,两个动画同时出现,未免也太滑稽了吧。所以我最终没有采用这种方案。

无论使用哪种方案,在复制时都会将suffix text复制进来,想要解决这个问题也很简单,可以EditText并重写EditText的onTextContextMenuItem方法,并判断id是否为android.R.id.copy。如果是,就重写一下copy的逻辑。至于为什么我知道可以这样做,可以看一下这篇博客。

图也给了,也解释了图片里面发生了什么,下面贴一下代码,注释已经补在代码里面。
方案一

open class SuffixTextWatcher(private val editText: EditText, private val suffixText: String) : TextWatcher {
    private var lastText = ""

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
    }

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        if (suffixText.isEmpty()) {
            return
        }
        editText.removeTextChangedListener(this)

        val preText = s.toString()
        val suffixText = suffixText
        // 如果输入的文本等于suffixText,就说明已经清空了输入的文本,那就直接设置为空字符串
        if (preText == suffixText) {
            lastText = ""
            editText.setText(lastText)
        } else {
            // 如果不是以suffixText结尾,则说明suffix已经遭到破坏,或者是还没有suffixText
            if (preText.isNotEmpty() && preText.endsWith(suffixText).not()) {
                // 如果lastText等于空,就说明还没有suffixText
                if (lastText.isEmpty()) {
                    lastText = preText.plus(suffixText)
                    editText.setText(lastText)
                    editText.setSelection(preText.length)
                } else {
                    // 如果执行到该else,就说明suffixText已经被破坏了
                    val suffixTextStartIndex = lastText.length - suffixText.length
                    // 如果两个文本的长度不相等
                    if (lastText.length != preText.length) {
                         // 获取上次已输入的字符串
                        val lastInputText = lastText.substring(0, suffixTextStartIndex)
                        // 如果lastText的长度小于preText的长度,就说明已经suffixText里面输入了字符
                        // 这个if-else就是用于获取实际输入的字符
                        val latestInputText = if (lastText.length < preText.length) {
                            // 获取输入的字符,并拼接到上次已输入的字符串末尾
                            val inputChar = preText.substring(start, start + 1)
                            lastInputText.plus(inputChar)
                        } else {
                            // 如果不大于,那就是小于,因为已经在上面判断了两个不相等
                            // 如果小于,就是将suffixText中的某个字符删除掉
                            
                            // 上次如果只输入了一个字符,本次就将输入的字符设置为空字符串
                            if (lastInputText.length == 1) {
                                ""
                            } else {
                                // 否则就删掉最后一个字符
                                lastInputText.substring(0, lastInputText.length - 1)
                            }
                        }
                        // 如果上面获取到的最新输入文本不为空,就在后面追加suffixText
                        // 否则就将lastText设置为空字符串
                        lastText = if (latestInputText.isNotEmpty()) {
                            latestInputText.plus(suffixText)
                        } else ""
                        editText.setText(lastText)
                        if (lastText.isNotEmpty()) {
                            editText.setSelection(latestInputText.length)
                        }
                    } else {
                        // 这个else一般执行不到,但为了防止出现考虑不到的问题,还是简单处理一下
                        if (start in suffixTextStartIndex until lastText.length) {
                            editText.setText(lastText)
                            editText.setSelection(suffixTextStartIndex)
                        }
                    }
                }
            } else {
                // 执行到该else,就说明suffixText没有遭到破坏
                // 如果用户没有乱来,一般就是执行到这里,但也有例外
                // 从gif图片可以看到,如果在su中间输入s,也是没有问题的,因为此时endWith为true,但这种情况下,selectionIndex是不正确的
                // 此时selectionIndex是在su中间,而不是在ss中间,所以下面的的代码就是处理这个问题
                // 处理的方式也很简单,获取suffixText的长度,并判断start是否在suffixText的范围内
                // 如果是,就将index设置到suffixText前面
                if(preText.isNotEmpty()) {
                    val suffixTextStartIndex = preText.length - suffixText.length
                    if (start >= suffixTextStartIndex) {
                        editText.setSelection(suffixTextStartIndex)
                    }
                }
                lastText = preText
            }
        }
        editText.addTextChangedListener(this)
    }

    override fun afterTextChanged(s: Editable?) {
    }

}

方案二

class SuffixEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
    AppCompatEditText(context, attrs) {

    // textWatcher用来补齐后缀
    private val textWatcher = SuffixTextWatcher(this)

    var suffixText = ""
        set(value) {
            val oldSuffix = field
            field = value
            textWatcher.suffixText = value
            updateSuffixText(oldSuffix, value)
        }

    init {
        addTextChangedListener(textWatcher)
        if (suffixText.isNotEmpty()){
            textWatcher.suffixText = suffixText
        }
    }

    private fun updateSuffixText(oldSuffix: String, newSuffix: String) {
        val text = text ?: ""
        if (oldSuffix.isEmpty() || text.isEmpty()) {
            return
        }
        val oldSuffixIndex = text.lastIndexOf(oldSuffix)
        if (oldSuffixIndex != -1) {
            setText(text.substring(0, oldSuffixIndex))
        }
    }

    // 这里就是修改selectionIndext的代码,如果用户的touch行为导致selectionIndex发生变化
    // EditText就会调用这里获取index,所以只需重写该方法即可
    override fun getOffsetForPosition(x: Float, y: Float): Int {
        var superResult = super.getOffsetForPosition(x, y)
        val text = text ?: ""
        val suffixText = suffixText
        if (text.isEmpty() || suffixText.isEmpty()) {
            return superResult
        }
        val textLength = text.length - suffixText.length
        // 如果index在suffixText的范围内,就设置为inputText的最大index
        if (superResult >= textLength) {
            superResult = textLength
        }
        return superResult
    }

    override fun onTextContextMenuItem(id: Int): Boolean {
        // 如果不希望用户复制的内容包含suffix text,就可以重写该方法,并按照我上面提到的代码去做
        return super.onTextContextMenuItem(id)
    }

    private class SuffixTextWatcher(val editText: EditText) : TextWatcher {
        var suffixText: String = ""

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        }

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            val text = s?.toString() ?: ""
            if (text.isEmpty()) {
                return
            }
            editText.removeTextChangedListener(this)
            // 如果没有suffix text,就在最后追加suffix text
            if (text.endsWith(suffixText).not()) {
                if (text.isNotEmpty()) {
                    editText.setText("$text$suffixText")
                    editText.setSelection(text.length)
                }
            } else {
                if (text == suffixText) {
                    editText.setText("")
                }
            }
            editText.addTextChangedListener(this)
        }

        override fun afterTextChanged(s: Editable?) {
        }
    }
}

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

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

相关文章

git在工作中的正常使用

开发A和B功能后进行发版。。 一、拉取代码 git clone http://ntc.ntsvars.com:8090/lvweijie/test.git二、开发功能A任务 创建A任务本地分支 #创建A分支&#xff0c;并切换A分支 git checkout -b A三、开发A任务 四、提交A功能文件到本地分支 git add .五、添加提交A功能备…

Tina_Linux打包流程说明指南_new

OpenRemoved_Tina_Linux_打包流程_说明指南_new 1 概述 1.1 编写目的 介绍Allwinner 平台上打包流程。 1.2 适用范围 Allwinner 软件平台Tina v3.0 版本以上。 1.3 相关人员 适用Tina 平台的广大客户&#xff0c;想了解Tina 打包流程的开发人员。 2 固件打包简介 固件…

Jenkins+Gitlab实现代码自动构建部署

一、环境准备 主机名ip安装软件jenkins192.168.75.149jenkinsgitlab192.168.75.147gitlabweb192.168.75.155部署应用 二、jenkins服务器配置 1、生产公钥 [rootjenkins ~]# ssh-keygen &#xff08;2&#xff09;获取公钥信息 公钥信息在配置 Gitlab SSH Keys 时用到。 &am…

Tcpdump抓包验证zookeeper的心跳机制

一、背景 在分布式系统中&#xff0c;zookeeper可以作为服务注册中心&#xff0c;所有提供服务的节点都可以在zookeeper上面注册&#xff0c;并作为一个node被组织起来&#xff0c;如下图&#xff1a; 在RPC框架中&#xff0c;这些服务提供者就是RPC服务的提供者。zookeeper注…

【测试】Python手机自动化测试库uiautomator2和weditor的详细使用

1.说明 我们之前在电脑操作手机进行自动化测试&#xff0c;基本上都是通过Appium的&#xff0c;这个工具确实强大&#xff0c;搭配谷歌官方的UiAutomator基本上可以完成各种测试&#xff0c;但缺点也很明显&#xff0c;配置环境太麻烦了&#xff0c;需要jdk、sdk等&#xff0c…

利用较新版本的IDEA 2022.3.2 创建Java Web的maven项目

1.创建项目 正常三步走&#xff0c;没什么可说的 2.用模板创建项目&#xff08;重要&#xff09; 第一步&#xff0c;一定要选Jakarta EE。这个模板是基于JavaWeb的一个标准模板&#xff0c;如果选了maven中的JavaWeb模板&#xff0c;那就变成了web目录在根目录下&#xff0c;…

测试跟踪模块UX交互升级,多个X-Pack功能开放至开源版,MeterSphere开源持续测试平台v2.7.0发布

2023年2月24日&#xff0c;MeterSphere一站式开源持续测试平台正式发布v2.7.0版本。 在这一版本中&#xff0c;MeterSphere在测试跟踪模块进行了UX交互升级&#xff0c;整个页面采用轻量化设计进行整体降噪&#xff0c;页面信息更加清晰易懂&#xff0c;操作流程更顺畅&#x…

【学习笔记】深入理解JVM之类加载机制

【学习笔记】深入理解JVM之类加载机制 以后基本上都在语雀上面更新&#xff0c;大家有兴趣可以看看嗷&#xff01; 首发地址&#xff1a; 知识库 文章流程图&#xff1a; 1、概述 首先我们先来看看一个 Class 文件所需要经过的一个流程图&#xff1a; 而我们今天要重点需讲的…

如何保护阿里云、政采云等云市场三方账号安全?

什么是云市场&#xff1f;根据百度百科释义&#xff0c;云市场是指物联网中分布在不同地点的海量的商品生产者和消费者之间各种经济关系的集合体&#xff0c;是通过相对集中的云平台资源联合物联网各个感知节点信息资源的方式&#xff0c;以运行分布在不同地点的海量的经济交换…

Java——数组

目录 前言 一、数组的定义 二、数组声明和创建 三、三种初始化及内存分析 Java内存分析 三种初始化 静态初始化 动态初始化 数组的默认初始化 数组的四个基本特点 四、下标越界及小结 五、数组的使用 For-Each循环 数组作方法入参 数组作返回值 六、二维数组 七…

项目管理工具dhtmlxGantt甘特图入门教程(十四):导出/导入 Excel到 iCal

这篇文章给大家讲解利用dhtmlxgantt导入/导出Excel到iCal的操作方法。 dhtmlxGantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表&#xff0c;可满足应用程序的所有需求&#xff0c;是完善的甘特图图表库 DhtmlxGantt正版试用下载&#xff08;qun&#xff1b;765665…

中国跨境平台出海,产业带依然是最大优势

对外贸工厂来说&#xff0c;借助跨境电商服务平台开拓海外市场可行吗&#xff1f;2023年2月11日&#xff0c;在郑州荥阳举办的Starday线下招商会联合线上直播荥阳站上&#xff0c;这是很多现场参会的企业负责人面对大屏上的招商介绍宣传时&#xff0c;大脑飞速思考的问题。2023…

android kotlin 协程(六) 源码浅析

android kotlin 协程(六) 源码浅析 前言: kotlin协程源码十分庞大, 本篇只能吧我理解的源码聊一聊,不会特别深入研究,只会浅浅的看看表层. 本来计划协程系列是10篇左右,后续是flow热流冷流之类的, 冷流操作符之类的应该不会在写了, flow当作Rxjava来用就可以,后续可能还会写一…

Bitlocker加密,与解除加密

引文&#xff1a;应为C盘空间不够用了&#xff0c;想着用U盘从新给C盘分下区。操作时才发现我系统里的磁盘都是Bitlocker加密的&#xff0c;分区工具操作不了磁盘&#xff0c;所以就找到一下方法来解决。1&#xff0c;先讲一下解除加密&#xff1a;直接点击 &#xff1a;设置-&…

python pandas 常用方法汇总

前言 一、pandas是什么&#xff1f; 二、使用步骤 1.引入库 2.处理时间序列数据 3.分组聚合&#xff08;groupby&#xff09; 3.1基本方法 3.2具体使用&#xff1a;如图包含三个字段&#xff0c;company、salary、age 总结 Pandas 最最常用函数罗列 Pandas 函数用法示…

软件测试2年半的我,谈谈自己的理解...

软件测试两年半的我&#xff0c;谈谈自己的理解从2020年7月毕业&#xff0c;就成为一名测试仔。日子混了一鲲年&#xff0c;感觉需要好好梳理一下自己的职业道路了&#xff0c;回顾与总结下吧。一、测试的定位做事嘛&#xff0c;搞清楚自己的定位很重要。要搞清楚自己的定位&am…

新手小白根据Forexclub6点建议就能选择到最佳外汇经纪商

选择外汇经纪商很重要&#xff0c;尤其是对于外汇交易者新手而言。 在确定您计划使用的外汇交易员之前&#xff0c;Forexclub建议考虑以下6个因素产品丰富即使在这个阶段&#xff0c;您只对外汇交易感兴趣&#xff0c;拥有期权也是件好事。 大多数外汇经纪商提供对其他金融资产…

Excel工作表不能移动或复制?看看是不是这两个原因

Excel工作表不能移动或复制&#xff1f;今天来看看如何解决。 大家都知道&#xff0c;Excel表格分为工作簿和工作表&#xff0c;工作簿就是整个Excel文件&#xff1b;工作簿里面&#xff0c;也就是Excel表可以有多个工作表。 而各个工作表之间是可以相互移动或复制的&#xf…

C++赋值运算符重载

赋值运算符重载 目录赋值运算符重载示例1&#xff1a;示例2&#xff1a;示例3&#xff1a;示例4&#xff1a;很巧妙的是&#xff0c;在编写这篇文章时&#xff08;2023年2月27日&#xff09;&#xff0c;再加100天就是6月7日&#xff0c;恰好是今年高考的百日誓师&#xff01; …

蓝库云|什么是供应链管理?SCM对制造业的重要性

企业在产品的销售经营上&#xff0c;往往不会考量到供应链管理(SCM)的流程规划&#xff0c;但现今的商业环境与以往不同&#xff0c;高度竞争与客户不断提升的期望&#xff0c;藉由做好供应链管理(SCM)&#xff0c;才能更准时的提供优质产品与优良服务&#xff0c;增强企业竞争…