EditText選擇模式的一些問題

過年這段時間正比如較有空,並且有一個客服相關的需求,借這個機會把一年前寫的支持輸入表情和@mention的EditText又重構了一遍,具體見SpEditTool,重構過程當中對EditText選擇模式又有了一些新的認識,在這裏記錄下git

選擇模式的光標

場景描述

在實現響應軟鍵盤光標移動事件以前已經實現了讓光標不進入@mention字符串的邏輯(離start位置近就重置回start位置,離end位置近就重置回end位置),可是在光標只移動一格的狀況下會回退到以前的光標位置,光標永遠沒法跨過一個@mention字符串。因此對於軟鍵盤的光標移動時通過@mention須要特殊處理github

當selectionStart=selectionEnd時

這種狀況比較好處理,無非是判斷光標是否進入了@mention內部,左移的時候就把selectionStart和selectionEnd都設置到@mention的start位置,右移的時候設置到end位置bash

當selectionStart!=selectionEnd時

這種狀況是使用軟鍵盤選中一段文字時出現微信

在處理這個場景時,我最開始犯了一個錯誤app

int selectionStart = Selection.getSelectionStart(text);
            int selectionEnd = Selection.getSelectionEnd(text);
複製代碼

我認爲selectionStart表明簽名的光標位置,selectionEnd表明後面的光標位置,selectionStart必定小於等於selectionEnd。 由於光標左右移動並無參數表示是移動哪一個光標,因此最初實現的時候想固然的忽略了這個點,以爲左右移動只有兩種狀況:ide

光標 移動方向 結果
前面的光標 左移 選中前面的@mention
後面的光標 右移 選中後面的@mention

然而實際的狀況是四種:ui

光標 移動方向 結果
前面的光標 左移 選中左邊的@mention
前面的光標 右移 取消選中左邊的@mention
後面的光標 右移 選中右邊的@mention
後面的光標 左移 取消選中右邊的@mention

固然這樣寫出來的邏輯是有問題的,在編碼的過程當中發現其實selectionStart和selectionEnd的意思和本身最開始想的並不同this

  • 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表示變化的光標spa

最終實現代碼

//處理光標左移事件
        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 {
    ...
}
複製代碼

一波推廣

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

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