Android鍵盤操作總結

orzangleli發表於2018-05-30

Android 鍵盤相關常見問題有:

  1. 限制輸入框內字數,超過字數不讓輸入,並且提示
  2. 點選外部區域鍵盤自動收起
  3. 如何獲取鍵盤高度
  4. 鍵盤與皮膚的切換衝突

下面將對上述問題各個擊破。

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上,我們可以使用多工鍵開啟分屏/多視窗模式,當我們開啟分屏之後,調整分屏的分界線時,都會觸發DecorViewOnGlobalLayoutListener,但是此時鍵盤並未觸發任何動作;而且,當我們點選某個輸入框之後,鍵盤在分屏模式下會變成懸浮模式,不會擠壓Activity的控制元件,所以當鍵盤彈出或收起時,OnGlobalLayoutListener不會接收到任何事件。這就導致了OnGlobalLayoutListener完全失效。還有一些其他的場景導致監聽鍵盤事件失效的情景,暫時想不起來,可以在評論處補充。

3.3 獲取鍵盤高度

在一般情況下,我們對ActivityPhoneWindowDecorView的佈局變化進行監聽,一般來說,變化值超過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聊天頁面,通常下面會做成類似於微信的樣式(點選後可切換表情皮膚和鍵盤)。點選表情按鈕,會彈出表情皮膚,且表情按鈕變成鍵盤模式;再次點選鍵盤模式,或者點選輸入框,會彈出輸入框,並收起表情皮膚。以下篇幅均稱表情皮膚為皮膚。

https://user-gold-cdn.xitu.io/2018/5/30/163b083f80df3e74?w=454&h=426&f=png&s=70418

4.2 常規思路

通常這樣的頁面佈局是一個RecyclerView+輸入區域。輸入區域在RecyclerView下面,所以整個佈局可以使用垂直的LinearLayout。鍵盤模式我們選擇adjustResize

常規的邏輯如下

  • 輸入區域包含輸入框和下面的表情皮膚,預設表情皮膚的visibilityGONE
  • 點選表情按鈕時,皮膚的可見性為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);
            }
        }
    });
}
複製程式碼

執行看看結果:

https://user-gold-cdn.xitu.io/2018/5/30/163b083f80b6aca9?w=376&h=673&f=gif&s=832475

出現了奇怪的一幀:

https://user-gold-cdn.xitu.io/2018/5/30/163b083f80f77972?w=372&h=667&f=png&s=89308

結論:

當螢幕中已顯示鍵盤時,點選表情按鈕彈出皮膚前,需要隱藏鍵盤,但是隱藏鍵盤我們只是呼叫的一個遠端服務(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中的解決切換鍵盤衝突)。

KBRootConflictLayoutonMeasure方法中,根據佈局高度變化是否超過某個閾值來判斷是否鍵盤彈起。

@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();
            }
        }
    }
}
複製程式碼

KBPanelConflictLayoutonMeasure方法中,我們根據是否隱藏狀態來判斷是否需要把鍵盤的高度變為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);
}
複製程式碼

這樣在測試下,看看沒有閃爍衝突的效果圖吧。

https://user-gold-cdn.xitu.io/2018/5/30/163b083f80ce1038?w=376&h=673&f=gif&s=516860

如果你把上述程式碼整理優化下,加上對RelativeLayout和FrameLayout的支援,對設定是否調整皮膚高度與鍵盤一致的支援,對多皮膚的切換的支援,提供一些工具類給使用者直接呼叫,那就是2000+star的github.com/Jacksgong/J…專案了。

附上本文Demo地址,歡迎點心:

github.com/hust2010107…

相關文章