Android 軟鍵盤踩坑記

蘇燦烤魚發表於2018-12-21

最近在開發一個朋友圈產品的時候遇到一個bug:軟鍵盤遮罩,在解決的過程中我通過百度、谷歌查詢了好半天,最終經歷了一番坎坷才解決,具體過程且聽我娓娓道來!

一、windowSoftInputMode

這個是在遇到軟鍵盤相關的問題,腦子裡第一個想到的知識點,但是效果如何呢?能解決問題,但是不完美! 先看沒有解決的效果圖:

Android 軟鍵盤踩坑記

就是說當一個列表的最後一項需要輸入時,軟鍵盤會將EditText完全遮罩! 其實將就一下這個效果也可以,但是我們可以選擇不將就——死磕到底!因此我們來看看 windowSoftInputMode 怎麼解決這個問題?

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

上面的表格說明了兩個問題:軟鍵盤顯示與Activity響應策略。在上面的專案中,軟鍵盤顯示是沒有問題的,只是Activity的部分內容被遮罩,可以調整策略解決的。那麼我們來依次嘗試一下這些個響應策略!

  • stateUnspecified:

Android 軟鍵盤踩坑記

預設的策略一進來軟鍵盤就將底部遮擋了,我們都無法操作底部的 item ,因此我們需要進來時不顯示軟鍵盤,增加一個策略

	android:windowSoftInputMode="stateHidden|stateUnspecified"
複製程式碼

現在進來倒是不顯示了,但是點選底部的item時還是一樣會被遮擋:

Android 軟鍵盤踩坑記

  • adjustPan:
	android:windowSoftInputMode="stateHidden|stateUnspecified"
複製程式碼

Android 軟鍵盤踩坑記

adjustPan 策略確實將 Activity 主視窗平移上去了,但是我的 Title 部分也平移上去了!這就是我說的不完美的地方,那麼我們試一下主視窗重繪呢?

  • adjustResize :
	android:windowSoftInputMode="stateHidden|adjustResize"
複製程式碼

Android 軟鍵盤踩坑記

adjustResize 策略並沒有起到作用,底部的輸入介面依然被遮罩了,這裡我只能接受 adjustPan 策略了!但是還有一個 adjustNoting 策略看看會不會是一樣?既然死磕,我們們就一個一個都試個遍!

  • adjustNoting
	android:windowSoftInputMode="stateHidden|adjustNothing"
複製程式碼

很好,果然沒有令我們失望,確實是不行!

Android 軟鍵盤踩坑記

而 ConstraintLayout、RelativeLayout 以及 FrameLayout 佈局將 EditText 置於佈局底部測試預設是正常的。

Android 軟鍵盤踩坑記

但是為什麼微信聊天頁面使用 RecyclerView 佈局的效果不是這樣的啊?為此我聯絡到了一個仿朋友圈的大神,他告訴了我第二種方法:動態計算軟鍵盤的高度

二、動態計算軟鍵盤高度

動態計算軟鍵盤的高度這一塊,我們增加一個難度,就是增加軟鍵盤與 EditText 之間的間距,說起來抽象,還是上圖:

Android 軟鍵盤踩坑記

至於為什麼要增加難度,還不是產品要求……既然人家能實現,我們也努把力實現唄!由於動態計算軟鍵盤高度這件事,我們們不需要再設定 SoftInputMode 了,因為整個過程純手工操作,不需要系統其它 api 支援了!

  1. 首先,我們需要做一些準備工作,將軟鍵盤與主頁內容剝離,主頁內容就是一個 RecyclerView ,軟鍵盤部分是一個佈局包含的 EditText ,軟鍵盤佈局如圖:

Android 軟鍵盤踩坑記

  1. 將上面的軟鍵盤進行封裝,這個是重點。說起來比較抽象,就上一張流程圖和程式碼:

Android 軟鍵盤踩坑記

	public class EmojiPanelView extends LinearLayout implements OnKeyBoardStateListener {
	···
	 public EmojiPanelView(Context context) {
        super(context);
        init();
    }
     private void init() {
        View itemView = LayoutInflater.from(getContext()).inflate(R.layout.view_emoji_panel, this, false);
        mEditText = itemView.findViewById(R.id.edit_text);
        mEditText.setOnTouchListener((v, event) -> {
            showSoftKeyBoard();
            return true;
        });

        mImageSwitch = itemView.findViewById(R.id.img_switch);
        mImageSwitch.setOnClickListener(v -> {
            if (isKeyBoardShow) {
                mImageSwitch.setImageResource(R.drawable.input_keyboard_drawable);
                changeLayoutNullParams(false);
                hideSoftKeyBoard();
                changeEmojiPanelParams(mKeyBoardHeight);
            } else {
                mImageSwitch.setImageResource(R.drawable.input_smile_drawable);
                showSoftKeyBoard();
            }
        });
        ···
        addOnSoftKeyBoardVisibleListener((Activity) getContext(), this);
        addView(itemView);
    }
 @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getY() < Utils.getScreenHeight() - Utils.dp2px(254f) && isShowing()) {
            dismiss();
        }
        return super.onTouchEvent(event);
    }
private void showSoftKeyBoard() {
        InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (inputMethodManager != null && mEditText != null) {
            mEditText.post(() -> {
                mEditText.requestFocus();
                inputMethodManager.showSoftInput(mEditText, 0);
            });
            new Handler().postDelayed(() -> {
                changeLayoutNullParams(true);
                changeEmojiPanelParams(0);
            }, 200);
        }
    }


    private void hideSoftKeyBoard() {
        InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (inputMethodManager != null && mEditText != null) {
            inputMethodManager.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
        }
    }
 private void changeLayoutNullParams(boolean isShowSoftKeyBoard) {
        LinearLayout.LayoutParams params = (LayoutParams) mLayoutNull.getLayoutParams();
        if (isShowSoftKeyBoard) {
            params.weight = 1;
            params.height = 0;
            mLayoutNull.setLayoutParams(params);
        } else {
            params.weight = 0;
            params.height = mDisplayHeight;
            mLayoutNull.setLayoutParams(params);
        }
    }

    private void changeEmojiPanelParams(int keyboardHeight) {
        if (mLayoutEmojiPanel != null) {
            LinearLayout.LayoutParams params = (LayoutParams) mLayoutEmojiPanel.getLayoutParams();
            params.height = keyboardHeight;
            mLayoutEmojiPanel.setLayoutParams(params);
        }
    }

    boolean isVisiableForLast = false;

    public void addOnSoftKeyBoardVisibleListener(Activity activity, final OnKeyBoardStateListener listener) {
        final View decorView = activity.getWindow().getDecorView();
        decorView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
            Rect rect = new Rect();
            decorView.getWindowVisibleDisplayFrame(rect);
            //計算出可見螢幕的高度
            int displayHight = rect.bottom - rect.top;
            //獲得螢幕整體的高度
            int hight = decorView.getHeight();
            //獲得鍵盤高度
            int keyboardHeight = hight - displayHight - Utils.calcStatusBarHeight(getContext());
            boolean visible = (double) displayHight / hight < 0.8;
            if (visible != isVisiableForLast) {
                listener.onSoftKeyBoardState(visible, keyboardHeight, displayHight - Utils.dp2px(48f));
            }
            isVisiableForLast = visible;
        });
    }


    @Override
    public void onSoftKeyBoardState(boolean visible, int keyboardHeight, int displayHeight) {
        this.isKeyBoardShow = visible;
        if (visible) {
            mKeyBoardHeight = keyboardHeight;
            mDisplayHeight = displayHeight;
        }
    }

	}
複製程式碼
  1. 將自定義的佈局加入到主頁內容當中,然後我們不用設定 windowSoftInputMode 就可以了。佈局:
   <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.CustomActivity">

    <include
            android:id="@+id/custom_top_layout"
            layout="@layout/toolbar_layout"/>

    <android.support.v7.widget.RecyclerView
            android:id="@+id/custom_items"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintTop_toBottomOf="@+id/custom_top_layout"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
    >

    </android.support.v7.widget.RecyclerView>

    <com.sasucen.softinput.widget.EmojiPanelView
            android:id="@+id/layout_face_panel"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent">

    </com.sasucen.softinput.widget.EmojiPanelView>

</android.support.constraint.ConstraintLayout>

複製程式碼

效果圖:

Android 軟鍵盤踩坑記

這個效果還是不錯的,但是現在大部分APP都是沉浸式狀態列了,那麼我們也加上沉浸式狀態列看看!

Android 軟鍵盤踩坑記

哦豁,輸入框被遮罩了!接下來我們們進入第三步——最終填坑!

最終填坑


我在走到這個地方的時候,當時記得抓瞎。百度了好多都沒有提及提及軟鍵盤遮罩和沉浸式狀態列之間的聯絡,使用windowSoftInputMode 的時候有效果,但是並不理想,因為EditText與軟鍵盤沒有間距了,如下圖。

Android 軟鍵盤踩坑記

後來諮詢上面的大佬的時候,他給了我一個思路——狀態列高度的丟失。後來我嘗試在螢幕可見高度以及螢幕整體高度的尺寸上做計算,結果都失敗了,EditText 完全被遮罩!因為不管 layout 增加還是還是減少狀態列的高度,EditText 的位置始終在軟鍵盤遮罩的位置。本來打算通過設定 titbar 的 padding 和修改狀態列的顏色,實現沉浸式狀態列,但是鑽牛角尖的我始終不甘心!後來想起之前看到的一篇文章隨手記技術團隊的部落格介紹“fitsSystemWindows”屬性的說明,於是我進行了嘗試:

  <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.CustomActivity">

   ······

    <com.sasucen.softinput.widget.EmojiPanelView
            android:id="@+id/layout_face_panel"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:layout_constraintBottom_toBottomOf="parent">

    </com.sasucen.softinput.widget.EmojiPanelView>

</android.support.constraint.ConstraintLayout>

複製程式碼

Android 軟鍵盤踩坑記


以上所述即是我自己關於軟鍵盤的踩坑總結,希望自己在下次不清楚的時候可以回來看看,也希望可以幫助到有需要的人。如有謬誤,還請各位指正!

原始碼

相關文章