Android简单支持项目符号的EditText

一、背景及样式效果      

因项目需要,需要文本编辑时,支持项目符号(无序列表)尝试了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
            }
        })

大功告成!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

新春营销不间断,AI 整活更省心

新年、春节历来都是营销的大热节点&#xff0c;各种好物集、年货节、送礼清单比比皆是。这些新鲜玩法的背后是大量的品牌内容「弹药库」。 然而&#xff0c;品牌想在竞争激烈的新春季刷满存在感&#xff0c;并非易事。一方面&#xff0c;节日期间&#xff0c;消费者对于内容的审…

交叉验证之KFold和StratifiedKFold的使用(附案例实战)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

云计算、Docker、K8S问题

1 云计算 云计算作为一种新兴技术&#xff0c;已经在现代社会中得到了广泛应用。它以其高效、灵活和可扩展特性&#xff0c;成为了许多企业和组织在数据处理和存储方面的首选方案。 1.1 什么是云计算&#xff1f;它有哪些特点&#xff1f; 云计算是一种通过网络提供计算资源…

项目02《游戏-06-开发》Unity3D

基于 项目02《游戏-05-开发》Unity3D &#xff0c; 接下来做 背包系统的 存储框架 &#xff0c; 首先了解静态数据 与 动态数据&#xff0c;静态代表不变的数据&#xff0c;比如下图武器Icon&#xff0c; 其中&#xff0c;武器的名称&#xff0c;描述&#xff…

全网第一篇把Nacos配置中心客户端讲明白的

入口 我们依旧拿ConfigExample作为入口 public class ConfigExample {public static void main(String[] args) throws NacosException, InterruptedException {String serverAddr "localhost";String dataId "test";String group "DEFAULT_GROU…

搭建frp

1.frp 是什么&#xff1f; frp 是一款高性能的反向代理应用&#xff0c;专注于内网穿透。它支持多种协议&#xff0c;包括 TCP、UDP、HTTP、HTTPS 等&#xff0c;并且具备 P2P 通信功能。使用 frp&#xff0c;您可以安全、便捷地将内网服务暴露到公网&#xff0c;通过拥有公网…

解决nvrtc: error: invalid value for --gpu-architecture (-arch)

问题描述 在使用pytorch3d的时候&#xff0c;可以正常的import&#xff0c;但是在执行错误的使用就会报&#xff0c;nvrtc: error: invalid value for --gpu-architecture (-arch)&#xff0c;的错误&#xff0c;图片如下&#xff1a; 我的环境是&#xff1a; 显卡&#xff1…

精细管理药厂设备,制药机械设备管理平台系统助力生产提效

制药行业的复杂性要求对药品的品质和安全性进行严格控制&#xff0c;而这离不开高效管理各类机械设备。然而&#xff0c;随着制药企业规模的不断扩大和技术的迅猛进步&#xff0c;如何有效管理这些设备成为一个亟待解决的问题。在这一挑战面前&#xff0c;PreMaint制药机械设备…

Antd+React+react-resizable实现表格拖拽功能

1、先看效果 2、环境准备 "dependencies": {"antd": "^5.4.0","react-resizable": "^3.0.4",},"devDependencies": {"types/react": "^18.0.33","types/react-resizable": "^…

前端面试题——Vue的双向绑定

前言 双向绑定机制是Vue中最重要的机制之一&#xff0c;甚至可以说是Vue框架的根基&#xff0c;它将数据与视图模板相分离&#xff0c;使得数据处理和页面渲染更为高效&#xff0c;同时它也是前端面试题中的常客&#xff0c;接下来让我们来了解什么是双向绑定以及其实现原理。…

Python的包安装工具——pip命令大全

对于大多数使用Python的人来说&#xff0c;一定知道pip这个包安装工具&#xff0c;但是对pip可能还不是很了解&#xff0c;今天作者给大家介绍一下pip的命令&#xff0c;以方便灵活使用pip。 一、pip工具使用方法 pip的语法如下&#xff1a; pip [options] 式中&#xff1a…

InverseMatrix3D

InverseMatrixVT3D: An Efficient Projection Matrix-Based Approach for 3D Occupancy Prediction https://github.com/DanielMing123/InverseMatrixVT3D InverseMatrix3D过程总结如下&#xff1a; 1. 用2D backbone提取N个视角的多尺度图像特征&#xff0c;表示如下&#xf…

机器学习聚类算法

聚类算法是一种无监督学习方法&#xff0c;用于将数据集中的样本划分为多个簇&#xff0c;使得同一簇内的样本相似度较高&#xff0c;而不同簇之间的样本相似度较低。在数据分析中&#xff0c;聚类算法可以帮助我们发现数据的内在结构和规律&#xff0c;从而为进一步的数据分析…

Centos 内存和硬盘占用情况以及top作用

目录 只查看内存使用情况&#xff1a; 内存使用排序取前5个&#xff1a; 硬盘占用情况 定位占用空间最大目录 top查看cpu及内存使用信息 前言-与正文无关 生活远不止眼前的苦劳与奔波&#xff0c;它还充满了无数值得我们去体验和珍惜的美好事物。在这个快节奏的世界中&…

Python 潮流周刊#38:Django + Next.js 构建全栈项目

△△请给“Python猫”加星标 &#xff0c;以免错过文章推送 你好&#xff0c;我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容&#xff0c;大部分为英文。本周刊开源&#xff0c;欢迎投稿[1]。另有电报频道[2]作为副刊&#xff0c;补充发布更加丰富的资讯&#xff0c;…

protoc结合go完成protocol buffers协议的序列化与反序列化

下载protoc编译器 下载 https://github.com/protocolbuffers/protobuf/releases ps: 根据平台选择需要的编译器&#xff0c;这里选择windows 解压 加入环境变量 安装go专用protoc生成器 https://blog.csdn.net/qq_36940806/article/details/135017748?spm1001.2014.3001.…

canvas图片上设置镂空文字效果

查看专栏目录 canvas实例应用100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

VR全景技术可以应用在哪些行业,VR全景技术有哪些优势

引言&#xff1a; VR全景技术&#xff08;Virtual Reality Panorama Technology&#xff09;是一种以虚拟现实技术为基础&#xff0c;通过360度全景影像、立体声音、交互元素等手段&#xff0c;创造出沉浸式的虚拟现实环境。该技术不仅在娱乐领域有着广泛应用&#xff0c;还可…

方案分享:F5怎么样应对混合云网络安全?

伴随着云计算走入落地阶段&#xff0c;企业的云上业务规模增长迅猛。具有部署灵活、成本低、最大化整合现有资产、促进业务创新等优点的混合云逐渐成为企业选择的部署方式。与此同时&#xff0c;安全运营的复杂度进一步提高。比如安全堆栈越来越复杂、多云基础设施和应用添加网…

小白Linux学习笔记-Linux开机启动流程

Linux 开机启动流程 文章目录 Linux 开机启动流程启动流程概览详细讲解开机软件 —— BIOS、Grub名词解释流程解释BIOS 开机文档 —— menu.lst、grub.confGrub 配置文档流程解释 init 程序流程解释init 执行的相关文件 run-level(启动等级) 相关的命令实验rhel6 单用户模式修改…