Android简单支持项目符号的EditText

news2024/12/27 0:18:10

一、背景及样式效果      

因项目需要,需要文本编辑时,支持项目符号(无序列表)尝试了BulletSpan,但不是很理想,并且考虑到影响老版本回显等因素,最终决定自定义一个BulletEditText。

        先看效果:

        

视频效果

二、自定义View BulletEditText        

        自定义控件BulletEditText源码:

package com.ml512.widget

import 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? = null

    constructor(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 = 0
                var 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 + 1
                        setTextAndSelection(tmp, start + count + offset)
                        return@doOnTextChanged
                    }
                }

                //加入项目符号
                if (tmp.substring(start, start + count) == "\n") {
                    changeBulletState(true)
                    tmp = tmp.replaceRange(start, start + count, "\n$bulletPoint")
                    offset += bulletOffsetIndex
                    setTextAndSelection(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 = isOpen
        editListener?.onBulletStateChange(isOpen)
    }

    /**
     * 设置是否开启项目号
     */
    fun setBullet(isOpen: Boolean) {
        isNeedBullet = isOpen
        val tmp = text.toString()
        var index = selectionStart
        var prefix = tmp.substring(0, index)
        val suffix = tmp.substring(index)

        //加项目号
        if (isOpen) {

            //首个段落
            if (!prefix.contains("\n") && prefix.startsWith(bulletPoint)) {
                return
            }
            index += bulletOffsetIndex
            if (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 -= bulletOffsetIndex
            prefix = prefix.replaceLast(bulletPoint, "")
            setTextAndSelection("$prefix$suffix", index)
            return
        }
        if (prefix.contains("\n$bulletPoint")) {
            index -= bulletOffsetIndex
            prefix = 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.BulletEditText
        android: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.isSelected
            etInput.setBullet(tvBullet.isSelected)
        }
        //项目符号状态监听,回显到按钮
        etInput.setEditListener(object :BulletEditText.EditListener{
            override fun onBulletStateChange(isOpen: Boolean) {
                tvBullet.isSelected = isOpen
            }
        })

大功告成!

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

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

相关文章

【深度学习】讲透深度学习第3篇:TensorFlow张量操作(代码文档已分享)

本系列文章md笔记&#xff08;已分享&#xff09;主要讨论深度学习相关知识。可以让大家熟练掌握机器学习基础,如分类、回归&#xff08;含代码&#xff09;&#xff0c;熟练掌握numpy,pandas,sklearn等框架使用。在算法上&#xff0c;掌握神经网络的数学原理&#xff0c;手动实…

vue + 动态加载图片

首先尝试我们经常用的require动态引入&#xff0c; 发现报错&#xff1a;require is not defind&#xff0c;这是因为 require 属于 Webpack 的方法&#xff0c;我现在的环境是 vue3.0 vite 1、 适用于处理少量链接的资源文件 import img from ./img.png; <img :src"…

Linux挂载本地ISO镜像源

1 创建挂载镜像的目录 mkdir /opt/rpm2 上传iso镜像到root目录 3 挂载镜像 mount -t iso9660 /root/CentOS-7-x86_64-DVD-2207-02.iso /opt/rpm/ 4 若是ftp文件夹挂载本地 mkdir /opt/iso 将ftp上software/caozuoxitong目录挂载到本地/opt/iso/ 目录 mount -t cifs //172.…

字符串左旋

题目&#xff1a;字符串左旋 内容&#xff1a;实现一个函数&#xff0c;可以左旋字符串中的K个字符。 例如&#xff1a; ABCDEF左旋一个字符可以得到BCDEFA ABCDEF左旋两个字符可以得到CDEFAB 方法一&#xff1a;移动字符 #include <stdio.h> #include <string.h>c…

深入分析AOP+自定义注解+RBAC实现操作权限管理设计思想

深入分析AOP自定义注解RBAC实现操作权限管理设计思想&#xff01;经过三个小节的部署&#xff0c;我们已经把这个思想走了一遍。下面内容是对于此次设计思想的一个详细介绍。帮助大家完善透彻的了解&#xff0c;到底自定义注解是如何实现的。以及&#xff0c;权限管理的核心思想…

程序报错无法打开源文件stdafx.h

在运行代码时&#xff0c;代码中头文件突然报错程序无法打开源文件stdafx.h include “stdafx.h”,编译器就说无法打开源文件&#xff0c;直接上干货解决方法是&#xff1a; 1.打开项目 ->项目属性&#xff08;最后一个&#xff09;-> C/C ->常规&#xff0c; 2在附…

音频几个相关概念及心理声学模型

系列文章目录 音频格式的介绍文章系列&#xff1a; 音频编解码格式介绍&#xff1a;音频几个相关概念及心理声学模型 https://blog.csdn.net/littlezls/article/details/135499627 音频编解码格式介绍&#xff1a;音频编码格式介绍 https://blog.csdn.net/littlezls/article/d…

nohost本地部署

1、安装node Node.js 官方网站下载&#xff1a;https://nodejs.org/en/download/ 2、安装whistle 安装命令为 npm install -g whistle 或 npm install -g cnpm --registryhttps://registry.npm.taobao.org 后&#xff0c;使用 cnpm install -g whistle 来安装 3、插件修改 官方…

【漏洞库】O2OA系统

O2OA invoke 后台远程命令执行漏洞 CNVD-2020-18740 漏洞描述 O2OA是一款开源免费的企业及团队办公平台&#xff0c;提供门户管理、流程管理、信息管理、数据管理四大平台,集工作汇报、项目协作、移动OA、文档分享、流程审批、数据协作等众多功能&#xff0c;满足企业各类管理…

JavaSE-项目小结-IP归属地查询(本地IP地址库)

一、项目介绍 1. 背景 IP地址是网络通信中的重要标识&#xff0c;通过分析IP地址的归属地信息&#xff0c;可以帮助我们了解访问来源、用户行为和网络安全等关键信息。例如应用于网站访问日志分析&#xff1a;通过分析访问日志中的IP地址&#xff0c;了解网站访问者的地理位置分…

【Docker】Docker Registry(镜像仓库)

文章目录 一、什么是 Docker Registry二、镜像仓库分类三、镜像仓库工作机制四、常用的镜像仓库五、常用命令镜像仓库命令镜像命令(部分)容器命令(部分) 六、docker镜像仓库实战综合实战一&#xff1a;搭建一个 nginx 服务综合实战二&#xff1a;Docker hub上创建自己私有仓库综…

使用maven对springboot项目进行瘦身

目录 一、什么是Maven 二、springboot 项目 三、springboot 项目瘦身 一、什么是Maven Maven是一个基于Java的项目管理和构建工具。它通过提供一个一致的项目结构、自动化构建脚本和依赖管理系统&#xff0c;简化了Java项目的构建过程。 Maven使用一种称为POM&#xff08;…

CentOS7局域网内搭建本地yum源

CentOS7.6 局域网内搭建本地yum源 一、背景 客户机房服务器无法直连公网&#xff0c;远程通过堡垒机部署环境&#xff0c;因为机器比较多&#xff0c;最终选择通过安装自定义yum源进行部署。以下为自己部署yum源过程&#xff0c;以备后续使用。 二、准备yum源Packages 网上…

如何以管理员身份删除node_modules文件

今天拉项目&#xff0c;然后需要安装依赖&#xff0c;但是一直报错&#xff0c;如下&#xff1a; 去搜这个问题会让把node_modules文件先删掉 再去安装依赖。我在删除的过程中会说请以管理员身份来删除。 那么windows如何以管理员身份删除node_modules文件呢&#xff1f; wi…

impala与kudu进行集成

文章目录 概要Kudu与Impala整合配置Impala内部表Impala外部表Impala sql操作kuduImpala jdbc操作表如果使用了Hadoop 使用了Kerberos认证&#xff0c;可使用如下方式进行连接。 概要 Impala是一个开源的高效率的SQL查询引擎&#xff0c;用于查询存储在Hadoop分布式文件系统&am…

性能篇:如何解决高并发下 I/O瓶颈?

大家好,我是小米!今天我们来聊一个在高并发场景下经常遇到的挑战,那就是I/O瓶颈。随着互联网的快速发展,我们的应用在处理海量数据时,I/O操作成为了一个极为关键的环节。那么,问题来了,什么是I/O呢? 什么是I/O I/O(Input/Output)是计算机系统中一个至关重要的概念,…

python+pytest接口自动化 —— 参数关联

整理了一些软件测试方面的资料、面试资料&#xff08;接口自动化、web自动化、app自动化、性能安全、测试开发等&#xff09;&#xff0c;有需要的小伙伴可以文末关注我的文末公众号或者进软件交流群&#xff0c;无套路自行领取~ 什么是参数关联&#xff1f; 参数关联&#…

Java语法学习坐标体系/绘图

Java语法学习坐标体系/绘图 大纲 基本介绍绘图 具体案例 1. 基本介绍 2.绘图 基本介绍&#xff1a; 注意每次自动调用&#xff0c;就会重新执行一次paint方法里的所有程序 先自定义面板 创建一个类继承JPanel&#xff0c;然后重写构造器&#xff0c;paint方法 class M…

机器学习5-线性回归之损失函数

在线性回归中&#xff0c;我们通常使用最小二乘法&#xff08;Ordinary Least Squares, OLS&#xff09;来求解损失函数。线性回归的目标是找到一条直线&#xff0c;使得预测值与实际值的平方差最小化。 假设有数据集 其中 是输入特征&#xff0c; 是对应的输出。 线性回归的…

css1基础选择器

大纲 一.标签选择器 比较简单&#xff0c;前面直接写目标标签 二.类选择器 应用 例子 三.多类名选择器&#xff08;调用时中间用空格隔开&#xff09; 四.id选择器 应用 五.通配符选择器 应用 六.总结