最近有個(gè)需求:評(píng)論@人。網(wǎng)上已經(jīng)有一些文章分享了類(lèi)似功能實(shí)現(xiàn)邏輯咖气,但是幾乎都是擴(kuò)展EditText類(lèi)挨措,這種實(shí)現(xiàn)方式肯定不能進(jìn)入我的首發(fā)陣容。你以為是因?yàn)樗环厦嫦驅(qū)ο罅笤瓌t崩溪?錯(cuò)浅役,只因?yàn)樗?strong>不夠優(yōu)雅!不夠優(yōu)雅伶唯!不夠優(yōu)雅觉既!
那么,只有飲水機(jī)代碼怎么辦乳幸?當(dāng)然是
read the fuking source code
功夫不負(fù)有心人瞪讼,我讀了一遍EditText源碼,然后就造出了這個(gè)“優(yōu)雅的”輪子(開(kāi)玩笑粹断,EditText源碼怎么能叫fuking source code符欠,他有一個(gè)爸爸叫TextView)。廢話不多說(shuō)瓶埋,上酸菜希柿。
在此之前,你需要記住一個(gè)跟文本相關(guān)的思想:一切皆Span
一悬赏、添加標(biāo)簽文本樣式狡汉,并與標(biāo)簽的業(yè)務(wù)數(shù)據(jù)綁定
所有人都知道文本樣式與Spannable有關(guān)。這里同樣使用Spannable闽颇,我定義了一個(gè)DataBindingSpan<T>接口盾戴,主要有兩個(gè)功能:
- 讓用戶提供一個(gè)CharSequence對(duì)象作為標(biāo)簽,它決定了標(biāo)簽文本的樣式和內(nèi)容
- 提供一個(gè)方法返回DataBindingSpan<T>對(duì)象所綁定的業(yè)務(wù)數(shù)據(jù)兵多。
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
}
}
這個(gè)類(lèi)僅僅包裝了一個(gè)字符串尖啡,spannedText()返回一個(gè)改變標(biāo)簽文本顏色為紅色的字符串,同時(shí) bindingData()將該字符串作為業(yè)務(wù)數(shù)據(jù)返回剩膘。
你也可以把它換成其他的衅斩,user對(duì)象不錯(cuò)。spannedText()返回username怠褐,bindingData()返回userId畏梆,你就可以輕松實(shí)現(xiàn)@人功能業(yè)務(wù)數(shù)據(jù)綁定相關(guān)的邏輯了。
二、保證文本上綁定的數(shù)據(jù)的安全可靠
當(dāng)我們把Span綁定到文本上以后奠涌,我們需要在文本發(fā)生變化時(shí)宪巨,保證文本和數(shù)據(jù)的安全性,可靠性溜畅,一致性捏卓。
其實(shí)從DataBindingSpan開(kāi)始,我們就在處理這個(gè)事情了慈格。正如SpannableData所展現(xiàn)的一樣怠晴,當(dāng)spannedText()返回的是一個(gè)Spannable對(duì)象時(shí),使用Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
作為flag浴捆。它不能在頭部和尾部擴(kuò)展Span的范圍蒜田,只允許中間插入。同時(shí)汤功,當(dāng)Span覆蓋的文本被刪除時(shí)物邑,Span也會(huì)被刪除溜哮。也就是說(shuō)滔金,它天生具有一定數(shù)據(jù)安全可靠的屬性。這會(huì)為我們省掉很多事情茂嗓。
當(dāng)然餐茵,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
并不具備完全的安全性。畢竟它不能阻止中間插入述吸。這個(gè)事情得我們自己來(lái)做忿族。那么,為了禁止中間插入蝌矛,我們應(yīng)該怎么做呢道批?
這個(gè)需求又產(chǎn)生了兩個(gè)問(wèn)題:
- 當(dāng)普通文本發(fā)生變化后,如何監(jiān)控一個(gè)Span起始位置發(fā)生變化入撒?
- 如何禁止Span內(nèi)部插入光標(biāo)隆豹?
對(duì)于第一個(gè)問(wèn)題,我在網(wǎng)上看到過(guò)一種思路茅逮。維護(hù)一個(gè)Span起始位置管理器SpanRangeManager璃赡,然后利用TextWather監(jiān)聽(tīng)文本變化,文本的任何變化都會(huì)導(dǎo)致SpanRangeManager重新測(cè)算Span的位置献雅。
當(dāng)然碉考,如果我使用這種方式,就不會(huì)有這篇博客了挺身。其實(shí)Android SDK便有一個(gè)優(yōu)秀的Span管理器侯谁,那就是SpannableStringBuilder。同時(shí)SDK提供了一個(gè)偵聽(tīng)器SpanWatcher偵聽(tīng)SpannableStringBuilder中Span的變化。有興趣的同學(xué)可以去看一看他的源碼墙贱。
第二個(gè)問(wèn)題技扼,我們要保證文本與數(shù)據(jù)的一致性,禁止光標(biāo)插入到Span覆蓋的文本中間嫩痰。有三種做法:
- 普通文本剿吻,當(dāng)標(biāo)簽文本被破壞(刪除、插入串纺、追加文本)時(shí)丽旅,讓綁定的數(shù)據(jù)失效,這就是微信的做法纺棺。
- 普通文本榄笙,把標(biāo)簽文本作為一個(gè)整體,不能對(duì)標(biāo)簽內(nèi)部插入光標(biāo)祷蝌,杜絕數(shù)據(jù)被破壞的情況茅撞,這是微博的做法。
- 占位符巨朦,使用不可分割的Span(如ImageSpan)替換米丘,這是QQ的做法。
微博糊啡、微信的方法都必須要對(duì)軟鍵盤(pán)刪除鍵拄查、文本變化、光標(biāo)活動(dòng)棚蓄、文本選中狀態(tài)以及span變化進(jìn)行監(jiān)聽(tīng)和處理堕扶。QQ就簡(jiǎn)單多了,后面會(huì)講到梭依。
微博的做法
1. 偵聽(tīng)并處理光標(biāo)活動(dòng)稍算、選中狀態(tài)以及Span位置變化
對(duì)于光標(biāo)活動(dòng)和選中狀態(tài)偵聽(tīng),如果采用繼承EditText的方式實(shí)現(xiàn)標(biāo)簽文本功能役拴,重寫(xiě)onSelectionChanged(int selStart, int selEnd)方法便能夠偵聽(tīng)光標(biāo)活動(dòng)糊探。但是,這種方式怎么能算優(yōu)雅呢扎狱?
要想“優(yōu)雅地”實(shí)現(xiàn)怎么辦侧到?還是那句話:
read the fuking source code
兩個(gè)角色:
- Selection
- SpanWatcher
如果有一篇文章叫做《Selection如何管理文本光標(biāo)活動(dòng)和選中狀態(tài)?》淤击,那么它一定能回答這個(gè)問(wèn)題匠抗。這里不會(huì)詳細(xì)講述Selection內(nèi)部實(shí)現(xiàn),你只需要知道兩點(diǎn):
- 選中狀態(tài)具有起點(diǎn)(start)和終點(diǎn)(end)污抬,而start與end反映在文本中汞贸,其實(shí)是兩個(gè)NoCopySpan: START绳军, END。
- 光標(biāo)是一種特殊的選中狀態(tài)矢腻,start與end在同一位置门驾;
既然選中狀態(tài)的實(shí)現(xiàn)是Span,它就是與View無(wú)關(guān)的多柑,而與Spannable有關(guān)奶是。也就是說(shuō),我們可以不使用EditText自身的API卻能夠管理它的光標(biāo)活動(dòng)和選中狀態(tài)(請(qǐng)注意這幾句話竣灌,他是“優(yōu)雅實(shí)現(xiàn)”的基石)聂沙。
Selection管理光標(biāo)活動(dòng)。那么初嘹,SpanWatcher又是什么及汉?前面說(shuō)了,它是SpannableStringBuidler中用于偵聽(tīng)Span變化的監(jiān)聽(tīng)器屯烦。有個(gè)東西和它很像坷随,TextWatcher。沒(méi)錯(cuò)驻龟,他倆有同一個(gè)爹NoCopySpan温眉。他倆一個(gè)偵聽(tīng)文本變化,一個(gè)偵聽(tīng)Span變化迅脐。下面是SpanWatcher的源碼:
/**
* When an object of this type is attached to a Spannable, its methods
* will be called to notify it that other markup objects have been
* added, changed, or removed.
*/
public interface SpanWatcher extends NoCopySpan {
/**
* This method is called to notify you that the specified object
* has been attached to the specified range of the text.
*/
public void onSpanAdded(Spannable text, Object what, int start, int end);
/**
* This method is called to notify you that the specified object
* has been detached from the specified range of the text.
*/
public void onSpanRemoved(Spannable text, Object what, int start, int end);
/**
* This method is called to notify you that the specified object
* has been relocated from the range <code>ostart…oend</code>
* to the new range <code>nstart…nend</code> of the text.
*/
public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, int nend);
}
我們已經(jīng)知道光標(biāo)是一種Span芍殖。也就是說(shuō),我們可以通過(guò)SpanWatcher偵聽(tīng)光標(biāo)活動(dòng)谴蔑,通過(guò)Selection實(shí)現(xiàn)當(dāng)光標(biāo)移動(dòng)到Span內(nèi)部時(shí),讓它重新移動(dòng)到Span最近的邊緣位置龟梦,Span內(nèi)部永遠(yuǎn)無(wú)法插入光標(biāo)隐锭。這樣便能夠?qū)崿F(xiàn)把標(biāo)簽文本(spanned text)看作一個(gè)整體的思路。下面是代碼實(shí)現(xiàn):
package com.iyao
import android.text.Selection
import android.text.SpanWatcher
import android.text.Spannable
import kotlin.math.abs
import kotlin.reflect.KClass
class SelectionSpanWatcher<T: Any>(private val kClass: KClass<T>): SpanWatcher {
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 (abs(selEnd - spanEnd) > 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 (abs(selStart - spanEnd) > abs(selStart - spanStart)) spanStart else spanEnd
Selection.setSelection(text, index, Selection.getSelectionEnd(text))
}
}
}
override fun onSpanRemoved(text: Spannable?, what: Any?, start: Int, end: Int) {
}
override fun onSpanAdded(text: Spannable?, what: Any?, start: Int, end: Int) {
}
}
現(xiàn)在计贰,我們只需要在setText()之前把這個(gè)Span添加到文本上就可以了钦睡。
2. 偵聽(tīng)軟鍵盤(pán)刪除鍵并處理選中狀態(tài)
現(xiàn)在已經(jīng)把Span覆蓋的文本作為一個(gè)整體,且無(wú)法插入光標(biāo)躁倒,但是當(dāng)我們從Span尾部刪除文本荞怒,仍是逐字刪除。我們的要求是刪除Span文本時(shí)秧秉,能夠整體刪除整個(gè)Span褐桌,這就需要監(jiān)聽(tīng)鍵盤(pán)刪除鍵。
package com.iyao
import android.text.Selection
import android.text.Spannable
class KeyCodeDeleteHelper private constructor(){
companion object {
fun onDelDown(text: Spannable): Boolean {
val selectionStart = Selection.getSelectionStart(text)
val selectionEnd = Selection.getSelectionEnd(text)
text.getSpans(selectionStart, selectionEnd, DataBindingSpan::class.java).firstOrNull { text.getSpanEnd(it) == selectionStart }?.run {
return (selectionStart == selectionEnd).also {
val spanStart = text.getSpanStart(this)
val spanEnd = text.getSpanEnd(this)
Selection.setSelection(text, spanStart, spanEnd)
}
}
return false
}
}
}
讓我們使用它
editText.setOnKeyListener { v, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {
return@setOnKeyListener KeyCodeDeleteHelper.onDelDown((v as EditText).text)
}
return@setOnKeyListener false
}
//取數(shù)據(jù)
val strings = editText.text.let {
it.getSpans(0, it.length, DataBindingSpan::class.java)
}.map { it.bindingData() }
現(xiàn)在就可以實(shí)現(xiàn)微博一樣效果了象迎。一切都那么順利荧嵌。
然而呛踊,當(dāng)你運(yùn)行起來(lái)會(huì)發(fā)現(xiàn),SelectionSpanWatcher完全沒(méi)有效果啦撮。輪子都造好了谭网,你告訴我軸承斷了。
并且赃春,當(dāng)你打印EditText文本上的Span時(shí)愉择,你找不到SelectionSpanWatcher。這說(shuō)明SelectionSpanWatcher在setText()
過(guò)程中被清除掉了织中。那我們能不能把它放在setText()之后設(shè)置呢薄辅?如果你這么做,你會(huì)發(fā)現(xiàn)一個(gè)新問(wèn)題抠璃。setText()添加的文本沒(méi)有效果站楚。似乎我們不能通過(guò)setText()添加內(nèi)容,只能使用getText()追加內(nèi)容搏嗡。不僅如此窿春,我們必須完全禁用setText(),因?yàn)槊恳淮握{(diào)用采盒,都會(huì)清除掉SelectionSpanWatcher旧乞。
這種方式看起來(lái)還不錯(cuò),但是換一個(gè)不熟悉這個(gè)特性的人來(lái)使用怎么辦磅氨?告訴他不能用setText()方法尺栖?或者用內(nèi)聯(lián)方法或繼承的方式為EditText新增一個(gè)方法? 這些都可以烦租,唯一的缺點(diǎn)是延赌,它不是我想要的優(yōu)雅。我要讓它就像使用普通EditText一樣正常使用setText()方法叉橱。
需要思考的問(wèn)題是挫以,SelectionSpanWatcher在哪里消失了?我要重新找回這個(gè)軸承窃祝。
3. 讓輪子優(yōu)雅實(shí)現(xiàn)的軸承:Editable.Factory
SelectionSpanWatcher在setText()方法中消失了掐松。我需要去閱讀它的源碼。
EditText重寫(xiě)了getText()
粪小、setText(CharSequence text, BufferType type)
方法大磺。
@Override
public Editable getText() {
CharSequence text = super.getText();
// This can only happen during construction.
if (text == null) {
return null;
}
if (text instanceof Editable) {
return (Editable) super.getText();
}
super.setText(text, BufferType.EDITABLE);
return (Editable) super.getText();
}
@Override
public void setText(CharSequence text, BufferType type) {
super.setText(text, BufferType.EDITABLE);
}
從源碼上看,重寫(xiě)的唯一目的是將BufferType設(shè)置為BufferType.EDITABLE
探膊。
我們都知道TextView有三種文本模式:
- BufferType.NORMAL 靜態(tài)文本模式杠愧,這種模式的文本無(wú)法編輯,也沒(méi)有富文本樣式突想。
- BufferType.SPANNABLE 帶文本樣式的模式殴蹄,不可編輯究抓。當(dāng)TextView.isTextSelectable()返回true時(shí),TextView的文本模式袭灯。
- BufferType.EDITABLE EditText的文本模式刺下,可編輯,帶文本樣式稽荧。
這里不具體講這三種模式相關(guān)的內(nèi)容橘茉。只需要知道EditText的模式是BufferType.EDITABLE。
那么姨丈,BufferType.EDITABLE與“軸承”又有什么關(guān)系呢畅卓? 確實(shí)有關(guān)系。
閱讀上面的源碼片段時(shí)蟋恬,不知道有沒(méi)有人注意到setText(CharSequence)
傳入一個(gè)CharSequence對(duì)象翁潘,TextView#getText()
返回的是CharSequence對(duì)象, EditText#getText()
卻返回一個(gè)Editable對(duì)象歼争。它是在什么時(shí)候拜马,如何完成的轉(zhuǎn)換呢?它會(huì)不會(huì)是一個(gè)突破口沐绒?
從Editable getText()源碼看俩莽,它是在super.setText(text, BufferType.EDITABLE)中完成轉(zhuǎn)換的。
在TextView源碼中乔遮,setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen)
有這樣一個(gè)流程分支:
private void setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) {
if (type == BufferType.EDITABLE || getKeyListener() != null|| needEditableForNotification) {
...
Editable t = mEditableFactory.newEditable(text);
text = t;
...
}
...
mBufferType = type;
setTextInternal(text);
...
}
由此可見(jiàn)扮超,我們賦值給EditText的CharSequence對(duì)象先經(jīng)過(guò)mEditableFactory轉(zhuǎn)換為Editable對(duì)象,最終被真正賦值給EditText蹋肮,mEditableFactory的類(lèi)型正是Editable.Factory出刷,這是一個(gè)靜態(tài)內(nèi)部類(lèi)。我們看看Editable.Factory的具體實(shí)現(xiàn)是什么括尸。
/**
* Factory used by TextView to create new {@link Editable Editables}. You can subclass
* it to provide something other than {@link SpannableStringBuilder}.
*
* @see android.widget.TextView#setEditableFactory(Factory)
*/
public static class Factory {
private static Editable.Factory sInstance = new Editable.Factory();
/**
* Returns the standard Editable Factory.
*/
public static Editable.Factory getInstance() {
return sInstance;
}
/**
* Returns a new SpannedStringBuilder from the specified
* CharSequence. You can override this to provide
* a different kind of Spanned.
*/
public Editable newEditable(CharSequence source) {
return new SpannableStringBuilder(source);
}
}
很簡(jiǎn)單的轉(zhuǎn)換巷蚪,它將CharSequence對(duì)象轉(zhuǎn)換為Editable的子類(lèi)SpannableStringBuilder的對(duì)象。我們看一看這個(gè)構(gòu)造器濒翻。
public SpannableStringBuilder(CharSequence text, int start, int end) {
...
mText = ArrayUtils.newUnpaddedCharArray(GrowingArrayUtils.growSize(srclen));
...
if (text instanceof Spanned) {
Spanned sp = (Spanned) text;
Object[] spans = sp.getSpans(start, end, Object.class);
for (int i = 0; i < spans.length; i++) {
if (spans[i] instanceof NoCopySpan) {
continue;
}
...
setSpan(false, spans[i], st, en, fl, false);
}
restoreInvariants();
}
}
這就是軸承斷掉的原因所在。
前面提到SpanWatcher繼承自NoCopySpan啦膜,而NoCopySpan是一個(gè)標(biāo)記接口有送。它的作用就是標(biāo)記一個(gè)Span無(wú)法被拷貝。SpannableStringBuilder在構(gòu)造的時(shí)候僧家,會(huì)忽略掉所有NoCopySpan及其子類(lèi)雀摘。因此,SelectionSpanWatcher沒(méi)有被賦值給EditText的文本八拱。
既然NoCopySpan不被復(fù)制阵赠,那我們等SpannableStringBuilder構(gòu)造好后重新設(shè)置便好了涯塔。Editable.Factory的注釋讓我看到了希望。他可以被重寫(xiě)清蚀,并被重新注入EditText匕荸。
android.widget.TextView#setEditableFactory(Factory)
下面是重寫(xiě)的Editable.Factory,作用是重新把NoCopySpan設(shè)置到SpannableStringBuilder上枷邪。
package com.iyao
import android.text.Editable
import android.text.NoCopySpan
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.BackgroundColorSpan
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)
}
}
}
}
沒(méi)錯(cuò)榛搔,算空行一共17行代碼。它就是這個(gè)輪子的新軸承《В現(xiàn)在我們重新使用它践惑。通過(guò)editText.setEditableFactory()
換上新的軸承,讓輪子跑起來(lái)嘶卧。
editText.setEditableFactory(NoCopySpanEditableFactory(SelectionSpanWatcher(DataBindingSpan::class)))
editText.setOnKeyListener { v, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {
return@setOnKeyListener KeyCodeDeleteHelper.onDelDown((v as EditText).text)
}
return@setOnKeyListener false
}
一個(gè)“優(yōu)雅的”實(shí)現(xiàn)誕生了尔觉,你可以像微博一樣在評(píng)論中使用@人了。
微信的做法
微信的處理方式要簡(jiǎn)單一些芥吟,他們不禁止在Span覆蓋的文本中插入光標(biāo)侦铜,而是當(dāng)Span覆蓋的文本改變后清除Span以及數(shù)據(jù)。他們同樣要監(jiān)聽(tīng)刪除鍵實(shí)現(xiàn)Span整體刪除运沦,只是表現(xiàn)上與微博稍有區(qū)別泵额。
微信的三部曲。
首先携添,定義一個(gè)接口用來(lái)判斷Span是否失效嫁盲。
package com.iyao
import android.text.Spannable
interface RemoveOnDirtySpan {
fun isDirty(text: Spannable): Boolean
}
其次,讓SpannableData實(shí)現(xiàn)此接口烈掠。當(dāng)然羞秤,你也可以讓RemoveOnDirtySpan繼承DataBindingSpan,盡管我覺(jué)得這樣不符合“六大”左敌。
class SpannableData(private val spanned: String): DataBindingSpan<String>, RemoveOnDirtySpan {
override fun spannedText(): CharSequence {
return SpannableString(spanned).apply {
setSpan(ForegroundColorSpan(Color.RED), 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
override fun bindingData(): String {
return spanned
}
override fun isDirty(text: Spannable): Boolean {
val spanStart = text.getSpanStart(this)
val spanEnd = text.getSpanEnd(this)
return spanStart >= 0 && spanEnd >= 0 && text.substring(spanStart, spanEnd) != spanned
}
}
最后瘾蛋,重新寫(xiě)一個(gè)DirtySpanWatcher用來(lái)刪除失效的Span
package com.iyao
import android.text.SpanWatcher
import android.text.Spannable
class DirtySpanWatcher(private val removePredicate: (Any) -> Boolean) : SpanWatcher {
override fun onSpanChanged(text: Spannable, what: Any, ostart: Int, oend: Int, nstart: Int,
nend: Int) {
if (what is RemoveOnDirtySpan && what.isDirty(text)) {
val spanStart = text.getSpanStart(what)
val spanEnd = text.getSpanEnd(what)
text.getSpans(spanStart, spanEnd, Any::class.java).filter {
removePredicate.invoke(it)
}.forEach {
text.removeSpan(it)
}
}
}
override fun onSpanRemoved(text: Spannable, what: Any, start: Int, end: Int) {
}
override fun onSpanAdded(text: Spannable, what: Any, start: Int, end: Int) {
}
}
現(xiàn)在,我們讓微信也跑起來(lái)矫限。
editText.setEditableFactory(NoCopySpanEditableFactory(DirtySpanWatcher{
it is ForegroundColorSpan || it is RemoveOnDirtySpan
}))
editText.setOnKeyListener { v, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {
KeyCodeDeleteHelper.onDelDown((v as EditText).text)
}
return@setOnKeyListener false
}
需要注意哺哼,微信和微博有一點(diǎn)小區(qū)別,微博有二次確認(rèn)刪除選中叼风,微信沒(méi)有取董。代碼上的差別僅僅是微信少了一個(gè)return@setOnKeyListener
QQ的做法
QQ的做法太簡(jiǎn)單,我不太想講它无宿。這里寫(xiě)一個(gè)簡(jiǎn)單的Demo演示一下茵汰。
QQ同樣需要用到DataBindingSpan<T>
,甚至你也可以不用孽鸡。它的核心是ImageSpan蹂午。
class SpannableData(private val spanned: String): DataBindingSpan<String> {
override fun spannedText(): CharSequence {
return SpannableString("@$spanned ").apply {
setSpan(ImageSpan(LabelDrawable("@$spanned", color = Color.LTGRAY), spanned), 0, length-1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
override fun bindingData(): String {
return spanned
}
}
現(xiàn)在只需要實(shí)現(xiàn)一個(gè)繪制文字的Drawable栏豺,這里我取名叫LabelDrawable,也許并不準(zhǔn)確豆胸。
class LabelDrawable(val text: CharSequence, private val textPaint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
textSize = 42f
this.color = Color.DKGRAY
textAlign = Paint.Align.CENTER
}, color: Int): ColorDrawable(color) {
init {
calculateBounds()
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
canvas.drawText(text, 0, text.length, bounds.centerX().toFloat(), bounds.centerY().toFloat() + getBaselineOffset(textPaint.fontMetrics), textPaint)
}
private fun calculateBounds() {
textPaint.getTextBounds(text.toString(), 0, text.length, bounds)
bounds.inset(-8, -4)
bounds.offset(8, 0)
}
private fun getBaselineOffset(fontMetrics: Paint.FontMetrics): Float {
return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent
}
}
就像普通的Span一樣使用他就行了奥洼。
如果想要做的更好一點(diǎn),你需要處理多行文本measure配乱、layout溉卓、draw等問(wèn)題。給個(gè)小提示搬泥,TextView截屏也是一個(gè)Drawable桑寨。如果有一個(gè)View,即使它并未attach到Window上忿檩,我們也可以手動(dòng)調(diào)用measure()尉尾、layout()、draw()方法獲取一個(gè)View的截圖Drawable用來(lái)添加到ImageSpan中使用燥透,不過(guò)這樣無(wú)法響應(yīng)觸摸事件沙咏。
三、獲取文本中綁定的數(shù)據(jù)
val strings = editText.text.let {
it.getSpans(0, it.length, DataBindingSpan::class.java)
}.map { it.bindingData() }