前言
軟鍵盤是Android進(jìn)行用戶交互的重要途徑之一,Android應(yīng)用開發(fā)基本無法避免不使用它。然而官方?jīng)]有提供一套明確的API來獲取諸如:軟鍵盤是否正在展示、軟鍵盤高度等。本篇將著眼如此,探索解決方案。
本系列文章:
通過本篇文章,你將了解到:
1、軟鍵盤開啟與關(guān)閉
2、軟鍵盤界面適配
3、軟鍵盤高度獲取
1、軟鍵盤開啟與關(guān)閉
為方便起見,這里用鍵盤代替軟鍵盤來說明。
平時使用最多的無非就是EditText控件,當(dāng)點(diǎn)擊EditText時鍵盤就會彈出,當(dāng)點(diǎn)擊底部導(dǎo)航欄返回按鈕時鍵盤收起,如下:

既然已經(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)閉效果如下:

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時,效果如下:

可以看出,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

布局被頂上去
SOFT_INPUT_ADJUST_PAN

布局被頂上去
SOFT_INPUT_ADJUST_RESIZE

布局沒有動
SOFT_INPUT_ADJUST_NOTHING

布局沒有動
通過上面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)行后效果如下:

可以看出,ImageView被壓縮了,說明布局文件重新計算大小了。
改變布局前后的效果為什么不同?這個答案在下篇"原理篇"揭曉。
再先來分析SOFT_INPUT_ADJUST_UNSPECIFIED
還是將Activity布局文件改變,在ImageView里增加isScrollContainer 屬性
android:isScrollContainer="true"
其它不做更改,在SOFT_INPUT_ADJUST_UNSPECIFIED 模式下,運(yùn)行后效果如下:

可以看到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)成

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

因此,需要將被遮住的部分往上移動,移動多少呢?
通過調(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>
最終效果如下:

可以看出,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 嘗試控制鍵盤。若有問題,請留言,謝謝!