Magic Room

Android 软键盘的研究

问题重现

  • 当点击界面上的EditText的时候,整个界面被顶了上去,见下图
  • 在视频播放界面,点击评论弹出软键盘后,按Back键返回,软键盘不消失(已经做了隐藏键盘操作)

涉及知识点

  • Activity的 windowSoftInputMode属性的研究
  • InputMethodManager的隐藏和显示软键盘的正确用法

windowSoftInputMode

在Activity的属性中可以声明 10 种值如下:

  • stateHidden
  • stateVisible
  • stateUnspecified
  • stateUnchanged
  • stateAlwaysHidden
  • stateAlwaysVisible
  • adjustPan
  • adjustResize
  • adjustUnspecified
  • adjustNothing

前面6种 state开头的属性 控制的是软键盘显示与隐藏的模式,而后4种 控制的是软键盘窗口与Activity窗口的交互模式
这些模式的详细介绍可以参考文末参考链接,这里主要说明与上述问题相关的属性adjustResizeadjustPan

Google官方对两个Flag的解释如下:

WindowManager.java

    /** Adjustment option for {@link #softInputMode}: set to allow the
     * window to be resized when an input
     * method is shown, so that its contents are not covered by the input
     * method.  This can <em>not</em> be combined with
     * {@link #SOFT_INPUT_ADJUST_PAN}; if
     * neither of these are set, then the system will try to pick one or
     * the other depending on the contents of the window. If the window's
     * layout parameter flags include {@link #FLAG_FULLSCREEN}, this
     * value for {@link #softInputMode} will be ignored; the window will
     * not resize, but will stay fullscreen.
     */
    public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;

    /** Adjustment option for {@link #softInputMode}: set to have a window
     * pan when an input method is
     * shown, so it doesn't need to deal with resizing but just panned
     * by the framework to ensure the current input focus is visible.  This
     * can <em>not</em> be combined with {@link #SOFT_INPUT_ADJUST_RESIZE}; if
     * neither of these are set, then the system will try to pick one or
     * the other depending on the contents of the window.
     */
    public static final int SOFT_INPUT_ADJUST_PAN = 0x20;

设置这些模式有两个入口,一个是在xml 中注册Activity时,windowSoftInputMode属性控制的,也可以通过代码getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)来更改Flag

adjustPan是一种平移模式,即当软键盘弹出时,不会对当前窗口进行布局调整,当输入框要被遮挡时,窗口就会进行平移。我们在日常开发中,很多情况EditText输入框是在布局底部的,例如聊天界面和类似上面图片的情况,如果界面被顶上去是很丑的,影响用户体验。所以在这类情况下是不能使用此模式的。

adjustResize是一种压缩模式,即当软键盘弹出时,要对当前窗口调整屏幕的大小以便留出软键盘的空间

在Dialog主题下的Activity中,即前两张图可以很明显看出Activity的窗口被压缩了,但如果设置为全屏主题的Activity,如图3,发现界面还是会被顶起来,没有效果…

调整社区主Activity GsdSdkMainActivity的windowSoftInputMode属性为adjustResize,弹出键盘后的效果如下:

虽然不会平移了,但发现输入框却完全被挡住了…. 查找资料后发现针对这种情况,要手动监听布局变化去做一些调整,刚好模式为adjustResize会调整界面布局。

作法一:
重写根布局的ViewGroup,一般为继承LinearLayout或者RelativeLayout,然后重写onSizeChanged方法,当界面变化的时候会去调用onSizeChanged,我们可以写一个回调去通知界面做一些需求上的调整。

public class ResizeLayout extends RelativeLayout {

    private static final String TAG = MainActivity.class.getSimpleName();

    public ResizeLayout(Context context) {
        super(context);
    }

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

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.e(TAG, "onSizeChanged! w=" + w + ",h=" + h + ",oldw=" + oldw + ",oldh=" + oldh);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.e(TAG, "onMeasure: ");
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        Log.e(TAG, "onLayout  l=" + l + ", t=" + t + ",r=" + r + ",b=" + b);
    }
}             

但经过测试发现,当设置为全屏模式时,并不会去调用onSizeChanged方法,只会进行onMeasure和onLayout

/com.idreamsky.softinput E/MainActivity: onMeasure:                   
/com.idreamsky.softinput E/MainActivity: onLayout  l=0, t=0,r=1080,b=1920                     
/com.idreamsky.softinput E/MainActivity: onMeasure:                            
/com.idreamsky.softinput E/MainActivity: onLayout  l=0, t=0,r=1080,b=1920          
/com.idreamsky.softinput E/MainActivity: onMeasure:                    
/com.idreamsky.softinput E/MainActivity: onLayout  l=0, t=0,r=1080,b=1920           

非全屏模式下,例如上面的Dialog风格Activity,在弹出和隐藏键盘时,Log日志如下:

/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onSizeChanged! w=884,h=788,oldw=884,oldh=1626
/com.idreamsky.softinput E/MainActivity: onLayout  l=0, t=0,r=884,b=788
/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onMeasure: 
/com.idreamsky.softinput E/MainActivity: onSizeChanged! w=884,h=1626,oldw=884,oldh=788
/com.idreamsky.softinput E/MainActivity: onLayout  l=0, t=0,r=884,b=1626           

每次弹出或隐藏软键盘都会调用onSizeChanged方法,但这种作法有局限性,所以并不好用。

作法二:
对界面的根布局进行监听,这也是目前社区解决文首问题的方案
在界面加载后注册ViewTreeObserver mRootView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener)
在页面销毁的时候别忘了移除掉监听,否则onGlobalLayoutListener持有外部类的引用有可能造成内存泄露
mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(onGlobalLayoutListener)

接下来最重要的就是监听里面的实现了

GsdVideoPlayerFragment.java

private ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        //判断隐藏软键盘是否弹出
//            if (((Activity) mContext).getWindow().getAttributes().softInputMode == WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
//                showKeyboard(mContext);
//            }

        Rect r = new Rect();
        View root = mRootView.getRootView();
        root.getWindowVisibleDisplayFrame(r);
        Display display = ((Activity)mContext).getWindow().getWindowManager().getDefaultDisplay();
        int h = -1;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
            Point p = new Point();
            display.getSize(p);
            h = p.y;
        } else {
            h = display.getHeight();
        }
        int inputHeight = h - r.bottom;
        boolean visible = inputHeight > 100;
        if (visible == mSoftInputVisible) {
            return;
        }
        if (visible) { //弹出键盘
            if (mBottomEmptyView != null) {
                mBottomEmptyView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, inputHeight -
                        ImageUtils.dip2px(mContext, 70)));//键盘高度减去主页tab和bottomMargin
                mBottomEmptyView.setVisibility(View.VISIBLE);
            }
        } else if (mBottomEmptyView != null) {
            mBottomEmptyView.setVisibility(View.GONE);
        }
        mSoftInputVisible = visible;
    }
};

这里的主要思路是在EditText的下方设置一个EmptyView,当检测到窗口进行了界面调整后,改变它的大小以便将EditText顶上去能让用户看到,这里计算了软键盘的高度,并减去了TAB栏的高度及下方的BottomMargin,剩下的就是EmptyView应该显示的高度了。做完上述调整后,当弹起软键盘后是这样的:

软键盘的隐藏

上面已经解决了软键盘弹出时,界面被顶上去以及软键盘遮挡了输入框的问题。当在上图界面中,按back或左上箭头返回后,发现回退到上一个界面软键盘并没有消失,但实际上在回退过程已经做了隐藏软键盘的操作

BaseFragment.java

/**
 * 收起输入法键盘
 */
protected void hideSoftKeyBoard() {
    try {
        InputMethodManager imm = (InputMethodManager) mContext
                .getSystemService(Service.INPUT_METHOD_SERVICE);
        View focusView = ((Activity) mContext).getCurrentFocus();
        if (null != focusView) {
            IBinder windowToken = focusView.getWindowToken();
            imm.hideSoftInputFromWindow(windowToken,
                    InputMethodManager.HIDE_NOT_ALWAYS);
            //imm.hideSoftInputFromWindow(windowToken, 0);
            ((Activity) mContext).getWindow().setSoftInputMode(
                    WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
        }
    } catch (Exception e) {

    }
}

是Fragment基类抽出来的一个方法,开始我以为是因为隐藏软键盘的方法不正确,于是改成了注释中的代码进行测试,发现问题不会出现了。但是很疑惑为什么用上面的方法就不行呢,而且在自己写的小Demo中,也是可以进行隐藏软键盘的,经过排查,发现在上面的onGlobalLayoutListener回调里有这么一段:

//判断隐藏软键盘是否弹出
  if (((Activity) mContext).getWindow().getAttributes().softInputMode == WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
  showKeyboard(mContext);
  }

public void showKeyboard(Context ctx) {
    InputMethodManager imm = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
    imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}

也就是我上面注释的代码,当我们回退界面时,做了软键盘的隐藏操作imm.hideSoftInputFromWindow(windowToken,0),
然后又更改了窗口的SoftModesetSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN),根据上面的结论,隐藏键盘会触发onGlobalLayoutListener,上面的代码刚好判断了标记为SOFT_INPUT_STATE_HIDDEN时,强制弹起了软键盘imm.toggleSoftInput(InputMethodManager.SHOW_FORCED,0) ….. 所以就出现了最开始的现象。

其实GlobalLayoutListener里注释的代码和隐藏软键盘后改了SoftMode都是不必要的,将这些注释掉后就一切正常了,但这也提醒我们要去了解下这几个隐藏键盘和显示键盘的方法到底有什么区别。

最好的方法就是看源码或者官方文档的解释了

InputMethodManager.java

/**
 * Flag for {@link #hideSoftInputFromWindow} to indicate that the soft
 * input window should only be hidden if it was not explicitly shown
 * by the user.
 */
public static final int HIDE_IMPLICIT_ONLY = 0x0001;

/**
 * Flag for {@link #hideSoftInputFromWindow} to indicate that the soft
 * input window should normally be hidden, unless it was originally
 * shown with {@link #SHOW_FORCED}.
 */
public static final int HIDE_NOT_ALWAYS = 0x0002;

/**
 * Synonym for {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)}
 * without a result: request to hide the soft input window from the
 * context of the window that is currently accepting input.
 * 
 * @param windowToken The token of the window that is making the request,
 * as returned by {@link View#getWindowToken() View.getWindowToken()}.
 * @param flags Provides additional operating flags.  Currently may be
 * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
 */
public boolean hideSoftInputFromWindow(IBinder windowToken, int flags) {
    return hideSoftInputFromWindow(windowToken, flags, null);
}

HIDE_IMPLICIT_ONLY : 表示如果用户未显示地显示软键盘窗口,则隐藏窗口
HIDE_NOT_ALWAYS: 表示软键盘窗口总是隐藏,除非开始时以SHOW_FORCED显示
还是挺绕口的,自己领会吧。经过查找资料,发现一般写成imm.hideSoftInputWindow(windowToken,0)就好了。

参考资料