Android状态栏和软键盘配合的填坑之路
adjustResize,键盘弹起时,将界面Layout高度压缩,留出空间显示软键盘。
adjustPan,需要存在滚动控件,键盘弹起时,滚动列表,留出控件显示软键盘,如果没有滚动控件,则将全部控件上移,而不会压缩界面Layout。
package="com.hule.dashi.live">android:name=".AudioLiveRoomActivity"android:windowSoftInputMode="adjustResize" />
遇到的问题
搜索了一番,结果发现是谷歌在2.2年代就留下的坑,却一直没修复...那么我们是不是可以监听软键盘弹起事件,获取软键盘高度,手动压缩布局Layout高度不就可以了?
结果Android并没有提供软键盘弹起的监听事件,擦,这么坑的?那么还有什么办法么?
办法还是有的
结果又发现问题,在刘海屏上有问题,底部会空出一大片,而我修改后的做法是:计算出键盘高度后,给布局底部控件设置一个MarginBottom,自然会有一种顶出输入框的效果。
源码解析
首先我们传入rootView构造KeyboardFix。
给rootView设置addOnGlobalLayoutListener回调。
addOnGlobalLayoutListener的onGlobalLayout回调时,就是键盘弹起和下降。
possiblyResizeChildOfContent(),computeUsableHeight()计算可见高度。
可见高度小于原始高度一定阀值,则当做为键盘弹起。否则为下降软键盘。
最后回调监听者。
public class KeyboardFix implements ViewTreeObserver.OnGlobalLayoutListener {/*** 父容器*/private View vRootView;/*** 父容器的高度*/private int mContentHeight;/*** 标记,第一次回调时保存父容器的高度*/private boolean isFirst = true;/*** 最后一次显示父容器的高度,用于判断是否显示虚拟键盘*/private int mLastShowHeight;//1.首先我们传入rootView构造KeyboardFix。public KeyboardFix(View rootView) {if (rootView != null) {//2. 给rootView设置addOnGlobalLayoutListener回调。rootView.getViewTreeObserver().addOnGlobalLayoutListener(this);}}@Overridepublic void onGlobalLayout() {//3. addOnGlobalLayoutListener的onGlobalLayout回调时,就是键盘弹起和下降。possiblyResizeChildOfContent();}/*** 重新调整跟布局的高度*/private void possiblyResizeChildOfContent() {//4. possiblyResizeChildOfContent(),computeUsableHeight()计算可见高度。//计算内容的可见高度int usableHeightNow = computeUsableHeight();if (isFirst) {//兼容华为等机型mContentHeight = usableHeightNow;isFirst = false;}//没有改变,忽略if (usableHeightNow == mLastShowHeight) {return;}mLastShowHeight = usableHeightNow;//5. 可见高度小于原始高度一定阀值,则当做为键盘弹起。否则为下降软键盘。boolean isShowKeyboard = usableHeightNow + 200 < mContentHeight;//6. 最后回调监听者。for (OnKeyboardChangeCallback listener : mOnKeyboardChangeCallbacks) {listener.onChange(isShowKeyboard, mContentHeight, usableHeightNow);}}/*** 计算内容的可见高度** @return 父容器的可见高度*/private int computeUsableHeight() {Rect rect = new Rect();vRootView.getWindowVisibleDisplayFrame(rect);return (rect.bottom - rect.top);}/*** 虚拟键盘显示隐藏的监听*/public interface OnKeyboardChangeCallback {/*** 虚拟键盘显示发生变化时调用** @param isVisible true为可见,false为不可见* @param contentHeight 原始内容高度* @param usableHeight 当前可用高度*/void onChange(boolean isVisible, int contentHeight, int usableHeight);/*** 界面暂时回调*/void onPause();/*** 界面销毁时回调*/void onDestroy();}}
拓展KeyboardFix,处理输入框弹起
例如我们给输入框设置一个父容器,点击输入框弹起软键盘,这时我们的键盘回调会回调,计算软键盘高度,我们将软键盘高度作为输入框父容器的MarginBottom,就形成了输入框被软键盘弹起的情况。
由于这种情况很通用,所以可以抽取为一个通用的Callback。同时为Callback提供生命周期回调。
/*** 回调空实现*/public static class OnKeyboardChangeAdapter implements OnKeyboardChangeCallback {@Overridepublic void onChange(boolean isVisible, int contentHeight, int usableHeight) {}@Overridepublic void onPause() {}@Overridepublic void onDestroy() {}}/*** 存在输入框场景的监听,键盘弹起时,设置输入框距离底部一个键盘高度,键盘下降时取消距离*/public static class CallbackToInput extends OnKeyboardChangeAdapter {/*** 输入框容器*/private View vInputContainer;public CallbackToInput(View inputContainer) {vInputContainer = inputContainer;}@Overridepublic void onChange(boolean isVisible, int contentHeight, int usableHeight) {super.onChange(isVisible, contentHeight, usableHeight);if (isVisible) {showInputView(usableHeight, contentHeight);} else {setBottomMargin(0);}}@Overridepublic void onPause() {super.onPause();//界面暂停时,下降输入框setBottomMargin(0);}/*** 显示虚拟键盘时调用,显示输入框,如果绑定了列表控件则滚动到后一个item** @param height 用于计算虚拟键盘的高度*/private void showInputView(int height, int contentHeight) {if (vInputContainer == null) {return;}//虚拟键盘的高度int keyboardHeight = contentHeight - height;setBottomMargin(keyboardHeight);}/*** 重新设置inputContainer的底部边距*/private void setBottomMargin(int bottomMargin) {if (vInputContainer == null) {return;}ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) vInputContainer.getLayoutParams();if (params.bottomMargin != bottomMargin) {params.bottomMargin = bottomMargin;vInputContainer.requestLayout();}}}
使用监听
mKeyboardFix.addOnKeyboardChangeListener(new KeyboardFix.CallbackToInput(inputContainer));拓展KeyboardFix,键盘弹起,自动滚动RecyclerView
/*** 存在列表场景的监听,一般键盘弹起时,列表需要滚动到底部,就可以添加该类型监听*/public static class CallbackToList extends OnKeyboardChangeAdapter {private RecyclerView vRecyclerView;private boolean mIsReverse;/*** 是否反转,反转则滚动到第0位,非反转则滚动到列表的最后1位** @param isReverse true为反转*/public CallbackToList(RecyclerView recyclerView, boolean isReverse) {vRecyclerView = recyclerView;mIsReverse = isReverse;}@Overridepublic void onChange(boolean isVisible, int contentHeight, int usableHeight) {super.onChange(isVisible, contentHeight, usableHeight);//键盘弹起时滚动到底部if (isVisible) {int position;if (mIsReverse) {position = 0;} else {if (vRecyclerView.getAdapter() == null) {return;}position = vRecyclerView.getAdapter().getItemCount() - 1;}if (vRecyclerView != null && vRecyclerView.getAdapter() != null) {vRecyclerView.scrollToPosition(position);}}}}
使用监听
mKeyboardFix.addOnKeyboardChangeListener(new KeyboardFix.CallbackToList(recyclerView, false));分发生命周期事件
@Overrideprotected void onPause() {super.onPause;if (mKeyboardFix != null) {mKeyboardFix.onPause();}}@Overrideprotected void onDestroy() {super.onDestroy();if (mKeyboardFix != null) {mKeyboardFix.onDestroy();}}
完整代码
public class KeyboardFix implements ViewTreeObserver.OnGlobalLayoutListener {/*** 父容器*/private View vRootView;/*** 父容器的高度*/private int mContentHeight;/*** 标记,第一次回调时保存父容器的高度*/private boolean isFirst = true;/*** 最后一次显示父容器的高度,用于判断是否显示虚拟键盘*/private int mLastShowHeight;/*** 键盘监听*/private ListmOnKeyboardChangeCallbacks; /*** 根容器*/public KeyboardFix(View rootView) {mOnKeyboardChangeCallbacks = new CopyOnWriteArrayList<>();vRootView = rootView;if (rootView != null) {rootView.getViewTreeObserver().addOnGlobalLayoutListener(this);}}@Overridepublic void onGlobalLayout() {possiblyResizeChildOfContent();}public void addOnKeyboardChangeListener(OnKeyboardChangeCallback onKeyboardChangeCallback) {mOnKeyboardChangeCallbacks.add(onKeyboardChangeCallback);}/*** 重新调整跟布局的高度*/private void possiblyResizeChildOfContent() {//计算内容的可见高度int usableHeightNow = computeUsableHeight();if (isFirst) {//兼容华为等机型mContentHeight = usableHeightNow;isFirst = false;}//没有改变,忽略if (usableHeightNow == mLastShowHeight) {return;}mLastShowHeight = usableHeightNow;//判断是否弹起软键盘boolean isShowKeyboard = usableHeightNow + 200 < mContentHeight;for (OnKeyboardChangeCallback listener : mOnKeyboardChangeCallbacks) {listener.onChange(isShowKeyboard, mContentHeight, usableHeightNow);}}/*** 计算内容的可见高度** @return 父容器的可见高度*/private int computeUsableHeight() {Rect rect = new Rect();vRootView.getWindowVisibleDisplayFrame(rect);return (rect.bottom - rect.top);}/*** 界面暂时时调用*/public void onPause() {if (mOnKeyboardChangeCallbacks != null) {for (OnKeyboardChangeCallback callback : mOnKeyboardChangeCallbacks) {callback.onPause();}}}/*** 界面销毁时调用,取消注册,防止内存泄漏*/public void onDestroy() {if (vRootView != null) {vRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);vRootView = null;}if (mOnKeyboardChangeCallbacks != null) {for (OnKeyboardChangeCallback callback : mOnKeyboardChangeCallbacks) {callback.onDestroy();}mOnKeyboardChangeCallbacks.clear();mOnKeyboardChangeCallbacks = null;}}/*** 虚拟键盘显示隐藏的监听*/public interface OnKeyboardChangeCallback {/*** 虚拟键盘显示发生变化时调用** @param isVisible true为可见,false为不可见* @param contentHeight 原始内容高度* @param usableHeight 当前可用高度*/void onChange(boolean isVisible, int contentHeight, int usableHeight);/*** 界面暂时回调*/void onPause();/*** 界面销毁时回调*/void onDestroy();}/*** 回调空实现*/public static class OnKeyboardChangeAdapter implements OnKeyboardChangeCallback {@Overridepublic void onChange(boolean isVisible, int contentHeight, int usableHeight) {}@Overridepublic void onPause() {}@Overridepublic void onDestroy() {}}/*** 显示软键盘*/public void showSoftInput(final View view) {InputMethodManager manager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);if (manager == null) {return;}view.setFocusable(true);view.setFocusableInTouchMode(true);view.requestFocus();manager.showSoftInput(view, InputMethodManager.SHOW_FORCED);}/*** 存在输入框场景的监听,键盘弹起时,设置输入框距离底部一个键盘高度,键盘下降时取消距离*/public static class CallbackToInput extends OnKeyboardChangeAdapter {/*** 输入框容器*/private View vInputContainer;public CallbackToInput(View inputContainer) {vInputContainer = inputContainer;}@Overridepublic void onChange(boolean isVisible, int contentHeight, int usableHeight) {super.onChange(isVisible, contentHeight, usableHeight);if (isVisible) {showInputView(usableHeight, contentHeight);} else {setBottomMargin(0);}}@Overridepublic void onPause() {super.onPause();//界面暂停时,下降输入框setBottomMargin(0);}/*** 显示虚拟键盘时调用,显示输入框,如果绑定了列表控件则滚动到后一个item** @param height 用于计算虚拟键盘的高度*/private void showInputView(int height, int contentHeight) {if (vInputContainer == null) {return;}//虚拟键盘的高度int keyboardHeight = contentHeight - height;setBottomMargin(keyboardHeight);}/*** 重新设置inputContainer的底部边距*/private void setBottomMargin(int bottomMargin) {if (vInputContainer == null) {return;}ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) vInputContainer.getLayoutParams();if (params.bottomMargin != bottomMargin) {params.bottomMargin = bottomMargin;vInputContainer.requestLayout();}}}/*** 存在列表场景的监听,一般键盘弹起时,列表需要滚动到底部,就可以添加该类型监听*/public static class CallbackToList extends OnKeyboardChangeAdapter {private RecyclerView vRecyclerView;private boolean mIsReverse;/*** 是否反转,反转则滚动到第0位,非反转则滚动到列表的最后1位** @param isReverse true为反转*/public CallbackToList(RecyclerView recyclerView, boolean isReverse) {vRecyclerView = recyclerView;mIsReverse = isReverse;}@Overridepublic void onChange(boolean isVisible, int contentHeight, int usableHeight) {super.onChange(isVisible, contentHeight, usableHeight);//键盘弹起时滚动到底部if (isVisible) {int position;if (mIsReverse) {position = 0;} else {if (vRecyclerView.getAdapter() == null) {return;}position = vRecyclerView.getAdapter().getItemCount() - 1;}if (vRecyclerView != null && vRecyclerView.getAdapter() != null) {vRecyclerView.scrollToPosition(position);}}}}}
总结
虽然谷歌没有提供键盘监听,但是我们可以曲线救国,当然希望官方可以修复这个bug,并且提供回调为最好,本篇作为记录而写。
