Android 鍵盤相關常見問題有:
- 限制輸入框內字數,超過字數不讓輸入,並且提示
- 點選外部區域鍵盤自動收起
- 如何獲取鍵盤高度
- 鍵盤與皮膚的切換衝突
下面將對上述問題各個擊破。
1. 限制輸入框內字數,超過字數不讓輸入,並且提示
etReply.setFilters(new InputFilter[]{new InputFilter() {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
if (source.length() + dest.length() > COMMENT_MAX_NUM) {
Crouton.makeText(AppUtils.getString(R.string.infodetail_comment_limit), Style.ALERT).show();
}
return null;
}
}, new InputFilter.LengthFilter(COMMENT_MAX_NUM)});
複製程式碼
2. 點選外部區域鍵盤自動收起
如果當前頁面是Activity那麼可以直接重寫dispatchTouchEvent
方法。在ACTION_DOWN
事件時,判斷點選的座標是否在輸入框座標的上面,如果是那麼呼叫隱藏鍵盤的方法。
如果當前頁面是Fragment,那麼Fragment中增加一個dispatchTouchEvent
方法,內部邏輯同上,在Fragment所依賴的Activity程式碼中將dispatchTouchEvent
事件透傳給Fragment的dispatchTouchEvent
,如果鍵盤需要隱藏,Fragment的dispatchTouchEvent
方法需要返回true
,表示消費本次所有觸控事件,不再繼續傳遞。
3. 如何獲取鍵盤高度
首先需要知道一點,鍵盤高度不是固定的。使用者使用不同的輸入法,高度可能不一樣;甚至有些輸入法,可以直接調節輸入法皮膚的高度。
3.1 有沒有系統的api可以供我們獲取鍵盤高度?
沒有。
3.2 有什麼方法可以間接獲取鍵盤高度?
系統給我們提供了一個頁面佈局變化的監聽器OnGlobalLayoutListener
,這個監聽器可以通知我們佈局發生改變,我們可以在此時獲取自己的高度,再通過螢幕寬度和狀態列高度等間接計算出鍵盤的高度。
-
那麼就有一個問題,
OnGlobalLayoutListener
接收到變化動作時,一定是鍵盤彈出或消失麼?不一定。
-
那為什麼使用
OnGlobalLayoutListener
可以監聽鍵盤的狀態?我們知道每個view的寬高變化都會導致
OnGlobalLayoutListener
的觸發,但是對當前Activity的Window物件中的DecorView進行監聽時,一般來說,DecorView的尺寸不會發生變化,發生變化的主要原因就是鍵盤的收起和展開,這時候加上簡單的判斷(變化超過某個閾值)就可以獲取鍵盤的高度,以及是否彈起。 -
有沒有使用
OnGlobalLayoutListener
監聽鍵盤失效的情景?在Android7.0上,我們可以使用多工鍵開啟分屏/多視窗模式,當我們開啟分屏之後,調整分屏的分界線時,都會觸發
DecorView
的OnGlobalLayoutListener
,但是此時鍵盤並未觸發任何動作;而且,當我們點選某個輸入框之後,鍵盤在分屏模式下會變成懸浮模式,不會擠壓Activity的控制元件,所以當鍵盤彈出或收起時,OnGlobalLayoutListener
不會接收到任何事件。這就導致了OnGlobalLayoutListener
完全失效。還有一些其他的場景導致監聽鍵盤事件失效的情景,暫時想不起來,可以在評論處補充。
3.3 獲取鍵盤高度
在一般情況下,我們對Activity
的PhoneWindow
中DecorView
的佈局變化進行監聽,一般來說,變化值超過60dp
就可以認為是鍵盤彈出或收起了。而且在非全屏主題下,
鍵盤高度 = 螢幕高度 - 狀態列高度 - 主檢視高度 - 標題欄高度, 於是我們可以通過下面程式碼間接計算出鍵盤高度。
mDecorView = this.getActivity().getWindow().getDecorView();
mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Rect rect = new Rect();
mDecorView.getWindowVisibleDisplayFrame(rect);
// 不能使用decorView.getHeight()獲取decorview的高度,獲取的高度不會發生變化
int displayHeight = rect.bottom;
if (Math.abs(displayHeight - mOldDecorViewHeight) > dp60) {
mOldDecorViewHeight =displayHeight;
int rootHeight = mRoot.getHeight();
int statusBarHeight = getStatusBarHeight();
int screenHeight = getScreenHeight();
int titleBarHeight = getTitleBarHeight();
//在非全屏模式下, 鍵盤高度 = 螢幕高度 - 狀態列高度 - 主檢視高度 - 標題欄高度
int keyboardHeight = screenHeight - statusBarHeight - rootHeight - titleBarHeight;
Log.i("lxc", "keyboardHeight ---> " + keyboardHeight + " 鍵盤: " + (keyboardHeight > 0 ? "彈出" : "收起"));
}
}
};
mDecorView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
複製程式碼
當我點選輸入框彈出和收起鍵盤時,會出現下面的log日誌:
'''console 05-29 15:22:50.368 8982-8982/com.orzangleli.myapplication I/lxc: keyboardHeight ---> 0 鍵盤: 收起 05-29 15:22:51.736 8982-8982/com.orzangleli.myapplication I/lxc: keyboardHeight ---> 873 鍵盤: 彈出 05-29 15:22:52.739 8982-8982/com.orzangleli.myapplication I/lxc: keyboardHeight ---> 0 鍵盤: 收起 05-29 15:22:53.892 8982-8982/com.orzangleli.myapplication I/lxc: keyboardHeight ---> 873 鍵盤: 彈出 '''
4. 鍵盤與皮膚的切換衝突
4.1 問題描述
在IM聊天頁面,通常下面會做成類似於微信的樣式(點選後可切換表情皮膚和鍵盤)。點選表情按鈕,會彈出表情皮膚,且表情按鈕變成鍵盤模式;再次點選鍵盤模式,或者點選輸入框,會彈出輸入框,並收起表情皮膚。以下篇幅均稱表情皮膚為皮膚。
4.2 常規思路
通常這樣的頁面佈局是一個RecyclerView+輸入區域。輸入區域在RecyclerView下面,所以整個佈局可以使用垂直的LinearLayout。鍵盤模式我們選擇adjustResize
。
常規的邏輯如下:
- 輸入區域包含輸入框和下面的表情皮膚,預設表情皮膚的
visibility
為GONE
。 - 點選表情按鈕時,皮膚的可見性為
VISIBLE
;收起輸入法鍵盤;按鈕圖片變為鍵盤模式。 - 再次點選鍵盤模式按鈕,皮膚的可見性為
GONE
;展開輸入法鍵盤;按鈕推盤變成表情模式。
我們按照上述思路寫下關鍵程式碼:
private void initView(View root) {
mInputEt = root.findViewById(R.id.et_input);
mFaceBtn = root.findViewById(R.id.btn_face);
mPanel = root.findViewById(R.id.panel);
mFaceBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mPanel.getVisibility() == View.VISIBLE) {
mPanel.setVisibility(View.GONE);
mFaceBtn.setImageResource(R.drawable.emoji_download_icon);
openKeyBoard(mInputEt);
} else {
mPanel.setVisibility(View.VISIBLE);
mFaceBtn.setImageResource(R.drawable.zz_chat_reply_keyboard);
closeKeyBoard(mInputEt);
}
}
});
}
複製程式碼
執行看看結果:
出現了奇怪的一幀:
結論:
當螢幕中已顯示鍵盤時,點選表情按鈕彈出皮膚前,需要隱藏鍵盤,但是隱藏鍵盤我們只是呼叫的一個遠端服務(Context.INPUT_METHOD_SERVICE
),它是何時執行我們無法控制(一般來說,涉及跨程式通訊,所以執行順序肯定是在皮膚顯示之後),所以我們無論我們先呼叫隱藏鍵盤api再顯示錶情皮膚,還是先顯示錶情皮膚在呼叫隱藏鍵盤的api都會出現這一幀現象,給人的感覺就是閃爍了一下。
4.3 解決方案
因此,我們隱藏和顯示錶情皮膚的時機不是點選表情按鈕時就立刻執行,而是需要等到輸入法皮膚完全顯示或完全隱藏後再進行。
這裡可能涉及到一些監聽鍵盤彈起/隱藏操作和獲取鍵盤高度的知識。可以參見上一節小結如何獲取鍵盤高度。我們獲取的鍵盤高度每次更新後直接儲存在SharedPreferences
,某些應用需要重新將彈起的皮膚高度重新設定為與鍵盤高度相同,如微信就需要記錄鍵盤的高度。但也不是每個應用都需要皮膚與鍵盤高度一致,如果你的應用不需要可以不用看如何獲取鍵盤高度。
如果我們在OnGlobalLayoutListener中監聽鍵盤的彈出或收起,並根據相應狀態設定皮膚的隱藏或顯示時會出現一些閃爍的問題(程式碼可以看Demo中的半解決切換鍵盤衝突)。因為閃爍的時間很短,所以錄製gif的時候無法看到,有興趣的可以執行Demo中半解決切換鍵盤衝突方案。
以鍵盤彈起為例,我們的流程是這樣的:
觸發鍵盤彈起 --> OnGlobalLayoutListener接收到佈局變化 --> 此時鍵盤已經完全彈起 --> X --> 隱藏表情皮膚
這個流程中的X
指的是bug出現的時候,鍵盤完全彈起時,表情鍵盤並沒有立即隱藏,而是隨後隱藏的,這就導致了半解決衝突的微弱閃爍的現象。
- 為什麼會出現這樣的微弱的閃爍?
我們來看看ViewGroup的測量過程。ViewGroup測量時,會先去遍歷測量所有的子View的尺寸,然後結合ViewGroup的測量模式計算出合適的尺寸。在我們這個案例裡,當表情皮膚已經展開時,如果切換到鍵盤,首頁鍵盤會擠壓整個佈局,也就是我們說的ViewGroup的佈局,但是此時執行ViewGroup的onMeasure時,裡面的表情皮膚仍然是可見的。然後我們在
OnGlobalLayoutListener
的回撥裡將表情皮膚的可見性設為GONE
, 但此時已經和鍵盤剛展開時已經不是同一幀了,所以看到了微弱的閃爍效果。
根據上面的分析,我們需要在鍵盤收起時的那一幀中,測量ViewGroup尺寸時,直接重新測量的皮膚控制元件的尺寸就可以了。我們把表情區域放進一個自定義的佈局控制元件KBPanelConflictLayout
,整個頁面的根佈局設為自定義控制元件KBRootConflictLayout
(程式碼可以看Demo中的解決切換鍵盤衝突)。
在KBRootConflictLayout
的onMeasure
方法中,根據佈局高度變化是否超過某個閾值來判斷是否鍵盤彈起。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
preNotifyChild(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void preNotifyChild(int width, int height) {
if (mOldHeight < 0) {
mOldHeight = height;
return ;
}
int deltaY = height - mOldHeight;
mOldHeight = height;
int minKeyboardHeight = 180;
if (Math.abs(deltaY) >= minKeyboardHeight) {
if (deltaY < 0) {
// 鍵盤彈起
if (mKBPanelConflictLayout != null) {
// 隱藏皮膚
mKBPanelConflictLayout.setHide();
}
} else {
// 鍵盤收起
if (mKBPanelConflictLayout != null) {
// 顯示皮膚
mKBPanelConflictLayout.setShow();
}
}
}
}
複製程式碼
在KBPanelConflictLayout
的onMeasure
方法中,我們根據是否隱藏狀態來判斷是否需要把鍵盤的高度變為0.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mHide) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void setHide() {
this.mHide = true;
setVisibility(View.GONE);
}
public void setShow() {
this.mHide = false;
setVisibility(View.VISIBLE);
}
複製程式碼
這樣在測試下,看看沒有閃爍衝突的效果圖吧。
如果你把上述程式碼整理優化下,加上對RelativeLayout和FrameLayout的支援,對設定是否調整皮膚高度與鍵盤一致的支援,對多皮膚的切換的支援,提供一些工具類給使用者直接呼叫,那就是2000+star的github.com/Jacksgong/J…專案了。
附上本文Demo地址,歡迎點心: