android -- EditText 設定 imeOptions 屬性為何失效?

_夏霂熠雨發表於2018-12-04

前言

最近改 bug 的時候碰到一個小知識點,在搜尋介面希望鍵盤上的 enter 鍵改為搜尋按鈕。也就是下圖的效果,是不是非常常見。

android -- EditText 設定 imeOptions 屬性為何失效?

然後我就記得 Editext 有個 imeOptions 的屬性,可以設定 enter 鍵的效果。所以果斷在 xml 中寫下 android:imeOptions="actionSearch",然後把問題改為已修復,信心滿滿。結果等編譯執行起來在手機上發現沒有起作用,還是 enter 鍵。 搜尋了一下,發現大家都是這樣回答的:

如果設定了 imeOptions 屬性,鍵盤沒有變化 ,那麼需要單獨加一些其他的屬性配合使用,xml中 屬性設定:

1 將singleLine設定為true

2.或者將inputType設定為text

試了一下,果然 ok 了。但為什麼這樣設定顯示就對了呢?你是不是同樣也有疑問? 所以就讓我們共同去探尋答案吧!

ImeOptions 屬性原始碼解析

這裡首先推薦一個看原始碼的外掛:AndroidSourceView

Step1

因為如果是在 JAVA 檔案中,我們設定 imeOptions 屬性程式碼為: editText.setImeOptions(EditorInfo.IME_ACTION_SEARCH);
所以首先要定位到 setImeOptions() 這個方法 ,在 EdiText 中沒有搜到,所以果斷跳到 EditText 的父類 TextView 的原始碼中,然後發現目標:

 /**
     * Change the editor type integer associated with the text view, which
     * is reported to an Input Method Editor (IME) with {@link EditorInfo#imeOptions}
     * when it has focus.
     * @see #getImeOptions
     * @see android.view.inputmethod.EditorInfo
     * @attr ref android.R.styleable#TextView_imeOptions
     * 當輸入法編譯器(IME)獲取焦點的時候,通過{@link EditorInfo#imeOptions} 報告給輸入法編輯器,
     * 來更改與文字檢視關聯的編輯器型別值。
     */
    public void setImeOptions(int imeOptions) {
       //1.判斷是否需要建立 Editor 物件
        createEditorIfNeeded();
        //2.判斷是否需要建立 InputContentType 
        mEditor.createInputContentTypeIfNeeded();
        //3. 將入參賦值給InputContentType.imeOptions
        mEditor.mInputContentType.imeOptions = imeOptions;
    }

複製程式碼

這個方法裡面只是進行了判斷是否需要建立一些物件,然後將我們的入參賦值給 InputContentType.imeOptions。從這方法的註釋中我們可以知道關鍵物件是 EditorInfo#imeOptions

Step2

繼續搜尋關鍵字 imeOptions ,然後發現下面這個方法:

 @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        if (onCheckIsTextEditor() && isEnabled()) {
            mEditor.createInputMethodStateIfNeeded();
            outAttrs.inputType = getInputType();
            if (mEditor.mInputContentType != null) {
                outAttrs.imeOptions = mEditor.mInputContentType.imeOptions;
                outAttrs.privateImeOptions = mEditor.mInputContentType.privateImeOptions;
                outAttrs.actionLabel = mEditor.mInputContentType.imeActionLabel;
                outAttrs.actionId = mEditor.mInputContentType.imeActionId;
                outAttrs.extras = mEditor.mInputContentType.extras;
                outAttrs.hintLocales = mEditor.mInputContentType.imeHintLocales;
            } else {
                outAttrs.imeOptions = EditorInfo.IME_NULL;
                outAttrs.hintLocales = null;
            }
            if (focusSearch(FOCUS_DOWN) != null) {
                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
            }
            if (focusSearch(FOCUS_UP) != null) {
                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
            }
            if ((outAttrs.imeOptions & EditorInfo.IME_MASK_ACTION)
                    == EditorInfo.IME_ACTION_UNSPECIFIED) {
                if ((outAttrs.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
                    // An action has not been set, but the enter key will move to
                    // the next focus, so set the action to that.
                    outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
                } else {
                    // An action has not been set, and there is no focus to move
                    // to, so let's just supply a "done" action.
                    outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
                }
                if (!shouldAdvanceFocusOnEnter()) {
                    outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
                }
            }
            if (isMultilineInputType(outAttrs.inputType)) {
                // Multi-line text editors should always show an enter key.
                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
            }
            outAttrs.hintText = mHint;
            if (mText instanceof Editable) {
                InputConnection ic = new EditableInputConnection(this);
                outAttrs.initialSelStart = getSelectionStart();
                outAttrs.initialSelEnd = getSelectionEnd();
                outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
                return ic;
            }
        }
        return null;
    }

複製程式碼

然後我們只關心我們探究的資訊,所以虛擬碼如下:

public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
            //  return mEditor == null ? EditorInfo.TYPE_NULL : mEditor.mInputType
            outAttrs.inputType = getInputType();
            if (mEditor.mInputContentType != null) {
                outAttrs.imeOptions = mEditor.mInputContentType.imeOptions;
            } else {
                outAttrs.imeOptions = EditorInfo.IME_NULL;
            }
            if (focusSearch(FOCUS_DOWN) != null) {
                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
            }
            if (focusSearch(FOCUS_UP) != null) {
                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
            }
            if ((outAttrs.imeOptions & EditorInfo.IME_MASK_ACTION)
                    == EditorInfo.IME_ACTION_UNSPECIFIED) {
             //..程式碼省略
            }
            if (isMultilineInputType(outAttrs.inputType)) {
                // Multi-line text editors should always show an enter key.
                //多行文字編譯器總是會顯示 enter 鍵
                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
            }
        return null;
    }

複製程式碼
Step3

我們首先分析一下:

  • 第一個 if 判斷: mEditor.mInputContentType 這個值是否為空,答案是 false ,因為我們在setImeOptions 方法的第 3 步已經給它賦值了,所以不會為空,所以outAttrs.imeOptions 結果為我們設定的值
  • 第二個 if 判斷:是來尋找我們的 Editext 物件的往下的一個方向是否存在可以獲取焦點的 View,這個得看實際輸入框下面佈局是否還會有一個輸入框,目前專案沒有,所以 false
  • 第三個 if 判斷:跟第二個條件一樣,只過是方向為向上查詢
  • 第四個 if 判斷:首先 (outAttrs.imeOptions & EditorInfo.IME_MASK_ACTION值是多少呢,我們程式碼裡設定的值是 IME_ACTION_SEARCH ,值為 3,EditorInfo.IME_MASK_ACTION 的值為 255 ,取二進位制值為 00000011 & 1111 1111 = 00000011 十進位制為 3,而EditorInfo.IME_ACTION_UNSPECIFIED 值為 0,所以結果為 false。

到目前為止 outAttrs.imeOptions 的結果依然為我們在程式碼中設定的值。

Step4

來看最後一個 if 判斷:isMultilineInputType(outAttrs.inputType) 光看欄位意思我們能猜到判斷輸入框是不是多行輸入。看一下具體程式碼:

private static boolean isMultilineInputType(int type) {
        return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE))
                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
    }
複製程式碼

type 這個值為我們在 onCreateInputConnection() 方法中通過getInputType () 方法獲取,結果為返回我們程式碼中設定的 inputType 值,而我們程式碼中沒有設定,那麼這個 inputType 的預設值是什麼呢?原始碼中沒有找到,然後我自己 debug 了一下,結果如下圖:

android -- EditText 設定 imeOptions 屬性為何失效?
所以預設值為 131073,而 InputType 各種型別的值如下:

public interface InputType {
    int TYPE_CLASS_DATETIME = 4;
    int TYPE_CLASS_NUMBER = 2;
    int TYPE_CLASS_PHONE = 3;
    int TYPE_CLASS_TEXT = 1;
    int TYPE_DATETIME_VARIATION_DATE = 16;
    int TYPE_DATETIME_VARIATION_NORMAL = 0;
    int TYPE_DATETIME_VARIATION_TIME = 32;
    int TYPE_MASK_CLASS = 15;
    int TYPE_MASK_FLAGS = 16773120;
    int TYPE_MASK_VARIATION = 4080;
    int TYPE_NULL = 0;
    int TYPE_NUMBER_FLAG_DECIMAL = 8192;
    int TYPE_NUMBER_FLAG_SIGNED = 4096;
    int TYPE_NUMBER_VARIATION_NORMAL = 0;
    int TYPE_NUMBER_VARIATION_PASSWORD = 16;
    int TYPE_TEXT_FLAG_AUTO_COMPLETE = 65536;
    int TYPE_TEXT_FLAG_AUTO_CORRECT = 32768;
    int TYPE_TEXT_FLAG_CAP_CHARACTERS = 4096;
    int TYPE_TEXT_FLAG_CAP_SENTENCES = 16384;
    int TYPE_TEXT_FLAG_CAP_WORDS = 8192;
    int TYPE_TEXT_FLAG_IME_MULTI_LINE = 262144;
    int TYPE_TEXT_FLAG_MULTI_LINE = 131072;
    int TYPE_TEXT_FLAG_NO_SUGGESTIONS = 524288;
    int TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 32;
    int TYPE_TEXT_VARIATION_EMAIL_SUBJECT = 48;
    int TYPE_TEXT_VARIATION_FILTER = 176;
    int TYPE_TEXT_VARIATION_LONG_MESSAGE = 80;
    int TYPE_TEXT_VARIATION_NORMAL = 0;
    int TYPE_TEXT_VARIATION_PASSWORD = 128;
    int TYPE_TEXT_VARIATION_PERSON_NAME = 96;
    int TYPE_TEXT_VARIATION_PHONETIC = 192;
    int TYPE_TEXT_VARIATION_POSTAL_ADDRESS = 112;
    int TYPE_TEXT_VARIATION_SHORT_MESSAGE = 64;
    int TYPE_TEXT_VARIATION_URI = 16;
    int TYPE_TEXT_VARIATION_VISIBLE_PASSWORD = 144;
    int TYPE_TEXT_VARIATION_WEB_EDIT_TEXT = 160;
    int TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS = 208;
    int TYPE_TEXT_VARIATION_WEB_PASSWORD = 224;
}

複製程式碼

結合 stackoverflow 上 Default inputType of EditText in android 答案和 InputType 的值,也就是說 inputType 預設值 為 InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_FLAG_MULTI_LINE 組合的結果。

然後我們繼續往下看,isMultilineInputType() 方法的返回值

(type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE))
                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
複製程式碼

轉換為十進位制為:(131073 & (15 | 131072)) == 1 | 131072 轉換為二進位制為:(100000000000000001 &(1111 | 100000000000000000)) == 1 | 100000000000000000100000000000000001 == 100000000000000001 即 true。

所以會進入最後一個條件中,而該返回值明確指出 多行文字編譯器總是會顯示 enter 鍵 。所以我們會看到我們設定的屬性失效了。

而如果我們設定了 singleLine = true ,原始碼中是這樣設定的:

 /**
     * Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
     * @param singleLine
     */
    private void setInputTypeSingleLine(boolean singleLine) {
        if (mEditor != null
                && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
                        == EditorInfo.TYPE_CLASS_TEXT) {
            if (singleLine) {
                mEditor.mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
            } else {
                mEditor.mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
            }
        }
    }
複製程式碼

如果 singleLine 為 true,inputType = ~ 131072,即011111111111111111 ,最後計算結果為 111111111111111111 == 100000000000000001,顯然 false。

或者設定了inputType = TYPE_CLASS_TEXT 我們也可以得出返回值為 false,所以不會進判斷條件,也就是 imeOptions 的值就是我們在程式碼裡或者 xml 中設定的。

----------------------------2018/12/05更新-----------------------------------

非常感謝 "與非" 的指正。之前的理解有點小偏差。

我們來看一下最後一步如果滿足了isMultilineInputType() == true這個條件,就會執行outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION; ,需要注意的是 IME_FLAG_NO_ENTER_ACTION* 這個 flag,只要設定了這個 flag,就只會顯示 “enter” 鍵。所以想要到達我們要設定的效果,關鍵不是“不能滿足多行輸入的特性”,而是需要清除這個flag,讓這個flag 失效

所以,就衍生了下面的問題:

如何設定 EditText 支援多行屬性同時我們設定的 imeOptions 效果也能實現? 答案的關鍵就是來清除 IME_FLAG_NO_ENTER_ACTION 這個 flag ,一般用位運算清除。 所以我們首先需要重寫 EditText 的 onCreateInputConnection() 方法,然後清除 IME_FLAG_NO_ENTER_ACTION 這個 flag 。 具體程式碼如下:

@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
        if(inputConnection != null){
            outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
        }
        return inputConnection;
    }
複製程式碼

ps: 查這個問題的時候還有種方案是這樣的:

editText.setHorizontallyScrolling(false); 
editText.setMaxLines(Integer.MAX_VALUE);
複製程式碼

看了程式碼,並沒有發現其原理。歡迎交流看法。

總結

所以要讓 imeOptions 滿足我們的需求,關鍵是取消 IME_FLAG_NO_ENTER_ACTION 這個標記位

可以選擇如下處理方式:

1. 設定 singleLine 屬性為 true

2. 或者設定 inputType 屬性。 而這個屬性值不一定非要是 ‘text’ 即TYPE_CLASS_TEXT 型別,只要最後結果不是 131072 (也就是 InputType.TYPE_CLASS_TEXT |InputType.TYPE_TEXT_FLAG_MULTI_LINE 即可,根據需求來定是 設定 text 或者 純數字鍵盤等等)

如果需要同時滿足多行輸入和 設定imeOptins 有效果可以重寫 EditText 的 onCreateInputConnection() 方法,然後清除 IME_FLAG_NO_ENTER_ACTION

參考文章

EditorInfo文件

EditText中imeOptions屬性使用及設定無效解決

線上進位制轉換

相關文章