目录
1 背景
在上面这样一个文本编辑框里,点击Server name时,需要在当前光标处插入真实的Server name,且此Server name需要作为一个整体块,光标不可以在Server name中间插入,同时也需要支持整体删除。
这里可分解成如下3个需求:
- 点击Server name时,需要在当前光标处插入真实的Server name;
- Server name需要作为一个整体块,光标不可以在Server name中间插入;
- 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) {
}
}