既要又要的正则匹配规则


目录

1 背景

2 浅谈

3 分析

3.1 如何识别成整体块?

3.1.1 正则匹配整体块

3.1.2 “ - ”开头“ - ”结尾

3.1.3 模糊匹配不行,采取精准匹配

3.2 如何作为整体块显示?

3.3 光标不可以中间插入

4 效果展示

5 参考代码


1 背景

        在上面这样一个文本编辑框里,点击Server name时,需要在当前光标处插入真实的Server name,且此Server name需要作为一个整体块,光标不可以在Server name中间插入,同时也需要支持整体删除。

        这里可分解成如下3个需求:

  1. 点击Server name时,需要在当前光标处插入真实的Server name;
  2. Server name需要作为一个整体块,光标不可以在Server name中间插入;
  3. Server name需要支持整体删除。

2 浅谈

        Server name作为一个文本插入到某个位置,这比较好实现,但需要作为整体块不可插入且可整体删除,这实现起来没那么容易。

  • 首先会遇到这样一个问题,如何实现整体块?
  • 其次还需要考虑,怎么实现光标不可移到整体块中间?
  • 以及,如何实现整体删除的逻辑?

3 分析

3.1 如何识别成整体块?

        要识别成整体块,需要精准确定整体块的位置,有两种方式可实现:

  • 其一,利用String的indexOf方法找到index,加上length即可确定其具体位置,但如果同时有多个整体块,这种方式就需要优化;
  • 其二,利用正则规则去匹配,可同时匹配到多个整体块。

3.1.1 正则匹配整体块

        使用正则匹配的方式可以实现,但需要考虑误匹配的问题,比如:直接使用字符串匹配肯定不合适,会直接导致整句话被匹配上。所以常规做法就是加入特殊字符,比如:“@username ”采用正则匹配的前提是——以“@”开头以“ ”结尾。username正则匹配规则和内容提取规则如下:

"[@][^@# \\f\\r\\t\\n]{1,30}[ ]" // username匹配正则规则

"(?<=@)[^@# \\f\\r\\t\\n]{1,30}(?=[ ])" // 提取username正则规则

        注:username中不允许使用“@”、“#”和空格“ ”。

3.1.2 “ - ”开头“ - ”结尾

        Server name匹配规则我们采用如下方式:以“ - ”开头以“ - ”结尾。

"( - )[^- \\f\\r\\t\\n]{1,50}( - )" // Server name匹配正则规则

"(?<=( - ))[^- \\f\\r\\t\\n]{1,50}(?=( - ))" // 提取Server name正则规则

        我们知道正则规则中的“[^- \\f\\r\\t\\n]{1,50}”表示:匹配上的50个字符中不能包含“-”、“ ”、换页“\f”、回车“\r”、制表“\t”、换行“\n”。

        这就存在一个问题,Server name是有可能包含“-”和“ ”的,要么需要定义好Server name中不允许“-”和“ ”,要么就重新设计正则匹配规则,否则会匹配不上Server name。

        那么去掉“^- ”的约束呢,像下面这种正则规则是否合适?

"( - )[^\\f\\r\\t\\n]{1,50}( - )" // Server name匹配正则规则

        答案是不行的🙅‍♂️,会存在误匹配。只有一个Server name能够精确匹配,但是多个Server name会被识别成一个Server name,因为正则匹配采用的贪婪模式,会一直往后找寻。

 

3.1.3 模糊匹配不行,采取精准匹配

        这里我们采用的是模糊匹配:“[^- \\f\\r\\t\\n]{1,50}”,那么如果采用Server name精准匹配呢?

像下面这种,事实证明可以解决模糊匹配的问题。

"( - )Server name( - )" // Server name匹配正则规则

        这个时候只要我们,根据Server name,动态生成匹配规则即可。

    private const val REGEXP_TAG_SERVER_NAME = "( - )"
        
    val pattern = Pattern.compile(
        REGEXP_TAG_SERVER_NAME + "Server name" +  REGEXP_TAG_SERVER_NAME,
        Pattern.CASE_INSENSITIVE
    )
    val matcher = pattern.matcher("待匹配的字符串")
    if (matcher.find()) {
        val start = matcher.start()
        val end = matcher.end()
    }

3.2 如何作为整体块显示?

        整块显示,实现光标不可移到整体块中间,这里Span可解决问题。万物皆Span,如果你还没使用过Span,那可以去了解下了。

    fun function(){
        val ssb = SpannableStringBuilder("Hey @username , welcome to - agg group - !");
        val what = UnEditableSpan(" - agg group - ", "", "", "")
        ssb.setSpan(what, 27, 42, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    }

    // 自定义EditText构造中调用
    setEditableFactory(
        NoCopySpanEditableFactory(
            SelectionSpanWatcher(
                UnEditableSpan::class
            )
        )
    )

        顺带一提,@username这种紫色高亮显示,他们实现如出一辙,增加下面一条setSpan即可: 

    ssb.setSpan(
        ForegroundColorSpan(Color.parseColor("#B5ABFF")),
        27,
        42,
        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
    )

3.3 光标不可以中间插入

class SelectionSpanWatcher<T : Any>(private val kClass: KClass<T>) : SpanWatcherAdapter() {
        private var selStart = 0
        private var selEnd = 0
        override fun onSpanChanged(text: Spannable,what: Any,ostart: Int,oend: Int,nstart: Int,nend: Int) {
            if (what === Selection.SELECTION_END && selEnd != nstart) {
                selEnd = nstart
                text.getSpans(nstart, nend, kClass.java).firstOrNull()?.run {
                    val spanStart = text.getSpanStart(this)
                    val spanEnd = text.getSpanEnd(this)
                    val index =
                        if (Math.abs(selEnd - spanEnd) > Math.abs(selEnd - spanStart)) spanStart else spanEnd
                    Selection.setSelection(text, Selection.getSelectionStart(text), index)
                }
            }
    
            if (what === Selection.SELECTION_START && selStart != nstart) {
                selStart = nstart
                text.getSpans(nstart, nend, kClass.java).firstOrNull()?.run {
                    val spanStart = text.getSpanStart(this)
                    val spanEnd = text.getSpanEnd(this)
                    val index =
                        if (Math.abs(selStart - spanEnd) > Math.abs(selStart - spanStart)) spanStart else spanEnd
                    Selection.setSelection(text, index, Selection.getSelectionEnd(text))
                }
            }
        }
    }

注:手动输入匹配规则及其内容也可成功匹配,如:手动输入“ - agg group - ”。 

4 效果展示

5 参考代码

    class UnEditableSpan(val showText: String = "", val hashTagName: String = "", val id: String = "", val type: String = "") : MetricAffectingSpan() {
        override fun updateMeasureState(p: TextPaint) {
        }
        override fun updateDrawState(tp: TextPaint) {
        }
    }

    class NoCopySpanEditableFactory(private vararg val spans: NoCopySpan) : Editable.Factory() {
        override fun newEditable(source: CharSequence): Editable {
            return SpannableStringBuilder.valueOf(source).apply {
                spans.forEach {
                    setSpan(it, 0, source.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
                }
            }
        }
    }

    open class SpanWatcherAdapter : SpanWatcher {
        override fun onSpanChanged(
            text: Spannable, what: Any, ostart: Int, oend: Int, nstart: Int,
            nend: Int
        ) {
        }
    
        override fun onSpanRemoved(text: Spannable, what: Any, start: Int, end: Int) {
        }
    
        override fun onSpanAdded(text: Spannable, what: Any, start: Int, end: Int) {
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值