Android-軟鍵盤一招搞定(實(shí)踐篇)

前言

軟鍵盤是Android進(jìn)行用戶交互的重要途徑之一,Android應(yīng)用開發(fā)基本無法避免不使用它。然而官方?jīng)]有提供一套明確的API來獲取諸如:軟鍵盤是否正在展示、軟鍵盤高度等。本篇將著眼如此,探索解決方案。
本系列文章:

Android 軟鍵盤一招搞定(實(shí)踐篇)
Android 軟鍵盤一招搞定(原理篇)

通過本篇文章,你將了解到:

1、軟鍵盤開啟與關(guān)閉
2、軟鍵盤界面適配
3、軟鍵盤高度獲取

1、軟鍵盤開啟與關(guān)閉

為方便起見,這里用鍵盤代替軟鍵盤來說明。
平時使用最多的無非就是EditText控件,當(dāng)點(diǎn)擊EditText時鍵盤就會彈出,當(dāng)點(diǎn)擊底部導(dǎo)航欄返回按鈕時鍵盤收起,如下:


device-2020-10-11-102755 (2).gif

既然已經(jīng)有彈出、收起鍵盤的例子,那么找找其如何控制鍵盤的。

鍵盤彈出

先看看看看EditText如何彈出鍵盤的。

#TextView.java
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getActionMasked();
        ...
        final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)
                && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
        if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
                && mText instanceof Spannable && mLayout != null) {
            boolean handled = false;
            ...
            if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
                //獲取InputMethodManager
                final InputMethodManager imm = getInputMethodManager();
                viewClicked(imm);
                if (isTextEditable() && mEditor.mShowSoftInputOnFocus && imm != null) {
                    //彈出鍵盤
                    imm.showSoftInput(this, 0);
                }
                ...
            }
            ...
        }
        return superResult;
    }

可以看出彈出鍵盤只需要兩個步驟:

1、獲取InputMethodManager 實(shí)例
2、調(diào)用showSoftInput(xx)

鍵盤關(guān)閉

         InputMethodManager inputMethodManager = (InputMethodManager)view.getContext().getSystemService(INPUT_METHOD_SERVICE);
         inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);

同樣的,可以看出收起鍵盤只需要兩步:

1、獲取InputMethodManager 實(shí)例
2、調(diào)用hideSoftInputFromWindow(xx)

彈出/關(guān)閉 工具類

將彈出/關(guān)閉方法提取出來:

public class SoftInputUtil {
    public static void showSoftInput(View view) {
        if (view == null)
            return;
        InputMethodManager inputMethodManager = (InputMethodManager)view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (inputMethodManager != null) {
            inputMethodManager.showSoftInput(view, 0);
        }
    }

    public static void hideSoftInput(View view) {
        if (view == null)
            return;
        InputMethodManager inputMethodManager = (InputMethodManager)view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (inputMethodManager != null) {
            inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
        }
    }
}

有幾點(diǎn)需要注意的是:

1、inputMethodManager.showSoftInput(xx) 一般來說需要傳入的View是EditText 類型的。如果傳入其它View,需要進(jìn)行額外的操作才能彈出鍵盤。
2、inputMethodManager.showSoftInput(xx) 、inputMethodManager. hideSoftInputFromWindow(xx) 兩個方法的最后一個參數(shù)用來匹配關(guān)閉鍵盤時判斷當(dāng)初彈出鍵盤時傳入的類型,一般填0即可。
3、inputMethodManager. hideSoftInputFromWindow(xx) 第一個參數(shù)傳入的IBinder windowToken類型。每個Activity創(chuàng)建時候會生成windowToken,該值存儲在AttachInfo里,因此對于同一個Activity里的ViewTree,每個View持有的windowToken 都是指向通過一個對象。

針對上述1、3點(diǎn)再補(bǔ)充一下:
對于1點(diǎn):

假設(shè)當(dāng)傳入的View是Button類型時,需要設(shè)置Button.setFocusableInTouchMode(true),此時能夠彈出鍵盤。
比較完善的做法是:還需要在onTouchEvent(xx)里彈出鍵盤、需要將Button與鍵盤關(guān)聯(lián)。實(shí)際上就是模仿EditText的工作,系統(tǒng)都提供了EditText接收輸入字符,沒必要自己再整一套,因此彈出鍵盤時通常傳入EditText。

對于3點(diǎn):

因?yàn)橥粋€ViewTree里的windowToken都是一致的,因此不一定要傳入EditText,可以傳入Button等,只要屬于同一個ViewTree即可。

使用工具類彈出、關(guān)閉效果如下:


device-2020-10-11-212952 (1).gif

2、軟鍵盤界面適配

一個小Demo

先看看Demo,設(shè)置Activity布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/myviewgroup"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="20dp"
    android:orientation="vertical"
    android:layout_gravity="center_vertical"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/iv"
        android:src="@drawable/test"
        android:layout_marginTop="20dp"
        android:layout_marginBottom="20dp"
        android:layout_width="match_parent"
        android:layout_height="300dp">
    </ImageView>

    <EditText
        android:hint="輸入框2"
        android:id="@+id/et2"
        android:layout_gravity="bottom"
        android:layout_marginTop="200dp"
        android:layout_width="100dp"
        android:layout_height="40dp">
    </EditText>
    
</LinearLayout>

很簡單,就是個ImageView和EditText在LinearLayout里縱向排列。
當(dāng)點(diǎn)擊EditText時,效果如下:


default.gif

可以看出,EditText隨著鍵盤頂上去了,ImageView隨著鍵盤頂上去了。
試想以下問題如何解決。

1、當(dāng)鍵盤彈出時,只想EditText頂上去,ImageView保持不動
2、當(dāng)鍵盤彈出時,任何View都不需要頂上去

先簡單分析:
我們知道軟鍵盤其實(shí)就是個Dialog,當(dāng)鍵盤彈起的時,實(shí)際上當(dāng)前能看到的是兩個Window:1是Activity的Window,2是Dialog的Window。問題的關(guān)鍵是Dialog的Window展示遮蓋了Activity的Window的部分區(qū)域,為了使EditText能夠被看到,Activity的布局被向上頂上去,猜想Window 屬性有控制是否頂上去的參數(shù)。還真有,這個參數(shù)就是WindowManager.LayoutParams.softInputMode。

WindowManager.LayoutParams.softInputMode

softInputMode 顧名思義:軟鍵盤的模式
它控制著鍵盤是否可見、鍵盤關(guān)聯(lián)的EditText是否跟隨鍵盤移動等,我們重點(diǎn)關(guān)注以下屬性:

#WindowManager.java
public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;
public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;
public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;

分別介紹以上取值的作用:

SOFT_INPUT_ADJUST_UNSPECIFIED
不指定調(diào)整方式,系統(tǒng)自行決定使用哪種調(diào)整方式

SOFT_INPUT_ADJUST_RESIZE
調(diào)整方式為布局需要重新計算大小適配當(dāng)前可見區(qū)域

SOFT_INPUT_ADJUST_PAN
調(diào)整方式為布局需要整體移動

SOFT_INPUT_ADJUST_NOTHING
不做任何操作

設(shè)置softInputMode有兩種方式:
1、代碼設(shè)置:
獲取Window對象并設(shè)置

getWindow().getAttributes().softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;

2、xml里設(shè)置
在AndroidManifest.xml設(shè)置Activity 屬性

android:windowSoftInputMode="adjustResize"

來看看各種取值的效果:
SOFT_INPUT_ADJUST_UNSPECIFIED

default.gif

布局被頂上去

SOFT_INPUT_ADJUST_PAN

default.gif

布局被頂上去

SOFT_INPUT_ADJUST_RESIZE

device-2020-10-14-211817.gif

布局沒有動

SOFT_INPUT_ADJUST_NOTHING

device-2020-10-14-211817.gif

布局沒有動

通過上面4張圖,你可能就有疑惑了:怎么SOFT_INPUT_ADJUST_UNSPECIFIED和SOFT_INPUT_ADJUST_PAN 效果一致,SOFT_INPUT_ADJUST_RESIZE和SOFT_INPUT_ADJUST_NOTHING效果一致呢?
SOFT_INPUT_ADJUST_PAN 效果是頂上去,SOFT_INPUT_ADJUST_NOTHING 是不做任何操作。
這兩個值得效果沒有疑義,關(guān)鍵是SOFT_INPUT_ADJUST_RESIZE和SOFT_INPUT_ADJUST_UNSPECIFIED效果。

先來分析SOFT_INPUT_ADJUST_RESIZE
將Activity布局文件改造如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/myviewgroup"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="20dp"
    android:orientation="vertical"
    android:layout_gravity="center_vertical"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/iv"
        android:src="@drawable/test"
        android:layout_marginTop="20dp"
        android:layout_marginBottom="20dp"
        android:scaleType="fitXY"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="0dp">
    </ImageView>

    <EditText
        android:hint="輸入框2"
        android:id="@+id/et2"
        android:layout_gravity="bottom"
        android:layout_marginTop="10dp"
        android:layout_width="100dp"
        android:layout_height="40dp">
    </EditText>
    
</LinearLayout>

實(shí)際上以上布局僅僅是擴(kuò)大了ImageView展示范圍。
關(guān)于ImageView scaleType 請移步:ImageView scaleType 各種不同效果解析
其它不做更改,在SOFT_INPUT_ADJUST_RESIZE 模式下,運(yùn)行后效果如下:

device-2020-10-14-213218.gif

可以看出,ImageView被壓縮了,說明布局文件重新計算大小了。
改變布局前后的效果為什么不同?這個答案在下篇"原理篇"揭曉。

再先來分析SOFT_INPUT_ADJUST_UNSPECIFIED
還是將Activity布局文件改變,在ImageView里增加isScrollContainer 屬性

android:isScrollContainer="true"

其它不做更改,在SOFT_INPUT_ADJUST_UNSPECIFIED 模式下,運(yùn)行后效果如下:


device-2020-10-14-213218.gif

可以看到SOFT_INPUT_ADJUST_UNSPECIFIED 模式下產(chǎn)生的效果可能與SOFT_INPUT_ADJUST_PAN相同,也可能與SOFT_INPUT_ADJUST_RESIZE相同,也就是:

當(dāng)SOFT_INPUT_ADJUST_UNSPECIFIED 模式時,實(shí)際上是選擇了SOFT_INPUT_ADJUST_RESIZE或者SOFT_INPUT_ADJUST_PAN 模式進(jìn)行展示。

通過對以上四種取值的實(shí)驗(yàn),再來看看之前Demo里提出的兩個問題:

1、當(dāng)鍵盤彈出時,只想EditText頂上去,ImageView保持不動
答:接下來分析
2、當(dāng)鍵盤彈出時,任何View都不需要頂上去
答:設(shè)置SOFT_INPUT_ADJUST_NOTHING

3、軟鍵盤高度獲取

對于上面的問題1,既然想要EditText單獨(dú)頂上去,那么就需要知道當(dāng)前鍵盤彈出的高度,再設(shè)置EditText坐標(biāo)即可。
問題的關(guān)鍵轉(zhuǎn)變?yōu)槿绾潍@取鍵盤的高度。

Activity窗口的構(gòu)成

image.png

通常來說,手機(jī)由狀態(tài)欄、內(nèi)容區(qū)域、導(dǎo)航欄組成。
一般情況下(除去導(dǎo)航欄隱藏,狀態(tài)欄沉浸)對于我們來說,寫的布局文件都會展示在內(nèi)容區(qū)域,這部分是"能看到的"。
當(dāng)鍵盤彈出的時候,會遮蓋部分內(nèi)容區(qū)域:


image.png

因此,需要將被遮住的部分往上移動,移動多少呢?
通過調(diào)用方法:

#View.java
    public void getWindowVisibleDisplayFrame(Rect outRect) {
        ...
    }

可見區(qū)域的位置記錄在outRect里,而整個屏幕高度是已知的,因此就可以計算出被遮擋的區(qū)域需要頂上去的偏移量。

一個通用的計算方式

根據(jù)上面的分析,將計算方法封裝一下:

public class SoftInputUtil {

    private int softInputHeight = 0;
    private boolean softInputHeightChanged = false;

    private boolean isNavigationBarShow = false;
    private int navigationHeight = 0;

    private View anyView;
    private ISoftInputChanged listener;
    private boolean isSoftInputShowing = false;

    public interface ISoftInputChanged {
        void onChanged(boolean isSoftInputShow, int softInputHeight, int viewOffset);
    }

    public void attachSoftInput(final View anyView, final ISoftInputChanged listener) {
        if (anyView == null || listener == null)
            return;

        //根View
        final View rootView = anyView.getRootView();
        if (rootView == null)
            return;

        navigationHeight = getNavigationBarHeight(anyView.getContext());

        //anyView為需要調(diào)整高度的View,理論上來說可以是任意的View
        this.anyView = anyView;
        this.listener = listener;

        rootView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
                //對于Activity來說,該高度即為屏幕高度
                int rootHeight = rootView.getHeight();
                Rect rect = new Rect();
                //獲取當(dāng)前可見部分,默認(rèn)可見部分是除了狀態(tài)欄和導(dǎo)航欄剩下的部分
                rootView.getWindowVisibleDisplayFrame(rect);
                
                if (rootHeight - rect.bottom == navigationHeight) {
                    //如果可見部分底部與屏幕底部剛好相差導(dǎo)航欄的高度,則認(rèn)為有導(dǎo)航欄
                    isNavigationBarShow = true;
                } else if (rootHeight - rect.bottom == 0) {
                    //如果可見部分底部與屏幕底部平齊,說明沒有導(dǎo)航欄
                    isNavigationBarShow = false;
                }

                //cal softInput height
                boolean isSoftInputShow = false;
                int softInputHeight = 0;
                //如果有導(dǎo)航欄,則要去除導(dǎo)航欄的高度
                int mutableHeight = isNavigationBarShow == true ? navigationHeight : 0;
                if (rootHeight - mutableHeight > rect.bottom) {
                    //除去導(dǎo)航欄高度后,可見區(qū)域仍然小于屏幕高度,則說明鍵盤彈起了
                    isSoftInputShow = true;
                    //鍵盤高度
                    softInputHeight = rootHeight - mutableHeight - rect.bottom;
                    if (SoftInputUtils.this.softInputHeight != softInputHeight) {
                        softInputHeightChanged = true;
                        SoftInputUtils.this.softInputHeight = softInputHeight;
                    } else {
                        softInputHeightChanged = false;
                    }
                }

                //獲取目標(biāo)View的位置坐標(biāo)
                int[] location = new int[2];
                anyView.getLocationOnScreen(location);

                //條件1減少不必要的回調(diào),只關(guān)心前后發(fā)生變化的
                //條件2針對軟鍵盤切換手寫、拼音鍵等鍵盤高度發(fā)生變化
                if (isSoftInputShowing != isSoftInputShow || (isSoftInputShow && softInputHeightChanged)) {
                    if (listener != null) {
                        //第三個參數(shù)為該View需要調(diào)整的偏移量
                        //此處的坐標(biāo)都是相對屏幕左上角(0,0)為基準(zhǔn)的
                        listener.onChanged(isSoftInputShow, softInputHeight, location[1] + anyView.getHeight() - rect.bottom);
                    }

                    isSoftInputShowing = isSoftInputShow;
                }
            }
        });
    }


    //***************STATIC METHOD******************

    public static int getNavigationBarHeight(Context context) {
        if (context == null)
            return 0;
        Resources resources = context.getResources();
        int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
        int height = resources.getDimensionPixelSize(resourceId);
        return height;
    }

    public static void showSoftInput(View view) {
        if (view == null)
            return;
        InputMethodManager inputMethodManager = (InputMethodManager)view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (inputMethodManager != null) {
            inputMethodManager.showSoftInput(view, 0);
        }
    }

    public static void hideSoftInput(View view) {
        if (view == null)
            return;
        InputMethodManager inputMethodManager = (InputMethodManager)view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (inputMethodManager != null) {
            inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
        }
    }
}

使用方式如下:
在Activity里加如下代碼:

    private void attachView() {
        
        //editText2為需要調(diào)整的View
        editText2 = findViewById(R.id.et2);
        SoftInputUtil softInputUtil = new SoftInputUtil();
        softInputUtil.attachSoftInput(editText2, new SoftInputUtil.ISoftInputChanged() {
            @Override
            public void onChanged(boolean isSoftInputShow, int softInputHeight, int viewOffset) {
                if (isSoftInputShow) {
                    editText2.setTranslationY(et2.getTranslationY() - viewOffset);
                } else {
                    editText2.setTranslationY(0);
                }
            }
        });
    }

并且將windowSoftInputMode 設(shè)置為SOFT_INPUT_ADJUST_RESIZE。

android:windowSoftInputMode="adjustResize|stateAlwaysHidden"

stateAlwaysHidden 為默認(rèn)不顯示鍵盤。

再來看Activity的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/myviewgroup"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:layout_gravity="center_vertical"
    tools:context=".MainActivity">
    <ImageView
        android:id="@+id/iv"
        android:src="@drawable/test"
        android:background="@color/colorGreen"
        android:layout_marginTop="20dp"
        android:layout_marginBottom="20dp"
        android:layout_width="match_parent"
        android:layout_height="300dp">
    </ImageView>

    <EditText
        android:hint="輸入框2"
        android:id="@+id/et2"
        android:layout_marginTop="100dp"
        android:background="@drawable/bg"
        android:layout_gravity="bottom"
        android:layout_marginHorizontal="10dp"
        android:layout_width="match_parent"
        android:layout_height="40dp">
    </EditText>
</LinearLayout>

最終效果如下:


device-2020-10-16-001523.gif

可以看出,EditText被頂上去了,其它布局沒有變化。在動態(tài)切換導(dǎo)航欄是否展示之間EeditText也能正常顯示。
這就回答了上面的問題1:當(dāng)鍵盤彈出時,只想EditText頂上去,ImageView保持不動。
當(dāng)然對于問題1的還有其它更簡單的解決方式,在下一篇會分析。

以上是關(guān)于軟鍵盤彈出、關(guān)閉、是否展示、軟鍵盤高度、軟鍵盤模式等效果的解析。
你可能還有如下疑惑:

SOFT_INPUT_ADJUST_PAN 為什么能夠?qū)⒉季猪斏先ィ?br> SOFT_INPUT_ADJUST_RESIZE 為什么能夠重新設(shè)置布局區(qū)域?
SOFT_INPUT_ADJUST_UNSPECIFIED 內(nèi)部邏輯如何判斷?
鍵盤彈起為什么會執(zhí)行onLayout?
...

由于篇幅所限,這些問題將在下篇:Android 軟鍵盤一招搞定(原理篇)分析。

本文基于 Android 10.0。

如果您有需求,請直接拷貝文章里的SoftInputUtil.java 嘗試控制鍵盤。若有問題,請留言,謝謝!

如果您喜歡,請點(diǎn)贊,您的鼓勵是我前進(jìn)的動力。

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

相關(guān)閱讀更多精彩內(nèi)容

  • 為之于未有,治之于未亂。 在以往的項(xiàng)目開發(fā)中,關(guān)于軟鍵盤的知識點(diǎn)一直比較模糊,只是知道簡單的使用,當(dāng)遇到問題的時候...
    七戈閱讀 1,053評論 0 1
  • 如果在Activity中的布局的下方有EditText,獲取焦點(diǎn)彈出軟鍵盤的時候,如果不做處理,軟鍵盤可能會遮擋輸...
    張老夢閱讀 26,252評論 7 49
  • 關(guān)于Adnroid的軟鍵盤 今天在做聊天輸入框的時候,發(fā)現(xiàn)彈出的軟鍵盤把獲得焦點(diǎn)的EditText控件給擋住了,于...
    Sxgg閱讀 2,395評論 0 49
  • 軟鍵盤在Android中是重要的輸入設(shè)備,如果我們對其進(jìn)行友好化優(yōu)化的話,對提高用戶體驗(yàn)有大大的幫助。 1. In...
    Jinwong閱讀 2,499評論 0 8
  • 關(guān)于輸入框肯定有很多困惑。個人就有困惑來著。。。 主要分幾種使用場景: 1. 進(jìn)入有EditText控件的頁面,默...
    MonkeyLei閱讀 1,009評論 0 2

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