最近有个需求:评论@人。网上已经有一些文章分享了类似功能实现逻辑,但是几乎都是扩展EditText类,这种实现方式肯定不能进入我的首发阵容。你以为是因为它不符合面向对象六大原则?错,只因为它不够优雅!不够优雅!不够优雅!
那么,只有饮水机代码怎么办?当然是
read the fuking source code
功夫不负有心人,我读了一遍EditText源码,然后就造出了这个“优雅的”轮子(开玩笑,EditText源码怎么能叫fuking source code,他有一个爸爸叫TextView)。废话不多说,上酸菜。
在此之前,你需要记住一个跟文本相关的思想:一切皆Span
一、添加标签文本样式,并与标签的业务数据绑定
所有人都知道文本样式与Spannable有关。这里同样使用Spannable,我定义了一个DataBindingSpan<T>接口,主要有两个功能:
- 让用户提供一个CharSequence对象作为标签,它决定了标签文本的样式和内容
- 提供一个方法返回DataBindingSpan<T>对象所绑定的业务数据。
interface DataBindingSpan<T> {
fun spannedText(): CharSequence
fun bindingData(): T
}
示例代码:
class SpannableData(private val spanned: String): DataBindingSpan<String> {
override fun spannedText(): CharSequence {
return SpannableString(spanned).apply {
setSpan(ForegroundColorSpan(Color.RED), 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
override fun bindingData(): String {
return spanned
}
}
这个类仅仅包装了一个字符串,spannedText()返回一个改变标签文本颜色为红色的字符串,同时 bindingData()将该字符串作为业务数据返回。
你也可以把它换成其他的,user对象不错。spannedText()返回username,bindingData()返回userId,你就可以轻松实现@人功能业务数据绑定相关的逻辑了。
二、保证文本上绑定的数据的安全可靠
当我们把Span绑定到文本上以后,我们需要在文本发生变化时,保证文本和数据的安全性,可靠性,一致性。
其实从DataBindingSpan开始,我们就在处理这个事情了。正如SpannableData所展现的一样,当spannedText()返回的是一个Spannable对象时,使用Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
作为flag。它不能在头部和尾部扩展Span的范围,只允许中间插入。同时,当Span覆盖的文本被删除时,Span也会被删除。也就是说,它天生具有一定数据安全可靠的属性。这会为我们省掉很多事情。
当然,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
并不具备完全的安全性。毕竟它不能阻止中间插入。这个事情得我们自己来做。那么,为了禁止中间插入,我们应该怎么做呢?
这个需求又产生了两个问题:
- 当普通文本发生变化后,如何监控一个Span起始位置发生变化?
- 如何禁止Span内部插入光标?
对于第一个问题,我在网上看到过一种思路。维护一个Span起始位置管理器SpanRangeManager,然后利用TextWather监听文本变化,文本的任何变化都会导致SpanRangeManager重新测算Span的位置。
当然,如果我使用这种方式,就不会有这篇博客了。其实Android SDK便有一个优秀的Span管理器,那就是SpannableStringBuilder。同时SDK提供了一个侦听器SpanWatcher侦听SpannableStringBuilder中Span的变化。有兴趣的同学可以去看一看他的源码。
第二个问题,我们要保证文本与数据的一致性,禁止光标插入到Span覆盖的文本中间。有三种做法:
- 普通文本,当标签文本被破坏(删除、插入、追加文本)时,让绑定的数据失效,这就是微信的做法。
- 普通文本,把标签文本作为一个整体,不能对标签内部插入光标,杜绝数据被破坏的情况,这是微博的做法。
- 占位符,使用不可分割的Span(如ImageSpan)替换,这是QQ的做法。
微博、微信的方法都必须要对软键盘删除键、文本变化、光标活动、文本选中状态以及span变化进行监听和处理。QQ就简单多了,后面会讲到。
微博的做法
1. 侦听并处理光标活动、选中状态以及Span位置变化
对于光标活动和选中状态侦听,如果采用继承EditText的方式实现标签文本功能,重写onSelectionChanged(int selStart, int selEnd)方法便能够侦听光标活动。但是,这种方式怎么能算优雅呢?
要想“优雅地”实现怎么办?还是那句话:
read the fuking source code
两个角色:
- Selection
- SpanWatcher
如果有一篇文章叫做《Selection如何管理文本光标活动和选中状态?》,那么它一定能回答这个问题。这里不会详细讲述Selection内部实现,你只需要知道两点:
- 选中状态具有起点(start)和终点(end),而start与end反映在文本中,其实是两个NoCopySpan: START, END。
- 光标是一种特殊的选中状态,start与end在同一位置;
既然选中状态的实现是Span,它就是与View无关的,而与Spannable有关。也就是说,我们可以不使用EditText自身的API却能够管理它的光标活动和选中状态(请注意这几句话,他是“优雅实现”的基石)。
Selection管理光标活动。那么