EditText選擇模式的一些問(wèn)題

過(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的思路,在此特別感謝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容