過(guò)年這段時(shí)間正好比較有空,而且有一個(gè)客服相關(guān)的需求,借這個(gè)機(jī)會(huì)把一年前寫(xiě)的支持輸入表情和@mention的EditText又重構(gòu)了一遍,具體見(jiàn)SpEditTool,重構(gòu)過(guò)程中對(duì)EditText選擇模式又有了一些新的認(rèn)識(shí),在這里記錄下
選擇模式的光標(biāo)
場(chǎng)景描述
在實(shí)現(xiàn)響應(yīng)軟鍵盤(pán)光標(biāo)移動(dòng)事件之前已經(jīng)實(shí)現(xiàn)了讓光標(biāo)不進(jìn)入@mention字符串的邏輯(離start位置近就重置回start位置,離end位置近就重置回end位置),但是在光標(biāo)只移動(dòng)一格的情況下會(huì)回退到之前的光標(biāo)位置,光標(biāo)永遠(yuǎn)無(wú)法跨過(guò)一個(gè)@mention字符串。所以對(duì)于軟鍵盤(pán)的光標(biāo)移動(dòng)時(shí)經(jīng)過(guò)@mention需要特殊處理
當(dāng)selectionStart=selectionEnd時(shí)
這種情況比較好處理,無(wú)非是判斷光標(biāo)是否進(jìn)入了@mention內(nèi)部,左移的時(shí)候就把selectionStart和selectionEnd都設(shè)置到@mention的start位置,右移的時(shí)候設(shè)置到end位置
當(dāng)selectionStart!=selectionEnd時(shí)
這種情況是使用軟鍵盤(pán)選中一段文字時(shí)出現(xiàn)
在處理這個(gè)場(chǎng)景時(shí),我最開(kāi)始犯了一個(gè)錯(cuò)誤
int selectionStart = Selection.getSelectionStart(text);
int selectionEnd = Selection.getSelectionEnd(text);
我認(rèn)為selectionStart代表簽名的光標(biāo)位置,selectionEnd代表后面的光標(biāo)位置,selectionStart一定小于等于selectionEnd。
因?yàn)楣鈽?biāo)左右移動(dòng)并沒(méi)有參數(shù)表示是移動(dòng)哪個(gè)光標(biāo),所以最初實(shí)現(xiàn)的時(shí)候想當(dāng)然的忽略了這個(gè)點(diǎn),覺(jué)得左右移動(dòng)只有兩種情況:
| 光標(biāo) | 移動(dòng)方向 | 結(jié)果 |
|---|---|---|
| 前面的光標(biāo) | 左移 | 選中前面的@mention |
| 后面的光標(biāo) | 右移 | 選中后面的@mention |
然而實(shí)際的情況是四種:
| 光標(biāo) | 移動(dòng)方向 | 結(jié)果 |
|---|---|---|
| 前面的光標(biāo) | 左移 | 選中左邊的@mention |
| 前面的光標(biāo) | 右移 | 取消選中左邊的@mention |
| 后面的光標(biāo) | 右移 | 選中右邊的@mention |
| 后面的光標(biāo) | 左移 | 取消選中右邊的@mention |
當(dāng)然這樣寫(xiě)出來(lái)的邏輯是有問(wèn)題的,在編碼的過(guò)程中發(fā)現(xiàn)其實(shí)selectionStart和selectionEnd的意思和自己最開(kāi)始想的并不一樣
- selectionStart表示在選擇過(guò)程中不變的光標(biāo)位置
- selectionEnd表示在選擇過(guò)程中移動(dòng)的位置
所以知道了selectionStart/selectionEnd和左右移動(dòng)方向就可以覆蓋以上的四種情況了,但是場(chǎng)景分類跟之前會(huì)有些區(qū)別
| selectionEnd光標(biāo)移動(dòng)方向 | selectionEnd>selectionStart | 結(jié)果 |
|---|---|---|
| 左移 | true | 選中左邊的@mention |
| 左移 | false | 取消選中右邊的@mention |
| 右移 | true | 選中右邊的@mention |
| 右移 | false | 取消選中左邊的@mention |
對(duì)于Selection.setSelection(Spannable text, int start, int stop),start!=stop的情況下,start表示選擇過(guò)程中不變的光標(biāo),stop表示變化的光標(biāo)
最終實(shí)現(xiàn)代碼
//處理光標(biāo)左移事件
if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT
&& keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
int selectionStart = Selection.getSelectionStart(text);
int selectionEnd = Selection.getSelectionEnd(text);
IntegratedSpan[] integratedSpans = text.getSpans(selectionEnd, selectionEnd, IntegratedSpan.class);
if (integratedSpans != null && integratedSpans.length > 0) {
for (IntegratedSpan span : integratedSpans) {
int spanStart = text.getSpanStart(span);
int spanEnd = text.getSpanEnd(span);
//selectionEnd表示移動(dòng)的光標(biāo)
if (spanEnd == selectionEnd) {
Selection.setSelection(text, selectionStart, spanStart);
return true;
}
}
}
}
//處理光標(biāo)右移事件
if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT
&& keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
int selectionStart = Selection.getSelectionStart(text);
int selectionEnd = Selection.getSelectionEnd(text);
IntegratedSpan[] integratedSpans = text.getSpans(selectionEnd, selectionEnd, IntegratedSpan.class);
if (integratedSpans != null && integratedSpans.length > 0) {
for (IntegratedSpan span : integratedSpans) {
int spanStart = text.getSpanStart(span);
int spanEnd = text.getSpanEnd(span);
if (spanStart == selectionEnd) {
Selection.setSelection(text, selectionStart, spanEnd);
return true;
}
}
}
}
兩個(gè)地方的setSelection可能有些反直覺(jué),不過(guò)仔細(xì)想一想確實(shí)是取消選中和選中用的是同樣的參數(shù)
選擇模式下replace的問(wèn)題
有個(gè)朋友在使用這個(gè)庫(kù)的時(shí)候提了個(gè)Issues #7 ,就扔了一張圖

不得不說(shuō)這張圖還是挺有誤導(dǎo)性的,我最初一直以為后面輸入的部分的樣式是來(lái)自于第一個(gè)@mention,而且后面一長(zhǎng)串都帶了樣式,讓我認(rèn)為是持續(xù)輸入了多個(gè)字符都帶了樣式,這個(gè)現(xiàn)象挺讓我費(fèi)解的,因?yàn)槲业膁emo中所有setSpan(Object what, int start, int end, int flags)的flags全都是SPAN_EXCLUSIVE_EXCLUSIVE,按道理不會(huì)出現(xiàn)后面輸入的字符也帶樣式的情況,自己嘗試復(fù)現(xiàn)也沒(méi)有成功
今天一個(gè)偶然的操作讓我可以弄出圖上的效果,說(shuō)下自己的操作路徑
- 插入兩個(gè)@mention
- 選中第二個(gè)
- 然后調(diào)出輸入法選中26鍵中文輸入模式
- 打一長(zhǎng)串字母然后按回車
以上操作可以復(fù)現(xiàn)出Issues #7 中的問(wèn)題,但是原因卻不是第一個(gè)@mention的樣式影響到了后面的字符串,而是有兩個(gè)@mention,第二個(gè)@mention在選中狀態(tài)下被replace,樣式?jīng)]有消失
因?yàn)閹?kù)中自定義了一個(gè)SpannableStringBuilder,所以解決方案也比較簡(jiǎn)單
@Override
public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,
int tbend) {
...
//先刪除再插入,解決選擇模式下span樣式不正常消失的問(wèn)題
if (start != end && tbstart != tbend) {
super.replace(start, end, "", 0, 0);
super.insert(start, tb, tbstart, tbend);
} else {
super.replace(start, end, tb, tbstart, tbend);
}
...
return this;
}
當(dāng)然有可能Issues #7的問(wèn)題并不是我這樣操作出現(xiàn)的,后續(xù)有碰到同樣問(wèn)題的童鞋歡迎反饋
ImageSpan的replace
發(fā)現(xiàn)自己的東西有問(wèn)題,當(dāng)然得去試一試微信有沒(méi)有問(wèn)題,畢竟行業(yè)標(biāo)桿嘛。
令人失望的是微信的@mention并沒(méi)有上面的問(wèn)題,不過(guò)微信的單個(gè)表情在選中時(shí)打字會(huì)沒(méi)有效果
反過(guò)頭看自己的表情輸入,經(jīng)過(guò)上面的特別處理之后,選中單個(gè)表情輸入文字文字倒是照常輸進(jìn)去了,但是表情竟然沒(méi)刪掉
調(diào)試了一下發(fā)現(xiàn)選中表情時(shí)調(diào)用replace(int start, int end, CharSequence tb, int tbstart, int tbend),end只比start大1,但是demo中ImageSpan對(duì)應(yīng)的字符串長(zhǎng)度應(yīng)該都是4,問(wèn)題就出在這里了,對(duì)一個(gè)表情,選中情況下得replace4次才能被刪掉
原因看了下代碼沒(méi)分析出來(lái),不過(guò)解決方案倒是簡(jiǎn)單,之前@mention已經(jīng)實(shí)現(xiàn)了讓光標(biāo)不能進(jìn)入內(nèi)部的邏輯,將對(duì)應(yīng)的Span用IntegratedSpan標(biāo)記下就行了
public class IsoheightImageSpan extends ImageSpan implements IntegratedSpan {
...
}
一波推廣
一個(gè)高效可擴(kuò)展,在EditText/TextView中輸入和顯示gif和@mention等圖文混排內(nèi)容的庫(kù)
重構(gòu)過(guò)程中參考了iYaoy的思路,在此特別感謝