Android 软键盘输入法问题

做过 Android 软键盘的或多或少都会踩的坑,adjustResize 和 adjusPan 有什么区别,为什么 adjustPan 会把当前页面顶出去?全屏模式下 adjustResize 怎么也顶出去了,如何适配不让当前页面顶出屏幕?

1、软键盘介绍

系统软键盘是由 InputMethodService 管理,它为软键盘创建了一个继承 DiaologSoftInputWindow ,设置显示在底部:

1
2
mWindow = new SoftInputWindow(this, "InputMethod", mTheme, null, null, mDispatcherState,
WindowManager.LayoutParams.TYPE_INPUT_METHOD, Gravity.BOTTOM, false);

当点击输入框时,系统调整当前主窗口来显示底部的软键盘区域。那系统是根据什么策略来调整当前主窗口的空间来显示底部软键盘的呢?

系统对外提供了 windowSoftInputMode 字段,来控制主窗口为软键盘腾空间的策略,其属性有:

属性 含义 优缺点
adjustResize 调整Activity主窗口的尺寸来为屏幕上的软键盘腾出空间 优点:1、不会把标题栏顶出当前可见布局;2、有多项输入时,当前输入框下面的输入框可上下滑动输入. 缺点:1.需要界面本身可调整尺寸:要么使用滑动组件适配,要么自己在onLayout适配调整;2. 全屏时失效
adjustPan 自动平移窗口的内容,使当前焦点永远不被键盘遮盖,让用户始终都能看到其输入的内容 优点:不会失效,界面整体往上平移. 缺点:1、会把标题栏顶出当前可见布局;2、有多项输入时,当前输入框下面的输入框无法输入,必须收起键盘显示输入框再输入
adjustUnspecified 根据窗口是否存在任何可滚动的布局视图,来选择adjustResize或者adjustPan 在不同模式下优缺点如上俩种
adjustNoting 软键盘弹出时,主窗口Activity不会做出任何响应 主窗口不调整适配

adjust 系列的适配模式可用如下图来表达主窗口的调整适配:

软键盘windowsoftinput

2、软键盘遮挡及滑动问题

对于非全屏,我们使用 adjustResize 就能适配一般情况下的软键盘弹出场景,但对于以下情况还需要额外进行适配:

  • 非全屏模式下,软键盘弹出时遮挡住了需要显示的非输入焦点区域
  • 全屏模式下,adjustResize 无效,同时不想顶部标题栏被顶上去不见

那适配方式一般有如下俩种解决方案:

1、解决方案一:adjustResize

非全屏使用 adjustResize ,系统会自动调整主窗口的布局,所以可以有多种方式来适配:

  • 嵌套滚动布局,对于局部可以滚动适配的,可以把输入框放在 ScrollView
  • 监听主窗口的 onLayoutChange 变化,在软键盘弹出时平移被遮挡的显示区域

例如在面板根布局下监听 onLayoutChange ,获取弹出前后的位置,再添加被遮挡区域的高度进行绘制。

1
2
3
4
5
panelView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
}
});

对于卡噗的全屏模式来说,adjustResize失效 ,系统仍然会平移当前窗口把标题栏顶上去,而不会让主窗口调整。

软键盘异常

所以需要使用方案二的 onGlobalLayoutListener 全局监听来解决标题栏被顶出去的问题。

2、解决方案二:onGlobalLayoutListener

OnGlobalLayoutListenerViewTree 的全局布局状态或可见性发生变化时的监听,我们可以在软键盘弹出/消失触发主窗口的全局布局变化监听里,改变主窗口不把标题栏顶出去。

不把顶部标题栏顶出去,就是要重新设置主窗口的布局,有以下几种方式可以做到:

  • scrollTo:平移 rootView ,平移高度 = 输入法高度
  • layout:改变 rootViewheight,使 view 的高度 = 屏幕高度 - 输入法高度
  • padding:改变 rootViewpadding ,使底部边距 = 输入法高度

经过每种方法验证,最终发现是 padding 方案最适合。所以这里通过设置 rootView 的底部间距,来调整主窗口正好在输入法窗口之上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private ViewTreeObserver.OnGlobalLayoutListener mRootViewListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Rect rect = new Rect();
mRootView.getWindowVisibleDisplayFrame(rect);
int diff = mHeight - rect.bottom; //软键盘高度 = 屏幕高度-主窗口可见高度
int displayHeight = rect.bottom - rect.top; //主窗口可见高度
boolean visible = (double) displayHeight / mHeight < 0.8; //非主窗口高度比较大,视为软键盘可见
if (visible != isVisiableForLast || visible && diff != mLastDiff) {
if (mRootView.getPaddingBottom() != diff) {
if (visible) {
mSoftInputListener.softInputPop(true);
updateContentPadding(diff);
} else {
mSoftInputListener.softInputPop(false);
updateContentPadding(0);
}
}
mLastDiff = diff;
isVisiableForLast = visible;
}
}
};

适配情况如下所示:

软键盘正常

3、其他问题

1、部分机型兼容性问题,无 onGlobalLayoutListener 回调

在使用 onGlobalLayoutListener 方法时,部分低端机型上没有回调,导致标题栏仍然被顶上去。尝试的解决方案有:

  • 手动消除 EditText 的焦点,在键盘收起时, EditText 重获焦点后,触发 onGlobalLayoutListener 回调。但带来的问题可能是:回调在主窗口已被顶上去之后,此时再调整时下来窗口会闪动。

关于更多可能尝试的解决方案的,可以参考:https://stackoverflow.com/questions/7417123/android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible

2、 onGlobalLayoutListener 无法释放

FragmentonDestroyonGlobalLayoutListener 移除时无效,退出后仍然能收到监听。这里应该是在FragmentonDetach 后移除的无效,建议在 FragmentonDestroyView 中移除。

知道是不会有人点的,但万一被感动了呢?