Android 軟鍵盤踩坑記
分享一篇關於軟鍵盤的採坑記,希望對大家在開發過程中能有幫助。
最近在開發一個朋友圈產品的時候遇到一個bug:軟鍵盤遮罩,在解決的過程中我通過百度、谷歌查找了好半天,最終經歷了一番坎坷才解決,具體過程且聽我娓娓道來!
一、windowSoftInputMode
這個是在遇到軟鍵盤相關的問題,腦子裡第一個想到的知識點,但是效果如何呢?能解決問題,但是不完美! 先看沒有解決的效果圖:

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

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

預設的策略一進來軟鍵盤就將底部遮擋了,我們都無法操作底部的 item ,因此我們需要進來時不顯示軟鍵盤,增加一個策略
android:windowSoftInputMode="stateHidden|stateUnspecified"
現在進來倒是不顯示了,但是點選底部的item時還是一樣會被遮擋:

- adjustPan:
android:windowSoftInputMode="stateHidden|stateUnspecified"

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

adjustResize 策略並沒有起到作用,底部的輸入介面依然被遮罩了,這裡我只能接受 adjustPan 策略了!但是還有一個 adjustNoting 策略看看會不會是一樣?既然死磕,咱們就一個一個都試個遍!
- adjustNoting
android:windowSoftInputMode="stateHidden|adjustNothing"
很好,果然沒有令我們失望,確實是不行!

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

但是為什麼微信聊天頁面使用 RecyclerView 佈局的效果不是這樣的啊?為此我聯絡到了一個仿朋友圈的大神,他告訴了我第二種方法:動態計算軟鍵盤的高度
二、動態計算軟鍵盤高度
動態計算軟鍵盤的高度這一塊,我們增加一個難度,就是增加軟鍵盤與 EditText 之間的間距,說起來抽象,還是上圖:

至於為什麼要增加難度,還不是產品要求……既然人家能實現,咱也努把力實現唄!由於動態計算軟鍵盤高度這件事,咱們不需要再設定 SoftInputMode 了,因為整個過程純手工操作,不需要系統其它 api 支援了!
- 首先,我們需要做一些準備工作,將軟鍵盤與主頁內容剝離,主頁內容就是一個 RecyclerView ,軟鍵盤部分是一個佈局包含的 EditText ,軟鍵盤佈局如圖:

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

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; } } }
- 將自定義的佈局加入到主頁內容當中,然後我們不用設定 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>
效果圖:
這個效果還是不錯的,但是現在大部分APP都是沉浸式狀態列了,那麼我們也加上沉浸式狀態列看看!

哦豁,輸入框被遮罩了!接下來咱們進入第三步——最終填坑!
最終填坑
我在走到這個地方的時候,當時記得抓瞎。百度了好多都沒有提及提及軟鍵盤遮罩和沉浸式狀態列之間的聯絡,使用windowSoftInputMode 的時候有效果,但是並不理想,因為EditText與軟鍵盤沒有間距了,如下圖。

後來諮詢上面的大佬的時候,他給了我一個思路——狀態列高度的丟失。後來我嘗試在螢幕可見高度以及螢幕整體高度的尺寸上做計算,結果都失敗了,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 IOC架構設計
群內有許多技術大牛,有任何問題,歡迎廣大網友一起來交流,群內還不定期免費分享高階Android學習視訊資料和麵試資料包~