過年這段時間正好比較有空,而且有一個客服相關的需求,借這個機會把一年前寫的支援輸入表情和@mention的EditText又重構了一遍,具體見SpEditTool,重構過程中對EditText選擇模式又有了一些新的認識,在這裡記錄下
選擇模式的游標
場景描述
在實現響應軟鍵盤游標移動事件之前已經實現了讓游標不進入@mention字串的邏輯(離start位置近就重置回start位置,離end位置近就重置回end位置),但是在游標只移動一格的情況下會回退到之前的游標位置,游標永遠無法跨過一個@mention字串。所以對於軟鍵盤的游標移動時經過@mention需要特殊處理
當selectionStart=selectionEnd時
這種情況比較好處理,無非是判斷游標是否進入了@mention內部,左移的時候就把selectionStart和selectionEnd都設定到@mention的start位置,右移的時候設定到end位置
當selectionStart!=selectionEnd時
這種情況是使用軟鍵盤選中一段文字時出現
在處理這個場景時,我最開始犯了一個錯誤
int selectionStart = Selection.getSelectionStart(text);
int selectionEnd = Selection.getSelectionEnd(text);
複製程式碼
我認為selectionStart代表簽名的游標位置,selectionEnd代表後面的游標位置,selectionStart一定小於等於selectionEnd。 因為游標左右移動並沒有參數列示是移動哪個游標,所以最初實現的時候想當然的忽略了這個點,覺得左右移動只有兩種情況:
游標 | 移動方向 | 結果 |
---|---|---|
前面的游標 | 左移 | 選中前面的@mention |
後面的游標 | 右移 | 選中後面的@mention |
然而實際的情況是四種:
游標 | 移動方向 | 結果 |
---|---|---|
前面的游標 | 左移 | 選中左邊的@mention |
前面的游標 | 右移 | 取消選中左邊的@mention |
後面的游標 | 右移 | 選中右邊的@mention |
後面的游標 | 左移 | 取消選中右邊的@mention |
當然這樣寫出來的邏輯是有問題的,在編碼的過程中發現其實selectionStart和selectionEnd的意思和自己最開始想的並不一樣
- selectionStart表示在選擇過程中不變的游標位置
- selectionEnd表示在選擇過程中移動的位置
所以知道了selectionStart/selectionEnd和左右移動方向就可以覆蓋以上的四種情況了,但是場景分類跟之前會有些區別
selectionEnd游標移動方向 | selectionEnd>selectionStart | 結果 |
---|---|---|
左移 | true | 選中左邊的@mention |
左移 | false | 取消選中右邊的@mention |
右移 | true | 選中右邊的@mention |
右移 | false | 取消選中左邊的@mention |
對於Selection.setSelection(Spannable text, int start, int stop)
,start!=stop的情況下,start表示選擇過程中不變的游標,stop表示變化的游標
最終實現程式碼
//處理游標左移事件
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表示移動的游標
if (spanEnd == selectionEnd) {
Selection.setSelection(text, selectionStart, spanStart);
return true;
}
}
}
}
//處理游標右移事件
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;
}
}
}
}
複製程式碼
兩個地方的setSelection可能有些反直覺,不過仔細想一想確實是取消選中和選中用的是同樣的引數
選擇模式下replace的問題
有個朋友在使用這個庫的時候提了個Issues #7 ,就扔了一張圖
不得不說這張圖還是挺有誤導性的,我最初一直以為後面輸入的部分的樣式是來自於第一個@mention,而且後面一長串都帶了樣式,讓我認為是持續輸入了多個字元都帶了樣式,這個現象挺讓我費解的,因為我的demo中所有setSpan(Object what, int start, int end, int flags)
的flags全都是SPAN_EXCLUSIVE_EXCLUSIVE
,按道理不會出現後面輸入的字元也帶樣式的情況,自己嘗試復現也沒有成功
今天一個偶然的操作讓我可以弄出圖上的效果,說下自己的操作路徑
- 插入兩個@mention
- 選中第二個
- 然後調出輸入法選中26鍵中文輸入模式
- 打一長串字母然後按回車
以上操作可以復現出Issues #7 中的問題,但是原因卻不是第一個@mention的樣式影響到了後面的字串,而是有兩個@mention,第二個@mention在選中狀態下被replace,樣式沒有消失
因為庫中自定義了一個SpannableStringBuilder,所以解決方案也比較簡單
@Override
public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,
int tbend) {
...
//先刪除再插入,解決選擇模式下span樣式不正常消失的問題
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;
}
複製程式碼
當然有可能Issues #7的問題並不是我這樣操作出現的,後續有碰到同樣問題的童鞋歡迎反饋
ImageSpan的replace
發現自己的東西有問題,當然得去試一試微信有沒有問題,畢竟行業標杆嘛。 令人失望的是微信的@mention並沒有上面的問題,不過微信的單個表情在選中時打字會沒有效果
反過頭看自己的表情輸入,經過上面的特別處理之後,選中單個表情輸入文字文字倒是照常輸進去了,但是表情竟然沒刪掉
除錯了一下發現選中表情時呼叫replace(int start, int end, CharSequence tb, int tbstart, int tbend)
,end只比start大1,但是demo中ImageSpan對應的字串長度應該都是4,問題就出在這裡了,對一個表情,選中情況下得replace4次才能被刪掉
原因看了下程式碼沒分析出來,不過解決方案倒是簡單,之前@mention已經實現了讓游標不能進入內部的邏輯,將對應的Span用IntegratedSpan標記下就行了
public class IsoheightImageSpan extends ImageSpan implements IntegratedSpan {
...
}
複製程式碼
一波推廣
重構過程中參考了iYaoy的思路,在此特別感謝