EditText選擇模式的一些問題

sunhapper發表於2019-02-19

過年這段時間正好比較有空,而且有一個客服相關的需求,借這個機會把一年前寫的支援輸入表情和@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 ,就扔了一張圖

EditText選擇模式的一些問題

不得不說這張圖還是挺有誤導性的,我最初一直以為後面輸入的部分的樣式是來自於第一個@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 {
    ...
}
複製程式碼

一波推廣

一個高效可擴充套件,在EditText/TextView中輸入和顯示gif和@mention等圖文混排內容的庫

重構過程中參考了iYaoy的思路,在此特別感謝

相關文章