目录
在上面这样一个文本编辑框里,点击Server name时,需要在当前光标处插入真实的Server name,且此Server name需要作为一个整体块,光标不可以在Server name中间插入,同时也需要支持整体删除。
这里可分解成如下3个需求:
Server name作为一个文本插入到某个位置,这比较好实现,但需要作为整体块不可插入且可整体删除,这实现起来没那么容易。
要识别成整体块,需要精准确定整体块的位置,有两种方式可实现:
使用正则匹配的方式可以实现,但需要考虑误匹配的问题,比如:直接使用字符串匹配肯定不合适,会直接导致整句话被匹配上。所以常规做法就是加入特殊字符,比如:“@username ”采用正则匹配的前提是——以“@”开头以“ ”结尾。username正则匹配规则和内容提取规则如下:
- "[@][^@# \\f\\r\\t\\n]{1,30}[ ]" // username匹配正则规则
-
- "(?<=@)[^@# \\f\\r\\t\\n]{1,30}(?=[ ])" // 提取username正则规则
注:username中不允许使用“@”、“#”和空格“ ”。
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,因为正则匹配采用的贪婪模式,会一直往后找寻。
这里我们采用的是模糊匹配:“[^- \\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()
- }
整块显示,实现光标不可移到整体块中间,这里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
- )
- class SelectionSpanWatcher<T : Any>(private val kClass: KClass
) : 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 - ”。
- 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) {
- }
- }