优雅的监听软键盘隐藏
背景
科普基礎知識-WindowSoftInputMode
Activity 的主窗口與包含屏幕軟鍵盤的窗口的交互方式。改屬性的設置影響兩個方面:
- 當Activity成為用戶注意的焦點時軟鍵盤的狀態(tài)-隱藏還是可見
- 對Activity主窗口所做的調(diào)整-是否將其尺寸調(diào)小以為軟鍵盤騰出空間,或者當窗口部分被軟鍵盤遮擋時是否平移其那內(nèi)容使當前焦點可見。
該設置必須是下標所列的值之一,或者一個state...值加上一個adjust...值的組合。在任一一組設置多個值(例如,多個state...值)都會產(chǎn)生未定義結(jié)果。各個值之間使用垂直條(|)分割。
| stateUnspecified | 不指定軟鍵盤的狀態(tài)(隱藏還是可見)。 將由系統(tǒng)選擇合適的狀態(tài),或依賴主題中的設置。這是對軟鍵盤行為的默認設置。 |
| stateUnchanged | 當 Activity 轉(zhuǎn)至前臺時保留軟鍵盤最后所處的任何狀態(tài),無論是可見還是隱藏。 |
| stateHidden | 當用戶選擇 Activity 時 — 也就是說,當用戶確實是向前導航到 Activity,而不是因離開另一 Activity 而返回時 — 隱藏軟鍵盤。 |
| stateAlwaysHidden | 當 Activity 的主窗口有輸入焦點時始終隱藏軟鍵盤。 |
| stateVisible | 在正常的適宜情況下(當用戶向前導航到 Activity的主窗口時)顯示軟鍵盤。 |
| stateAlwaysVisible | 當用戶選擇 Activity 時 — 也就是說,當用戶確實是向前導航到 Activity,而不是因離開另一 Activity 而返回時 — 顯示軟鍵盤。 |
| adjustUnspecified | 不指定 Activity 的主窗口是否調(diào)整尺寸以為軟鍵盤騰出空間,或者窗口內(nèi)容是否進行平移以在屏幕上顯露當前焦點。 系統(tǒng)會根據(jù)窗口的內(nèi)容是否存在任何可滾動其內(nèi)容的布局視圖來自動選擇其中一種模式。 如果存在這樣的視圖,窗口將進行尺寸調(diào)整,前提是可通過滾動在較小區(qū)域內(nèi)看到窗口的所有內(nèi)容。這是對主窗口行為的默認設置。 |
| adjustResize | 始終調(diào)整 Activity 主窗口的尺寸來為屏幕上的軟鍵盤騰出空間 |
| adjustPan | 不調(diào)整 Activity 主窗口的尺寸來為軟鍵盤騰出空間, 而是自動平移窗口的內(nèi)容,使當前焦點永遠不被鍵盤遮蓋,讓用戶始終都能看到其輸入的內(nèi)容。 這通常不如尺寸調(diào)正可取,因為用戶可能需要關(guān)閉軟鍵盤以到達被遮蓋的窗口部分或與這些部分進行交互。 |
官方解釋
網(wǎng)上解決方案
第一種方案
由于Activity.onKeyDownn()是監(jiān)聽不到向下的按鍵,所以自定義Edittext,重寫onKeyPreIme方法
/*** 攔截鍵盤向下按鍵的 EditTextView*/ public class TextEditTextView extends DmtEditText {public TextEditTextView(Context context) {super(context);}public TextEditTextView(Context context, AttributeSet attrs) {super(context, attrs);}public TextEditTextView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overridepublic boolean onKeyPreIme(int keyCode, KeyEvent event) {if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == 1 && onKeyBoardHideListener != null) {onKeyBoardHideListener.onKeyHide();}return super.onKeyPreIme(keyCode, event);}/*** 鍵盤監(jiān)聽接口*/private OnKeyBoardHideListener onKeyBoardHideListener;public void setOnKeyBoardHideListener(OnKeyBoardHideListener onKeyBoardHideListener) {this.onKeyBoardHideListener = onKeyBoardHideListener;}public interface OnKeyBoardHideListener {void onKeyHide();} }為什么重寫onKeyDown()方法,監(jiān)聽不到虛擬鍵的向下按鍵,而重寫EditTextView的onKeyPreIme可以監(jiān)聽到,這篇博客寫的很明白。
第二種方案
使用ViewTreeObserver.OnGlobalLayoutListener來監(jiān)聽整個布局的變化,但是有問題,點擊軟鍵盤的“向下”按鍵,不會回調(diào)這個函數(shù)。
View.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener(){//當鍵盤彈出隱藏的時候會 調(diào)用此方法。@Overridepublic void onGlobalLayout() {final Rect rect = new Rect();activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);final int screenHeight = activity.getWindow().getDecorView().getRootView().getHeight();final int heightDifference = screenHeight - rect.bottom;boolean visible = heightDifference > screenHeight / 3;if(visible){Log.i(TAG,"軟鍵盤顯示");}else {Log.i(TAG,"軟鍵盤隱藏");}} });第三種方案
將布局撐滿全屏,來監(jiān)聽onMeasure()的變化,這種方式可以生效,但是和業(yè)務耦合性太大,而且在全面屏的時候處理比較麻煩,就不貼代碼了
總結(jié)
基本上所有監(jiān)聽軟鍵盤的方式,都是通過上面的三種方式實現(xiàn)的。實現(xiàn)效果比較好是Android鍵盤面板沖突 布局閃動處理方案
所有的方式都是在本頁面上去彈起軟鍵盤,軟鍵盤是dialog,而且頁面的邏輯和復雜程度有各種情況,非常難以考慮。真是讓人抓耳撓腮呀。
打個響指,換種思路
沒有非要在一個頁面內(nèi)完成軟鍵盤的調(diào)度,我們可以另起爐灶,去實現(xiàn)這個功能。
使用DialogFragment來實現(xiàn)軟鍵盤功能
這種情況還是與業(yè)務有一定的關(guān)聯(lián),我不敢說這種方案能夠解決所有情況下的軟鍵盤使用問題,但是能解決很多情況下的使用。
先說一下我的使用場景,因為我是負責直播模塊的開發(fā),軟鍵盤的彈出和隱藏時的輸入部分ui是不相同。使用抖音的直播舉個例子
可以看到軟鍵盤在打開和關(guān)閉的時候是不同的ui,那么就可以使用DialogFragment來實現(xiàn)功能。我們最主要實現(xiàn)就是監(jiān)聽軟鍵盤的彈出和隱藏,彈出問題不大,這里最深的坑就是監(jiān)聽鍵盤的消失,先總結(jié)一下鍵盤消失的場景:
ps: 2和3,看上去好像是一樣的啊,大家肯定有一些疑惑,看圖說話
從張圖中可以清晰的看出來兩者的區(qū)別,其實android的原生鍵盤是沒有向下的按鈕的,各個第三方的輸入法自己實現(xiàn)的,andorid中沒有回調(diào)可以監(jiān)聽到這個事件(坑爹啊)。而且
ViewTreeObserver 監(jiān)聽不到點擊這個按鈕時的布局變化(坑爹啊!!!),我只是在mix2手機上測試的,其他的手機類型我不敢確保也是同樣的問題。
來來來,在做個小結(jié):
- 軟鍵盤的彈出時可以監(jiān)聽的
- 消失幾種情況中,除了軟鍵盤的向下按鍵其他都可以做到監(jiān)聽,或者可以拿到觸發(fā)的時機。
搜了一大圈,好像沒有找到比較好的解決方案,怎么辦呢?但是別人家的直播都是可以做到的呀,這時看到了View.getWindowVisibleDisplayFrame()方法,來看下官方的解釋
/*** Retrieve the overall visible display size in which the window this view is* attached to has been positioned in. This takes into account screen* decorations above the window, for both cases where the window itself* is being position inside of them or the window is being placed under* then and covered insets are used for the window to position its content* inside. In effect, this tells you the available area where content can* be placed and remain visible to users.** <p>This function requires an IPC back to the window manager to retrieve* the requested information, so should not be used in performance critical* code like drawing.** @param outRect Filled in with the visible display frame. If the view* is not attached to a window, this is simply the raw display size.*/public void getWindowVisibleDisplayFrame(Rect outRect) {if (mAttachInfo != null) {try {mAttachInfo.mSession.getDisplayFrame(mAttachInfo.mWindow, outRect);} catch (RemoteException e) {return;}// XXX This is really broken, and probably all needs to be done// in the window manager, and we need to know more about whether// we want the area behind or in front of the IME.final Rect insets = mAttachInfo.mVisibleInsets;outRect.left += insets.left;outRect.top += insets.top;outRect.right -= insets.right;outRect.bottom -= insets.bottom;return;}// The view is not attached to a display so we don't have a context.// Make a best guess about the display size.Display d = DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);d.getRectSize(outRect);}大概意思:這個api是用來獲取窗口可視區(qū)域大小的。該大小會收到系統(tǒng)狀態(tài)欄、軟鍵盤和虛擬按鍵的影響。在應用開發(fā)中可以利用該api來獲取狀態(tài)欄的高度,軟鍵盤的高度和虛擬按鍵的高度。
解決方案:
既然沒有回調(diào)能夠拿到虛擬鍵盤的向下操作,那么我們就輪詢監(jiān)聽窗口的大小,由于這個dialogFragment的生命周期只是在輸入的時候存在,那么就以為這這個輪詢時間也不會太長,100ms輪詢一次來監(jiān)聽窗口變化的大小,完美解決(如果還有其他優(yōu)雅的解決方案,請告訴我)。
用DialogFragment實現(xiàn)輸入?yún)^(qū)域的好處:
有了以上兩點考慮,我就開始動手寫代碼
Version 1
源代碼就不貼了,太長了,而且大部分和業(yè)務相關(guān),所有的業(yè)務邏輯和功能邏輯全部寫在DialogFragment中,看上去沒什么問題,如果突然有一天,另外的一個地方要做到類似的邏輯,又要重新寫一遍功能邏輯,完全不能復用呀,這樣的實現(xiàn)方案是不行的,打回去重做。嗯,需要將業(yè)務邏輯和功能邏輯分割開,這樣可以很大程度上的復用當前代碼。
Version 2
首先要定義一個接口,來定義此類功能的統(tǒng)一調(diào)用方式,所有要實現(xiàn)此功能的類都要實現(xiàn)這個接口
public interface IKeyBoard {// EditTextView需要繼承TextEditTextView,TextEditTextView是用來監(jiān)聽虛擬按鍵向下的操作TextEditTextView getEditTextView();// 根布局View getRoot();// dialogFragment 銷毀時調(diào)用,也就是隱藏時void onDismiss(); }自定義EditTextView
/*** 攔截鍵盤向下按鍵的 EditTextView*/ public class TextEditTextView extends DmtEditText {public TextEditTextView(Context context) {super(context);}public TextEditTextView(Context context, AttributeSet attrs) {super(context, attrs);}public TextEditTextView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overridepublic boolean onKeyPreIme(int keyCode, KeyEvent event) {if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == 1 && onKeyBoardHideListener != null) {onKeyBoardHideListener.onKeyHide();}return super.onKeyPreIme(keyCode, event);}/*** 鍵盤監(jiān)聽接口*/private OnKeyBoardHideListener onKeyBoardHideListener;public void setOnKeyBoardHideListener(OnKeyBoardHideListener onKeyBoardHideListener) {this.onKeyBoardHideListener = onKeyBoardHideListener;}public interface OnKeyBoardHideListener {void onKeyHide();} }最主要的類來了:
/*** 這個類時用來實現(xiàn)輸入框隨軟鍵盤彈出的情況,這個dialogFragment只負責彈起鍵盤的操作,不負責具體的ui顯示和邏輯,* 應該實現(xiàn)一個UI類來實現(xiàn)相應的顯示和邏輯部分。* <p>* 使用方法:* 1. 需要實現(xiàn){@link IKeyBoard}* 2. UI類需要包含{@link TextEditTextView}* 3. 如果UI需要監(jiān)聽聲明周期,需要實現(xiàn){@link LifecycleObserver}* <p>* ps:父類的Fragment或者Activity window的setSoftInputMode設置為{@link WindowManager} SOFT_INPUT_ADJUST_NOTHING** @author liyachao* @date 2018/4/17*/public class KeyBoardDialogFragment extends DialogFragment implements TextEditTextView.OnKeyBoardHideListener,WeakHandler.IHandler {private static final String TAG = "KeyBoardDialogFragment";private IKeyBoard mKeyBoardView;private TextEditTextView mTextEditTextView;private boolean softKeyBoardIsVisible;private Activity mActivity;private WeakHandler mHandler;private Rect mRect = new Rect();public static KeyBoardDialogFragment newInstance(IKeyBoard keyBoard) {KeyBoardDialogFragment fragment = new KeyBoardDialogFragment();Bundle args = new Bundle();fragment.setArguments(args);fragment.setKeyBoardView(keyBoard);return fragment;}/*** 安全檢查* @param keyBoardView 業(yè)務邏輯的view*/public void setKeyBoardView(IKeyBoard keyBoardView) {if (keyBoardView == null) {throw new RuntimeException("keyBoardView must not be null");} else if (keyBoardView.getEditTextView() == null) {throw new RuntimeException("keyBoardView must has EditTextView");} else if (keyBoardView.getRoot() == null) {throw new RuntimeException("keyBoardView must has root layout");}mKeyBoardView = keyBoardView;mTextEditTextView = keyBoardView.getEditTextView();}/*** 設置主題 input_dialog_style_large的具體設置如下* <style name="input_dialog_style_large" parent="@android:style/Theme.Dialog">* <item name="android:windowBackground">@color/transparent</item> //winndow 背景為透明色 * <item name="android:windowNoTitle">true</item> // 沒有title* <item name="android:backgroundDimEnabled">false</item> // 沒有默認的背景色* <item name="android:windowAnimationStyle">@style/keyboard_dialog_animation</item> //window動畫,可以不設置* </style>* 業(yè)務邏輯view,注冊DialogFragment聲明周期* */@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setStyle(STYLE_NO_TITLE, R.style.input_dialog_style_large);if (mKeyBoardView == null || mKeyBoardView.getRoot() == null) {dismiss();return;}if (mKeyBoardView.getRoot() instanceof LifecycleObserver) {getLifecycle().addObserver((LifecycleObserver) mKeyBoardView.getRoot());}mHandler = new WeakHandler(this);}@Overridepublic void onAttach(Context activity) {super.onAttach(activity);mActivity = (Activity) activity;}@Nullable@Overridepublic View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {return mKeyBoardView.getRoot();}@Overridepublic void onActivityCreated(Bundle savedInstanceState) {super.onActivityCreated(savedInstanceState);mTextEditTextView.setOnKeyBoardHideListener(this);initWindowParams();}/*** 設置window屬性*/public void initWindowParams() {Window window = getDialog().getWindow();if (window == null) {return;}WindowManager.LayoutParams lp = getDialog().getWindow().getAttributes();lp.dimAmount = 0;lp.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;lp.width = ViewGroup.LayoutParams.MATCH_PARENT;lp.gravity = Gravity.BOTTOM;window.setBackgroundDrawable(new ColorDrawable(0));window.setAttributes(lp);window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);}@Overridepublic Dialog onCreateDialog(Bundle savedInstanceState) {Dialog dialog = super.onCreateDialog(savedInstanceState);dialog.setCanceledOnTouchOutside(true);return dialog;}@Overridepublic void onResume() {super.onResume();mHandler.sendEmptyMessageDelayed(1, 100);}@Overridepublic void onStop() {super.onStop();dismissAllowingStateLoss();mHandler.removeMessages(1);}@Overridepublic void onDestroy() {super.onDestroy();}@Overridepublic void onDismiss(DialogInterface dialog) {super.onDismiss(dialog);mKeyBoardView.onDismiss();}@Overridepublic void onKeyHide() {dismiss();}public void onGlobalLayout() {Window window = getDialog().getWindow();if (window != null) {mRect.setEmpty();window.getDecorView().getWindowVisibleDisplayFrame(mRect);int screenHeight = UIUtils.getScreenHeight(getContext());int heightDifference = screenHeight - (mRect.bottom - mRect.top);if (heightDifference > screenHeight / 3) {Log.d(TAG, "鍵盤彈出");softKeyBoardIsVisible = true;} else {if (softKeyBoardIsVisible) {Log.d(TAG, "鍵盤隱藏");dismiss();softKeyBoardIsVisible = false;}}}}@Overridepublic void handleMsg(Message msg) {if (msg.what == 1) {onGlobalLayout();mHandler.sendEmptyMessageDelayed(1, 100);}} }具體的實現(xiàn)就是上面了,大部分做了注釋,也不需要解釋了,基本上可以解決大部分場景,可以根據(jù)自己具體的業(yè)務邏輯做一些改動。demo就不給大家了,上面基本上就可以了。
demo下載
總結(jié)
以上是生活随笔為你收集整理的优雅的监听软键盘隐藏的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大学在校 计算机考试,大学必考证书 |
- 下一篇: 大学期间能考的计算机证书,大学期间可以考