Android 軟鍵盤相關問題

samay發表於2017-09-25

1. windowSoftInputMode屬性的使用

Android使用windowSoftInputMode來控制Activity 的主視窗與包含螢幕軟鍵盤的視窗的互動方式。 該屬性的設定影響兩個方面:

  • 當 Activity 成為使用者注意的焦點時軟鍵盤的狀態 — 隱藏還是可見。
  • 對 Activity 主視窗所做的調整 — 是否將其尺寸調小以為軟鍵盤騰出空間,或者當視窗部分被軟鍵盤遮擋時是否平移其內容以使當前焦點可見。

該設定必須是下表所列的值之一,或者是一個“state...”值加上一個“adjust...”值的組合。 在任一組中設定多個值(例如,多個“state...”值)都會產生未定義結果。各值之間使用垂直條 (|) 分隔。 例如:

<activity android:windowSoftInputMode="stateVisible|adjustResize" . . .複製程式碼

此處設定的值(“stateUnspecified”和“adjustUnspecified”除外)替換主題中設定的值。

說明
"stateUnspecified" 不指定軟鍵盤的狀態(隱藏還是可見)。 將由系統選擇合適的狀態,或依賴主題中的設定。這是對軟鍵盤行為的預設設定。
“stateUnchanged” 當 Activity 轉至前臺時保留軟鍵盤最後所處的任何狀態,無論是可見還是隱藏。
“stateHidden” 當使用者選擇 Activity 時 — 也就是說,當使用者確實是向前導航到 Activity,而不是因離開另一 Activity 而返回時 — 隱藏軟鍵盤。
“stateAlwaysHidden” 當 Activity 的主視窗有輸入焦點時始終隱藏軟鍵盤。
“stateVisible” 在正常的適宜情況下(當使用者向前導航到 Activity 的主視窗時)顯示軟鍵盤。
“stateAlwaysVisible” 當使用者選擇 Activity 時 — 也就是說,當使用者確實是向前導航到 Activity,而不是因離開另一 Activity 而返回時 — 顯示軟鍵盤。
“adjustUnspecified” 不指定 Activity 的主視窗是否調整尺寸以為軟鍵盤騰出空間,或者視窗內容是否進行平移以在螢幕上顯露當前焦點。 系統會根據視窗的內容是否存在任何可滾動其內容的佈局檢視來自動選擇其中一種模式。 如果存在這樣的檢視,視窗將進行尺寸調整,前提是可通過滾動在較小區域內看到視窗的所有內容。這是對主視窗行為的預設設定。
“adjustResize” 始終調整 Activity 主視窗的尺寸來為螢幕上的軟鍵盤騰出空間。
“adjustPan” 不調整 Activity 主視窗的尺寸來為軟鍵盤騰出空間, 而是自動平移視窗的內容,使當前焦點永遠不被鍵盤遮蓋,讓使用者始終都能看到其輸入的內容。 這通常不如尺寸調正可取,因為使用者可能需要關閉軟鍵盤以到達被遮蓋的視窗部分或與這些部分進行互動。

系統預設值為:stateUnspecified|adjustUnspecified

上述是引用android官方文件的說明,但是這個並不能讓我們理解所有內容。因此本次將具體探究這9個屬性是如何影響。

1. stateUnspecified

android官方描述為:不指定軟鍵盤的狀態(隱藏還是可見)。 將由系統選擇合適的狀態,或依賴主題中的設定。這是對軟鍵盤行為的預設設定。哪系統認為的合適的狀態是什麼樣的呢?

我們採用如下佈局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>複製程式碼

我們發現軟鍵盤沒有自動彈出,需要手動點選EditText後,鍵盤才會彈出來。
如果採用如下佈局


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </ScrollView>
</LinearLayout>複製程式碼

我們發現軟鍵盤會自動彈出。這個就很奇怪了,為什麼就加了一個ScrollView,難道是因為新增了ScrollView,軟鍵盤就可以自動彈出來嗎?我們看一下如下的佈局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </ScrollView>
</LinearLayout>複製程式碼

執行程式碼可以發現,這樣軟鍵盤也會不會自動彈出來。說明軟鍵盤自動彈出和ScrollView沒有直接關係。

如果我們採用如下佈局,我們發現軟鍵盤還是會自動彈出

    <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <EditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

            <Button
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="測試"
                android:id="@+id/btn_test"/>
        </LinearLayout>
    </ScrollView>
</LinearLayout>複製程式碼

如何依舊採用上述的佈局,但是我們在onCreate中加上如下程式碼

    Button button= (Button) findViewById(R.id.btn_test);
    button.setFocusable(true);
    button.setFocusableInTouchMode(true);
    button.requestFocus();
    button.requestFocusFromTouch();複製程式碼

我們發現軟鍵盤將不會自動彈出來。

現在我們總結一下,當屬性設定為stateUnspecified時,系統預設時不會自動彈出軟鍵盤,但是當介面上有滾動需求時(有ListView或ScrollView等)同時有獲得焦點的輸入框軟鍵盤將自動彈出來(如果獲得焦點不是輸入框也不會自動彈出軟鍵盤)。

2. stateUnspecified

這個比較簡單,進入當前介面是否彈出軟鍵盤由上一個介面決定。如果離開上一個介面,鍵盤是開啟,那邊該介面鍵盤就是開啟;否則就是關閉的.

3. stateHidden

這個也比較簡單,進入當前介面不管當前上一個介面或者當前介面如何,預設不顯示軟鍵盤。

4. stateAlwaysHidden

這個引數和stateHidden的區別是當我們跳轉到下個介面,如果下個頁面的軟鍵盤是顯示的,而我們再次回來的時候,軟鍵盤就會隱藏起來。而設定為stateHidden,我們再次回來的時候,軟鍵盤講顯示出來。

5. stateVisible

設定這個屬性,進入這個介面時無論介面是否需要彈出軟鍵盤,軟鍵盤都會顯示。

6. stateAlwaysVisible

這個引數和stateAlwaysVisible的區別是當我們從這個介面跳轉到下個介面,從下個介面返回到這個介面是軟鍵盤是消失的,當回到這個介面軟鍵盤依舊可以彈出顯示,而stateVisible確不會。

上述6個屬性定義的是進入介面,軟鍵盤是否顯示的。下面3個屬性設定的是軟鍵盤和介面顯示內容之間的顯示關係。

7. adjustResize,adjustPan

我們將這兩個屬性放在一起討論。為了說明這個問題,我們先看如下的佈局例子

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="3"
        android:background="@android:color/holo_red_dark">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="這個是一個測試"
            android:textSize="30sp" />
    </FrameLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="4"
        android:background="@android:color/holo_blue_light">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="這個是一個測試"
            android:textSize="30sp" />
    </FrameLayout>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:background="@android:color/white">

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />
    </FrameLayout>
</LinearLayout>複製程式碼

adjustResize

adjustResize
adjustResize

adjustPan

adjustPan
adjustPan

由上面兩張圖我們可以知道,如果設定adjustResize屬性,當鍵盤顯示時,通過對介面主視窗的大小調整(壓縮等)來實現留出軟鍵盤的大小空間;如果設定adjustPan屬性,當鍵盤顯示時,通過對介面佈局的移動來保證輸入框可以顯示出來。

8. adjustUnspecified

這個屬性是根據介面中否有可以滾動的控制元件來判斷介面是採用adjustResize還是adjustPan來顯示軟鍵盤。如果有可以滾動的控制,那可以將其理解為adjustResize,通過壓縮介面來實現.但是是有一個前提的:可通過滾動在較小區域內看到視窗的所有內容。如果沒有可以滾動的控制元件或者不符合前提條件,則是採用adjustPan,及移動佈局來實現。

2. 軟鍵盤的開啟和關閉

軟鍵盤的開啟和關閉主要通過InputMethodManager實現。InputMethodManager關於開啟關閉軟鍵盤的方法主要有如下幾個方法。

方法 說明
hideSoftInputFromInputMethod(IBinder token, int flags) 關閉/隱藏軟鍵盤,讓使用者看不到軟鍵盤,但是傳入的token必須是是系統中token。
hideSoftInputFromWindow(IBinder windowToken, int flags) 和下面hideSoftInputFromWindow方法一樣,只是resultReceiver傳入的值null
hideSoftInputFromWindow(IBinder windowToken, int flags, ResultReceiver resultReceiver) 關閉/隱藏軟鍵盤,和第一個方法的區別是他傳入的token是系統介面中View視窗的token
showSoftInput(View view, int flags, ResultReceiver resultReceiver) 和hideSoftInputFromWindow對應
showSoftInput(View view, int flags) 同上面一個方法,但是預設的resultReceiver為null
showSoftInputFromInputMethod(IBinder token, int flags) 和hideSoftInputFromInputMethod對應
toggleSoftInput(int showFlags, int hideFlags) 該方法是切換軟鍵盤,如果軟鍵盤開啟,呼叫該方法軟鍵盤關閉;反之如果軟鍵盤關閉,那麼就開啟
toggleSoftInputFromWindow(IBinder windowToken, int showFlags, int hideFlags) 和上面一個方法一樣,區別是傳入的當前View的token
  1. 在上述方法中我們看到需要傳入相關flags,相關flags有如下幾個

    引數 | 說明
    -------------------|---
    HIDE_IMPLICIT_ONLY | 表示軟鍵盤只有在使用者未明確顯示的情況才被隱藏
    HIDE_NOT_ALWAYS | 表示軟鍵盤將一致隱藏,除非呼叫SHOW_FORCED才會顯示
    SHOW_FORCED | 表示軟鍵盤強制顯示不會被關閉,除非使用者明確的要關閉
    SHOW_IMPLICIT | 表示軟鍵盤接收到一個不明確的顯示請求。

  2. 我們經常看到使用者傳入的引數為0,不屬於上述4個,那顯示的是什麼呢?其實如果傳入的hideflag的話表示的是就是關閉軟鍵盤,之前傳入的引數不會改變,showflag的話表示的是SHOW_IMPLICIT

總結:

關閉軟鍵盤的方法為

    public void hideKeyboard(View view) {
        ((InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(
                view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
    }複製程式碼

開啟關鍵盤的方法為

    public void OpenKeyboard(View view) {
        ((InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE)).toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_NOT_ALWAYS);
    }複製程式碼

3. 監聽軟鍵盤的開啟和關閉

3.1 常規方法

監聽軟鍵盤的開啟和關閉最常規的方法是監聽View的層次結構,這個View的層次結構的發生全域性性的事件如佈局發生變化等我們可以通過呼叫getViewTreeObserver監聽到佈局的變化。程式碼例項如下:

    view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
        public void onGlobalLayout() {
        // 虛擬鍵的高度
        int navigationBarHeight = 0;

        int resourceId = activityRootView.getContext().getResources().getIdentifier("navigation_bar_height", "dimen", "android");
        //判斷是否有虛擬鍵
        if (resourceId > 0 && checkDeviceHasNavigationBar(activityRootView.getContext())) {
            navigationBarHeight = activityRootView.getResources().getDimensionPixelSize(resourceId);
        }

        // status bar的高度
        int statusBarHeight = 0;
        resourceId = activityRootView.getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            statusBarHeight = activityRootView.getResources().getDimensionPixelSize(resourceId);
        }

        // 獲得app的顯示大小資訊
        Rect rect = new Rect();
        ((Activity)activityRootView.getContext()).getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);

        //獲得軟鍵盤的大小:螢幕高度-(status bar的高度+虛擬鍵的高度+app的顯示高度)
        int keyboardHeight = ((Activity) activityRootView.getContext()).getWindow().getDecorView().getRootView().getHeight() - (statusBarHeight + navigationBarHeight + rect.height());

        if (keyboardHeight>100 && !isSoftKeyboardOpened) {
            isSoftKeyboardOpened = true;
            notifyOnSoftKeyboardOpened(keyboardHeight);
            Log.d(TAG, "keyboard has been opened");

        } else  if(keyboardHeight <100 && isSoftKeyboardOpened){
            isSoftKeyboardOpened = false;
            notifyOnSoftKeyboardClosed();
            Log.d(TAG, "keyboard has been closed");
        }
        }
    } )複製程式碼

status bar是否存在其實也需要判斷,但是因為app本身可以判斷當前介面是否顯示status的高度,所以上述程式碼預設status顯示。

3.2 重寫根佈局的onMeasure

該方法使用於windowSoftInputMode被設定為adjustResize。如上文所致,adjustResize是採用壓縮介面佈局來實現軟鍵盤可以正常顯示。具體程式碼如下

public class KeyBoardFrameLayout extends FrameLayout {

    public KeyBoardFrameLayout(@NonNull Context context) {
        super(context);
    }

    public KeyBoardFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public KeyBoardFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int proposedheight = MeasureSpec.getSize(heightMeasureSpec);
        final int actualHeight = getHeight();

        int keyboardHeight = actualHeight - proposedheight;
        if (keyboardHeight > 100) {
           notifyOnSoftKeyboardOpened(keyboardHeight);
        } else if (keyboardHeight < -100) {
            notifyOnSoftKeyboardClosed();
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }複製程式碼

對比上述兩種方法:

  1. 第一種方法的優點是不管windowSoftInputMode被設定為什麼,都可以實現軟鍵盤的開關鍵盤;第二種需要將上述的佈局作為介面的根佈局才能實現監聽
  2. 第一種方法的缺點是事後監聽,已當onLayout之後才會拿到監聽,這個導致介面容易出現閃屏的情況;而第二種方法是在介面onLayout之前就拿到了監聽,因此不會出現閃屏的情況。

相關文章