Android软键盘

Catalogue
  1. 1 操作软键盘
    1. 1.1 显示软键盘
    2. 1.2 隐藏软键盘
    3. 1.3 切换键盘的弹出和隐藏
  2. 2 监听键盘的弹起与隐藏
  3. 3 如何避免键盘遮挡输入框
    1. 3.1 方法一:windowSoftInputMode:adjustResize和adjustPan
    2. 3.2 方法二:当键盘弹起时,让界面整体上移;键盘收起,让界面整体下移
      1. 3.2.1 主要实现步骤
      2. 3.2.2 实现原理
  4. 参考资料

说起Android软键盘,这里遇到的坑多数就是对它理解不够全面导致的,所以决定好好总结一下它。在避免自己二次掉坑的同时,如果能帮助到在这个知识区正疑惑的你,那就太好了。本篇的讲解将分以下几个主题:
1、操作软键盘
2、监听键盘的弹起与隐藏
3、如何避免键盘遮挡输入框

1 操作软键盘

想要操作软键盘,需要使用到 InputMethodManager ,它是一个系统服务,可以使用 Context.getSystemService() 获取到它。

1.1 显示软键盘

在 InputMethodManager中,有两个方法showSoftInput()和showSoftInputFromInputMethod(),而实际上,只有showSoftInput()是有效的。

这里我们只需要传递两个参数。它首先需要一个View ,使用软键盘就是为了输入,而输入就需要有接收输入内容的View ,这里接收输入的View ,最好是一个EditText(但这不是必须的)。
而第二个参数flags就是个标志位,从上面截图的方法签名上的文档上可以看到,它接收0或者SHOW_INPYT_IMPLICIT 两个参数,但是实际上,它有第三个参数,另外一个是SHOW_FORCED。一般没有特别需要的话,我们直接传递0就好了(实际上SHOW_INPYT_IMPLICIT、SHOW_FORCED并不影响显示,只是在隐藏的时候,会有一些限制。而且我们一般的话,不管显示或者隐藏都将flag设置为0,特殊情况遇到再做分析)。
现在,简单总结一下调用showSoftInput()会生效的关键点:
1、第一个参数,最好是EditText或者它的子类。
考虑到软键盘就是为了输入,EditText就是一个接收输入的控件。而这不是绝对的,如果不是一个EditText ,就必须要求这个View有两个属性,分别是:android:focusable=”true” 和android:focusableInTouchMode=”true”。
2、第一个参数,必须是可获取焦点的,并且当前已经获取到焦点。
EditText默认是允许获取焦点的,但是假如布局中,存在多个可获取焦点的控件,就需要提前让我们传递进去的View获取到焦点。获取焦点可以使用requestFocus()方法。
3、布局必须加载完成。
在onCreate()中,如果立即调用showSoftInput()是不会生效的。想要在页面一启动的时候就弹出键盘,可以在Activity上,设置 android:windowSoftInputMode属性来完成,或者做一个延迟加载,View.postDelayed()也是一个解决方案。
所以最终,完整的显示软键盘的代码就如下所示了。

1.2 隐藏软键盘

虽然showSoftInput()方法是有效的,但是想要隐藏软键盘,就没有提供对应的hideSoftInput()方法,但是却有一个hideSoftInputFromWindow()方法,可以用来隐藏软键盘。
先来看看这个方法的签名,它同样有两个方法可以调用。

它接收两个参数,第一个参数是一个IBinder ,可以直接传递一个 View.getWindowToken()的windowToken对象就可以了。而第二个参数,就是隐藏软键盘的标志位,如果没有特殊要求的话,直接传递0就好了。

注意:这里虽然原则上需要传递一个之前弹出键盘传递的时候,传递的View的windowToken,但是实际情况是你只需要传递一个存在于当前布局ViewTree中,随意一个View的windowToken就可以了。

最终隐藏软件的代码就是这样的。

1.3 切换键盘的弹出和隐藏

在InputMethodManager中,还提供了一个toggleSoftInput()方法,如同它的名字一样,它可以让软键盘在显示和隐藏之间切换。

该方法,接收两个flags ,分别是控制show和hide时候的标识,它们的含义和前面介绍的showSoftInput()和hideSoftInputFromWindow()一致,所以没有特殊要求,直接传递0就好了。
toggleSoftInput()方法不要求传递一个View或者windowToken,所以它并没有showSoftInput()中的一些限制,但是依然还有需要在布局绘制完成之后调用才会有效果。

虽然这个方法,限制很少,但是我们基本上不会使用它。主要原因在于,它是一个开关的方法,会根据当前的状态做相反的操作。这就导致很多时候,我们在代码中,无法直接根据InputMethodManager提供的方法判断当前软键盘的显示状态,这样也就无法确定调用它的时候的效果了。

2 监听键盘的弹起与隐藏

如果想要监听键盘的弹出和收起,可以使用ViewTreeObserver.OnGlobalLayoutListener这个监听,来监听布局的调整,从而判断出键盘的弹出和隐藏。下面是一个使用ViewTreeObserver.OnGlobalLayoutListener后获取屏幕中各个类型高度的例子,请注意代码中的注释:

1
2
3
4
5
6
7
8
9
10
11
12
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Log.d("sss_main", "root.getHeight(): " + root.getHeight());//当前所引用视图的高度
Log.d("sss_main", "root.getRootView().getHeight(): " + root.getRootView().getHeight());//屏幕高度
Log.d("sss_main", "getWindow().getDecorView(): " + getWindow().getDecorView().getHeight());//屏幕高度
Rect r = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(r);
Log.d("sss_main", "r.top: " + r.top);//状态栏高度
Log.d("sss_main", "r.bottom-r.top: " + (r.bottom - r.top));//用户界面的可视高度(除去状态栏和底部导航)
}
});

代码中r.bottom - r.top代表了除去状态栏、底部导航栏后,手机屏幕剩余可视高度。注意,这个剩余可视高度也不包括输入法弹窗的高度(如果屏幕中有输入法弹窗的话)。有了以上的基础,我们就可以通过getWindow().getDecorView().getHeight()-r.bottom>100?键盘弹出:键盘隐藏来判断键盘是否隐藏了。

注意:为什么要大于100呢?因为如果手机具备底部导航区域的话,要排除这个值的干扰。当然了,直接默认底部导航区域高度为100肯定是不精确的,实际应用中我们应该获取到真实的底部导航区域高度,具体如何获取底部导航区域高度可以参考这篇文章

3 如何避免键盘遮挡输入框

在开发中,经常会遇到键盘挡住输入框的情况,比如登录界面或注册界面,弹出的软键盘把登录或注册按钮挡住了,用户必须把软键盘收起,才能点击相应按钮,这样的用户体验非常不好。下面对几种在开发中常用的避免键盘遮挡输入框的方法进行总结。

3.1 方法一:windowSoftInputMode:adjustResize和adjustPan

主要实现方法:在 AndroidManifest.xml 对应的Activity里添加 android:windowSoftInputMode=”adjustPan” 或是 android:windowSoftInputMode=”adjustResize”属性
这两种属性的区别,官方的解释是:

这两个属性作用都是为了调整界面使键盘不挡住输入框,这里对这两种属性使用场景、优缺点、注意事项进行了全方面总结。对于fitsystemwindows还比较懵逼的你,可以参考这篇文章

最后附一张官方的截图。

插播知识:
这里再提一下clipToPadding和clipChildren这两个属性吧,虽然和本篇没有关系但是同样是有一定概率出现的,所以要讲一下。clipToPadding就是说控件的绘制区域是否在padding里面的,true的情况下如果你设置了padding那么绘制的区域就往里缩,clipToPadding主要应用在listview头部和尾部设置padding后希望可以在滑动item时能滑动到padding区域。clipChildren表示是否限制子View在所属父View的范围内,我们将其值设置为false后那么当子控件的高度高于父控件时也会完全显示,而不会被压缩。注意clipChildren一定是在布局文件的根节点设置,否则不起作用,具体的使用请看这篇文章以及这篇文章注意:有时候clipChildren会不起作用,如果确定考虑到了之前两篇文章所提及的内容,则应该想其它的办法替代clipChildren,这可以认为是clipChildren的BUG。

3.2 方法二:当键盘弹起时,让界面整体上移;键盘收起,让界面整体下移

使用场景:针对界面全屏模式,输入框不会被键盘遮挡。主要用于一些登录界面,或是需要把界面整体都顶上去的场景。

3.2.1 主要实现步骤

(1). 获取Activity布局xml的最外层控件,如xml文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main"
tools:context="com.example.liubin1.softkeyboardhelper.MainActivity">

<EditText
android:id="@+id/name"
android:hint="请输入用户名:"
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:layout_height="50dp"
/>
<EditText
android:id="@+id/pas"
android:layout_below="@id/name"
android:hint="请输入密码:"
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:layout_height="50dp"
/>
<Button
android:id="@+id/login_btn"
android:layout_below="@id/rpas"
android:layout_centerHorizontal="true"
android:text="登录"
android:layout_width="180dp"
android:layout_height="50dp" />
</RelativeLayout>

先获取到最外层控件:
RelativeLayout main = (RelativeLayout) findViewById(R.id.main);
(2). 获取到最后一个控件,如上面的xml文件,最后一个控件是Button:
Button login_btn = (Button) findViewById(R.id.login_btn);
(3). 给最外层控件和最后一个控件添加监听事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//在Activity的onCreate里添加如下方法
addLayoutListener(main,login_btn);
/**
* addLayoutListener方法如下
* @param main 根布局
* @param scroll 需要显示的最下方View
*/
public void addLayoutListener(final View main, final View scroll) {
main.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Rect rect = new Rect();
//1、获取main在窗体的可视区域
main.getWindowVisibleDisplayFrame(rect);
//2、获取main在窗体的不可视区域高度,在键盘没有弹起时,main.getRootView().getHeight()调节度应该和rect.bottom高度一样
int mainInvisibleHeight = main.getRootView().getHeight() - rect.bottom;
int screenHeight = main.getRootView().getHeight();//屏幕高度
//3、不可见区域大于屏幕本身高度的1/4:说明键盘弹起了
if (mainInvisibleHeight > screenHeight / 4) {
int[] location = new int[2];
scroll.getLocationInWindow(location);
// 4、获取Scroll的窗体坐标,算出main需要滚动的高度
int srollHeight = (location[1] + scroll.getHeight()) - rect.bottom;
//5、让界面整体上移键盘的高度
main.scrollTo(0, srollHeight);
} else {
//3、不可见区域小于屏幕高度1/4时,说明键盘隐藏了,把界面下移,移回到原有高度
main.scrollTo(0, 0);
}
}
});
}
}

3.2.2 实现原理

此方法通过监听 Activity 最外层布局控件来检测软键盘是否弹出,然后去手动调用控件的scrollTo方法达到调整布局目的。


参考资料

  1. Android软键盘的显示与隐藏
  2. Android各种键盘挡住输入框解决办法
  3. API指南
  4. stackoverflow:监听软件的弹出与隐藏