稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

安卓开发技术:监听软键盘的显示与隐藏

自从2007年乔帮主横空出世推出iPhone后,智能手机就都变成了触摸屏,且屏幕越来越大。输入自然也就通过软件来解决,现在来说绝大多数智能设备都是通过软键盘来做用户输入。在日常的开发过程中难免会跟软键盘打交道,比如强制显示或者强制隐藏等,也有些时候想要在键盘隐藏或者显示的时候做一点事情,这就需要准确的知道键盘的事件,这篇文章就来详细讨论一下Android下面如何做到监听键盘的事件。

硬键盘显示隐藏的事件监听

对于有硬键盘的设备,是会产生运行时的配置变更(Runtime Config Changes),因此可以通过Activity#onConfigurationChanged)回调来处理:

首先,要在AndroidManifest中给activity加上configChanges=“keyboardHidden”

然后,在代码中,处理:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Checks whether a hardware keyboard is available
    if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
        Toast.makeText(this, "keyboard visible", Toast.LENGTH_SHORT).show();
    } else if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "keyboard hidden", Toast.LENGTH_SHORT).show();
    }
}

需要注意的是,要用Configuration#hardKeyboardHidden而不是Configuration#keyboardHidden,因为只有当硬键盘状态变化时才会回调onConfigurationChanged。

软键盘显示隐藏事件监听

现在的Android设备很少才会有硬键盘,绝大多数都是软键盘,而SDK和API中却没有软键盘隐藏变化的相关事件,没有直接支持不代表做不到。我们通过其他的方式还是可以做到监听软键盘显示与隐藏状态变化的。

override onKeyPreIme

EditText获得焦点时,或者用户点击时,都会把软键盘弹起来(2.x版本长按MENU也可以强制弹出软键盘)。但是,隐藏软键盘一般都是BACK键,或者键盘自身提供隐藏的按扭,再有就是用代码强制隐藏。对于BACK键还是可以处理的,因为这属于事件(KeyEvent),是能监听到的。

核心原理

子例化EditText,并覆写方法onKeyPreIme)。这个方法能在输入法前面拦掉事件,从而可以做一些事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class KeyPreImeEditText extends EditText {
    public KeyPreImeEditText(Context context) {
        super(context);
    }

    public KeyPreImeEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public KeyPreImeEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            Log.e("keyboard", "onKeyPreIme we got back");
        }
        return super.onKeyPreIme(keyCode, event);
    }
}

优点

简单粗暴,都是SDK支持的方法和事件,所以不会有兼容性等蛋疼的问题。

缺点

这仅在输入法前拦截到BACK事件,而前面提到BACK仅是能让软键盘隐藏掉的一个方式而已,所以这个方法是不能够完全做到监听软键盘隐藏状态变化的。这个方法仅适用于想拦截BACK,做一些其他事情的场景。

override根布局的onMeasure

另外的思路就是观察软键盘引起的布局变化,比如软键盘弹起时Activity的整体布局都会发生变化。

核心原理

子例化Activity的根布局(比如LinearLayout或者RelativeLayout,然后覆写其onMeasure)方法,在其中判断View的当前高度与其本应有的高度,如果当前高度小于本应有的高度,则表明软键盘在:

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
public class KeyboardAwareLinearLayout extends LinearLayout {
    public KeyboardAwareLinearLayout(Context context) {
        super(context);
    }

    public KeyboardAwareLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public KeyboardAwareLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int proposedHeight = MeasureSpec.getSize(heightMeasureSpec);
        final int actualHeight = getHeight();

        if (actualHeight > proposedHeight) {
            Log.e("keyboard", "guess keyboard is shown");
        } else {
            Log.e("keyboard", "guess keyboard has been hidden");
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

优点

能够真正实现对软键盘隐藏和显示的监听,也不算复杂。

缺点

这个基于的原理是键盘对Activity的布局产生影响的情况,而这又受到其他条件控制。activity的属性windowSoftInputMode控制着软键盘与布局之间的影响关系,对于adjustPan以及全屏模式的adjustResize这种方法就失效了,因为这二种情况软键盘弹起时,布局是不会发生变化的,二种height值是一样的,自然无法分辨。

监听GlobalLayout

与上面的思路差不多,只不是监听GlobalLayout变化,然后根据布局高度与屏幕高度之差来判断。

注意:要记得把注册的GlobalLayoutListener再反注册掉。

计算根布局的高度差

判断的依据是根布局与DectorView之间的差值,在正常情况下应该等于status bar高度与action bar高度之和。当软键盘弹起时则会大于此值。

核心原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private boolean mKeyboardUp;

private void setListenerToRootView() {
        final View rootView = getWindow().getDecorView().findViewById(android.R.id.content);
        rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                final int headerHeight = getActionBarHeight() + getStatusBarHeight();
                int heightDiff = rootView.getRootView().getHeight() - rootView.getHeight();
                if (heightDiff > headerHeight) {
                    Log.e("keyboard", "keyboard is up");
                    if (!mKeyboardUp) {
                        mKeyboardUp = true;
                    }
                } else if (mOpen) {
                    Log.e("keyboard", "keyboard is hidden");
                    mKeyboardUp = false;
                }
            }
        });
    }

这段代码需要好好解释下:

  • android.R.id.content 通过这个id可以获得一个View的根布局,而不必要知道它具体的id。可以参考这个讨论
  • rootView 这个View是Activity的根布局,除去了actionbar的部分,是一个FrameLayout,注意这个并不是setContentView中设置的布局。rootView的第一个子View(rootView.getChildAt(0))就是setContentView()设置的布局。可以参考这个讨论
优点

不用子例化,不依赖于现有代码中的成员,可以直接插入到任何代码中。

缺点

本质上这跟上一个方法是一样的。因此对于adjustPan和全屏的adjustResize二种情况是无效的。针对这二种情况heightDiff不会变化。

计算根布局的的底部空隙

其实所有的方法都是为了发现软键盘对布局的影响,从而判断软键盘的显示和隐藏。还有一种方法就判断根布局的可视区域与屏幕底部的差值,如果这个差大于某个值,可以认定键盘弹起了。

核心原理
1
2
3
4
5
6
7
8
private boolean isKeyboardShown(View rootView) {
        final int softKeyboardHeight = 100;
        Rect r = new Rect();
        rootView.getWindowVisibleDisplayFrame(r);
        DisplayMetrics dm = rootView.getResources().getDisplayMetrics();
        int heightDiff = rootView.getBottom() - r.bottom;
        return heightDiff > softKeyboardHeight * dm.density;
}

得到的Rect就是根布局的可视区域,而rootView.bottom是其本应的底部坐标值,如果差值大于我们预设的值,就可以认定键盘弹起了。这个预设值是键盘的高度的最小值。这个rootView实际上就是DectorView,通过任意一个View再getRootView就能获得。

优点

适用所有情况,包括adjustPan和全屏的adjustResize也能准确判断出来。

结论

如果真的需要监听软键盘显示与隐藏的事件就可以通过上面的提到的最后一种方式来实现,简单方便且可靠,唯一要注意的就是要反注册掉所注册的GlobalLayoutListener.

参考资料

Comments