Android简单支持项目符号的EditText

一、背景及样式效果      

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

        先看效果:

        Android简单支持项目符号的EditText_第1张图片

视频效果

二、自定义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)根据光标位置判断当前段落是否含有项目符号,并回显按钮状态。

 
        //点击按钮设置添加/取消项目符号
        tvBullet.setOnClickListener {
            tvBullet.isSelected = !tvBullet.isSelected
            etInput.setBullet(tvBullet.isSelected)
        }
        //项目符号状态监听,回显到按钮
        etInput.setEditListener(object :BulletEditText.EditListener{
            override fun onBulletStateChange(isOpen: Boolean) {
                tvBullet.isSelected = isOpen
            }
        })

大功告成!

你可能感兴趣的:(android,android,BulletEditText,项目符号,Bullet,EditText,文本编辑器)