Android简单支持项目符号的EditText
一、背景及样式效果
因项目需要,需要文本编辑时,支持项目符号(无序列表)尝试了BulletSpan,但不是很理想,并且考虑到影响老版本回显等因素,最终决定自定义一个BulletEditText。
先看效果:

视频效果
二、自定义View BulletEditText
自定义控件BulletEditText源码:
package com.ml512.widgetimport android.content.Context
import android.util.AttributeSet
import androidx.core.widget.doOnTextChanged/*** @Description: 简单支持项目号的文本编辑器* @Author: Marlon* @CreateDate: 2024/2/1 17:44* @UpdateRemark: 更新说明:* @Version: 1.0*/
class BulletEditText : androidx.appcompat.widget.AppCompatEditText {/*** 是否开启项目符号*/private var isNeedBullet: Boolean = false/*** 项目符号*/private var bulletPoint: String = "• "/*** 项目符号占用字符数,方便设置光标位置*/private var bulletOffsetIndex = bulletPoint.length/*** 相关监听回调*/private var editListener: EditListener? = nullconstructor(context: Context) : super(context)constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context,attrs,defStyleAttr)init {this.doOnTextChanged { text, start, before, count ->//如果是关闭状态不做格式处理if (!isNeedBullet) {return@doOnTextChanged}if (count > before) {//处理项目号逻辑var offset = 0var tmp = text.toString()//连续回车去掉项目符号if (start >= bulletOffsetIndex && tmp.substring(start, start + count) == "\n") {val preSub = tmp.substring(start - bulletOffsetIndex, start)if (preSub == bulletPoint) {changeBulletState(false)tmp = tmp.replaceRange(start-bulletOffsetIndex, start + count, "")offset -= bulletOffsetIndex + 1setTextAndSelection(tmp, start + count + offset)return@doOnTextChanged}}//加入项目符号if (tmp.substring(start, start + count) == "\n") {changeBulletState(true)tmp = tmp.replaceRange(start, start + count, "\n$bulletPoint")offset += bulletOffsetIndexsetTextAndSelection(tmp, start + count + offset)}}}}override fun onSelectionChanged(selStart: Int, selEnd: Int) {super.onSelectionChanged(selStart, selEnd)//复制选择时直接返回,关闭项目符号if (selStart != selEnd) {changeBulletState(false)return}//判断当前段落是否有项目号,有开启,没有关闭val tmp = text.toString()val prefix = tmp.substring(0, selectionStart)if (prefix.isEmpty()) {changeBulletState(false)return}if (prefix.startsWith(bulletPoint) && !prefix.contains("\n")) {changeBulletState(true)return}val lastEnterIndex = prefix.lastIndexOf("\n")if (lastEnterIndex != -1 && lastEnterIndex + bulletOffsetIndex + 1 <= prefix.length) {val mathStr = prefix.substring(lastEnterIndex, lastEnterIndex + bulletOffsetIndex + 1)if (mathStr == "\n$bulletPoint") {changeBulletState(true)return}}changeBulletState(false)}/*** 更新bullet状态*/private fun changeBulletState(isOpen: Boolean) {isNeedBullet = isOpeneditListener?.onBulletStateChange(isOpen)}/*** 设置是否开启项目号*/fun setBullet(isOpen: Boolean) {isNeedBullet = isOpenval tmp = text.toString()var index = selectionStartvar prefix = tmp.substring(0, index)val suffix = tmp.substring(index)//加项目号if (isOpen) {//首个段落if (!prefix.contains("\n") && prefix.startsWith(bulletPoint)) {return}index += bulletOffsetIndexif (prefix.isEmpty() || (!prefix.contains("\n") && !prefix.startsWith(bulletPoint))) {setTextAndSelection("$bulletPoint$prefix$suffix", index)return}prefix = prefix.replaceLast("\n", "\n$bulletPoint")setTextAndSelection("$prefix$suffix", index)return}//去掉项目号if (prefix.startsWith(bulletPoint) && !prefix.contains("\n$bulletPoint")) {//首行逻辑index -= bulletOffsetIndexprefix = prefix.replaceLast(bulletPoint, "")setTextAndSelection("$prefix$suffix", index)return}if (prefix.contains("\n$bulletPoint")) {index -= bulletOffsetIndexprefix = prefix.replaceLast("\n$bulletPoint", "\n")setTextAndSelection("$prefix$suffix", index)}}/*** 设置文本及光标位置*/private fun setTextAndSelection(text: String, index: Int) {setText(text)setSelection(index)}/*** 替换最后一个字符*/private fun String.replaceLast(oldValue: String, newValue: String): String {val lastIndex = lastIndexOf(oldValue)if (lastIndex == -1) {return this}val prefix = substring(0, lastIndex)val suffix = substring(lastIndex + oldValue.length)return "$prefix$newValue$suffix"}/*** 设置监听*/fun setEditListener(listener: EditListener) {editListener = listener}/*** 监听回调*/interface EditListener {/*** 项目符号开关状态变化*/fun onBulletStateChange(isOpen: Boolean)}
}
三、调用
使用时一个项目符号的按钮开关设置调用setBullet(isOpen: Boolean) 设置是否开启项目符号,同时实现一个setEditListener(listener: EditListener)根据光标位置判断当前段落是否含有项目符号,并回显按钮状态。
<com.ml512.widget.BulletEditTextandroid:id="@+id/etInput"android:layout_width="match_parent"android:layout_height="200dp"android:layout_below="@+id/tvTitle"android:layout_marginStart="15dp"android:layout_marginTop="15dp"android:layout_marginEnd="15dp"android:layout_marginBottom="15dp"android:autofillHints="no"android:background="@drawable/shape_edit_bg"android:gravity="top"android:hint="@string/text_please_input_some_worlds"android:inputType="textMultiLine"android:padding="15dp"android:textColor="@color/black"android:textColorHint="@color/color_FF_999999"android:textSize="16sp" />
//点击按钮设置添加/取消项目符号tvBullet.setOnClickListener {tvBullet.isSelected = !tvBullet.isSelectedetInput.setBullet(tvBullet.isSelected)}//项目符号状态监听,回显到按钮etInput.setEditListener(object :BulletEditText.EditListener{override fun onBulletStateChange(isOpen: Boolean) {tvBullet.isSelected = isOpen}})
大功告成!
相关文章:
Android简单支持项目符号的EditText
一、背景及样式效果 因项目需要,需要文本编辑时,支持项目符号(无序列表)尝试了BulletSpan,但不是很理想,并且考虑到影响老版本回显等因素,最终决定自定义一个BulletEditText。 先看效果&…...
【axios报错异常】: Uncaught ReferenceError: axios is not defined
问题描述: 当前代码在vivo手机和小米手机运行是正常的,点击分享按钮调出相关弹框,发送接口进行分享,但是现在oppo手机出现了问题: 点击分享按钮没有反应. 问题解析: 安卓同事经过查询后,发现打印了错误: 但是不清楚这个问题是安卓端造成的还是前端造成的,大家都不清楚. 问题…...
Docker基础与持续集成
docker 基础知识: docker与虚拟机 !左边为虚拟机,右边为docker环境 – Server :物理机服务器Host OS :构建的操作系统Hypervisor :一种虚拟机软件,装了之后才能虚拟化操作系统Guest OS :虚拟化的操作系统…...
flutter开发实战-ijkplayer视频播放器功能
flutter开发实战-ijkplayer视频播放器功能 使用better_player播放器进行播放视频时候,在Android上会出现解码失败的问题,better_player使用的是video_player,video_player很多视频无法解码。最终采用ijkplayer播放器插件,在flutt…...
SpringFramework实战指南(五)
SpringFramework实战指南(五) 4.3 基于 注解 方式管理 Bean4.3.1 实验一: Bean注解标记和扫描 (IoC)4.3.2 实验二: 组件(Bean)作用域和周期方法注解4.3.3 实验三: Bean属性赋值:引用类型自动装配 (DI)4.3.4 实验四: Bean属性赋值:基本类型属性赋值 (DI)4.3.5 实验五:…...
力扣 121. 买卖股票的最佳时机
题目来源:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/ 好久没写代码了,啥啥都忘了 C题解1:贪心算法。(来源代码随想录) 因为股票就买卖一次,那么贪心的想法很自然就是取…...
【STM32+HAL库+CubeMX】UART轮询收发、中断收发、DMA收发方法及空闲中断详解
(转载)原文链接:https://blog.csdn.net/qq_39344192/article/details/131470735 1. 什么是UART? UART是一种异步串行通信接口,常用于通过串口与外部设备进行通信。它通过发送和接收数据帧来实现数据传输,使…...
基于Java医院管理系统设计与实现(源码+部署文档)
博主介绍: ✌至今服务客户已经1000、专注于Java技术领域、项目定制、技术答疑、开发工具、毕业项目实战 ✌ 🍅 文末获取源码联系 🍅 👇🏻 精彩专栏 推荐订阅 👇🏻 不然下次找不到 Java项目精品实…...
PHP://filter过滤器
今天刷题遇到了php://filter过滤器的知识点考察;不会,看了几篇写的不错的文章,本来想转载的,但是代码复制过来后发现格式很乱,和原文格式差太多了;算了,直接把文章连接拿过来吧,在这…...
蓝桥杯刷题day05——2023
1、题目描述 请求出在12345678 (含) 至 98765432 (含) 中 ,有多少个数中完全不包含 2023。 完全不包含 2023是指 无论将这个数的哪些数位移除都不能得到2023。 例如 20322175,33220022 都完全不包含 2023, 而20230415,20193213 …...
【51单片机】开发板和单片机的介绍(2)
前言 大家好吖,欢迎来到 YY 滴单片机系列 ,热烈欢迎! 本章主要内容面向接触过单片机的老铁 主要内容含: 欢迎订阅 YY滴C专栏!更多干货持续更新!以下是传送门! YY的《C》专栏YY的《C11》专栏YY的…...
《剑指 Offer》专项突破版 - 面试题 30 和 31:详解如何设计哈希表以及利用哈希表设计更加高级、复杂的数据结构
目录 一、哈希表的基础知识 二、哈希表的设计 2.1 - 插入、删除和随机访问都是 O(1) 的容器 2.2 - 最近最少使用缓存 一、哈希表的基础知识 哈希表是一种常见的数据结构,在解决算法面试题的时候经常需要用到哈希表。哈希表最大的优点是高效,在哈希表…...
回顾2023年及过去五年的成长经历
现在是2024年2月4日,我想回顾下过去两年的经历和感悟。总结下过去五年的成长经历。 最大的感悟就两点。第一,我相比于两年前成长了很多、也成熟了很多,不管是心智上还是心态上。而这些成长来自于读书、思考和结合实践的反思。第二࿰…...
99例电气实物接线及52个自动化机械手动图
给大家分享一些流水线设计中常见的一些结构,这些动态图很直观,有助于大家了解其原理,非常好懂。 1.家庭总电箱接线图 2.经典双控灯接线 3.五孔一开接线 4.电动机点动控制接线(不安全) 5.电动机自锁接线图(…...
SQL中聚合函数
SQL中的聚合函数是用于对一组值执行计算,并返回单个值的函数。它们通常在SELECT语句的SELECT列表中使用,并与GROUP BY子句结合使用来汇总数据。聚合函数忽略NULL值,只对非NULL值进行计算。以下是一些最常用的SQL聚合函数: 1. COU…...
深度学习预备知识1——数据操作
所有机器学习方法都涉及从数据中提取信息,因此需要一些关于数据的实用技能,包括存储、操作和预处理数据。 机器学习通常需要处理大型数据集。线性代数和矩阵是计算大量数据的有力工具,需要一些矩阵运算相关的线性代数知识。 深度学习是关于…...
【云原生运维问题记录】kubesphere登录不跳转问题
文章目录 现象问题排查 结论先行:kubesphere-system名称空间下reids宕机重启,会判断是否通过registry-proxy重新拉取镜像,该镜像原本是通过阿里云上拉取,代理上没有出现超时情况,导致失败。解决方案:删除re…...
深入学习Prometheus! 一款开源的监控和警报工具!
深入学习Prometheus! 一款开源的监控和警报工具! Prometheus是一个开源的监控和警报工具,它广泛用于记录和收集各种指标(如硬件资源使用情况、应用性能等),并提供强大的查询语言以帮助用户分析和查看这些数据。本文将…...
【webrtc】跟webrtc学list遍历
m98 代码:RTT G:\CDN\rtcCli\m98\src\video\call_stats.cc遍历list 进行删除 :remove_if void RemoveOldReports(int64_t now, std::list<CallStats::RttTime>* reports) {static constexpr const <...
网络安全产品之准入控制系统
文章目录 一、什么是准入控制系统二、准入控制系统的主要功能1. 接入设备的身份认证2. 接入设备的安全性检查 三、准入控制系统的工作原理四、准入控制系统的特点五、准入控制系统的部署方式1. 网关模式2. 控制旁路模式 六、准入控制系统的应用场景七、企业如何利用准入控制系统…...
超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地
借阿里云中企出海大会的东风,以**「云启出海,智联未来|打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办,现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...
动态 Web 开发技术入门篇
一、HTTP 协议核心 1.1 HTTP 基础 协议全称 :HyperText Transfer Protocol(超文本传输协议) 默认端口 :HTTP 使用 80 端口,HTTPS 使用 443 端口。 请求方法 : GET :用于获取资源,…...
RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill
视觉语言模型(Vision-Language Models, VLMs),为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展,机器人仍难以胜任复杂的长时程任务(如家具装配),主要受限于人…...
通过MicroSip配置自己的freeswitch服务器进行调试记录
之前用docker安装的freeswitch的,启动是正常的, 但用下面的Microsip连接不上 主要原因有可能一下几个 1、通过下面命令可以看 [rootlocalhost default]# docker exec -it freeswitch fs_cli -x "sofia status profile internal"Name …...
pycharm 设置环境出错
pycharm 设置环境出错 pycharm 新建项目,设置虚拟环境,出错 pycharm 出错 Cannot open Local Failed to start [powershell.exe, -NoExit, -ExecutionPolicy, Bypass, -File, C:\Program Files\JetBrains\PyCharm 2024.1.3\plugins\terminal\shell-int…...
es6+和css3新增的特性有哪些
一:ECMAScript 新特性(ES6) ES6 (2015) - 革命性更新 1,记住的方法,从一个方法里面用到了哪些技术 1,let /const块级作用域声明2,**默认参数**:函数参数可以设置默认值。3&#x…...
