Android键盘弹出的研究

参考:

http://stackoverflow.com/questions/2403632/android-show-soft-keyboard-automatically-when-focus-is-on-an-edittext

http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android/4737265#4737265

http://stackoverflow.com/questions/17410499/difference-between-adjustresize-and-adjustpan-in-android

键盘弹出基本上开发中都会用到,之前用的比较简单,最多也就是Activity windowSoftInputMode标签中设置属性,没有深入研究。直到最近在解决键盘弹出导致界面闪烁的问题以及在做直播功能需要在键盘弹出时控制View的测量遇到一些问题,决定总结一下键盘弹出相关的知识点:
1、键盘弹出,收起的控制。
2、键盘弹出对View的影响。
3、监听键盘的弹出和收起动作,获取键盘高度。
4、自定义View不受键盘弹出的影响。

键盘行为的控制

一般情况,键盘的显示和隐藏都交由系统控制,比如,当EditText获取焦点时,键盘会弹出来,当用户按返回键时,键盘会收起来。但有时我们需要手动控制键盘的隐藏和显示,比如点击某个按钮显示或者隐藏键盘。这时就要通过InputMethodManager 来实现。

显示键盘:

InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED,0);

隐藏键盘:

InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
View view = activity.getCurrentFocus();
imm.hideSoftInputFromWindow(view.getWindowToken(),0); 

键盘的弹出和收起已经没有问题,现在需要关注的是,键盘弹起时,整个界面的行为,对此,Android提供了两种方式来设置:
1、在manifest Activity 标签设置:

<activity android:windowSoftInputMode="adjustResize"> activity>

2、通过Java代码设置:

getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);

Java代码有一个好处是可以在运行时改变windowSoftInputMode的值,比如某个界面默认设置的是adjustResize,但是某种情况下,需要设置为adjustPan。

@Override
    public void onCheckedChanged(RadioGroup group, int checkedId) {
        if (checkedId == R.id.radioButtonAdjustPan) {
            getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
        } else {
            getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
        }
    }

从上面的代码可以看出来,windowSoftInputMode是对应Window的一个属性,因此,我们也可以在Dialog,PopupWindow中设置这个属性:

public class TestDialog extends Dialog {

    public TestDialog(Context context) {
        super(context, R.style.AlertDialog);
        init();
    }

    private void init() {
        setContentView(R.layout.dialog_edit_test);
        //getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
    }
}

adjustPan和adjustResize对View的影响

Android键盘弹出的研究_第1张图片

windowSoftInputMode的可设定的值有很多个,但基本上最常用的也就adjustPan和adjustResize两种:
adjustPan:窗口大小不变,但是整个窗口向上平移。
adjustResize:窗口大小被压缩,给键盘留出空间。

键盘弹起时,这两者对应的表现形式很好理解,现在重点是研究当windowSoftInputMode分别设置这两个属性时,对界面中的View有什么影响,会调用View的哪些方法。

下面的KRelativeLayout 继承RelativeLayout ,没有做任何逻辑处理,只加了Log:

public class KRelativeLayout extends RelativeLayout {

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

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

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

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        Log.i("Keyboard", "KRelativeLayout  onSizeChanged   width= " + w + "  height= " + h + "  oldWidth= " + oldw + "  oldHeight= " + oldh);
        super.onSizeChanged(w, h, oldw, oldh);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        Log.i("Keyboard", "KRelativeLayout  onMeasure   width= " + width + "  height= " + height);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        Log.i("Keyboard", "KRelativeLayout  onLayout   left= " + left + "  top= " + top + "  right= " + right + "  bottom= " + bottom);
        super.onLayout(changed, left, top, right, bottom);
    }

现在把KRelativeLayout 作为Layout中最顶层的View


<com.jx.androiddemos.keyboard.KRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:focusable="true"
    android:layout_height="match_parent">

    <RadioGroup
        android:id="@+id/mRadioGroup"
        android:layout_width="wrap_content"
        android:layout_margin="10dp"
        android:orientation="horizontal"
        android:layout_height="wrap_content">

        <RadioButton
            android:id="@+id/radioButtonAdjustPan"
            android:layout_width="wrap_content"
            android:checked="true"
            android:layout_height="wrap_content"
            android:text="AdjustPan"/>

        <RadioButton
            android:id="@+id/radioButtonAdjustResize"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="AdjustResize"/>
    RadioGroup>

    <RelativeLayout
        android:layout_width="match_parent"
                    android:layout_below="@id/mRadioGroup"
                    android:layout_height="wrap_content">
        <ImageView
            android:layout_width="match_parent"
            android:src="@drawable/b"
            android:layout_height="400dp"/>
    RelativeLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="@android:color/white"
        android:layout_gravity="bottom"
        android:orientation="vertical">

        <TextView
            android:id="@+id/mEduCourseNote"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="top"
            android:hint="输入公告"
            android:maxLength="100"
            android:textColor="#404040"
            android:textSize="13sp"/>

        <EditText
            android:id="@+id/mQuestionTextNum"
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:layout_gravity="right"
            android:text="100字"
            android:textColor="#CCCCCC"
            android:textSize="12sp"/>
    LinearLayout>

com.jx.androiddemos.keyboard.KRelativeLayout>

先把windowSoftInputMode设置成adjustPan时,从底部的EditText获取焦点弹出键盘到键盘收起这个过程,KRelativeLayout 打印的Log如下:

07-09 19:38:11.305 7486-7486/? I/Keyboard: KRelativeLayout  onMeasure   width= 540  height= 838
07-09 19:38:11.309 7486-7486/? I/Keyboard: KRelativeLayout  onLayout   left= 0  top= 0  right= 540  bottom= 838
07-09 19:51:28.796 7486-7486/? I/Keyboard: KRelativeLayout  onMeasure   width= 540  height= 838
07-09 19:51:28.798 7486-7486/? I/Keyboard: KRelativeLayout  onLayout   left= 0  top= 0  right= 540  bottom= 838

从Log可以看出,当windowSoftInputMode设置成adjustPan时,界面最顶层的View会调用onMeasure 和onLayout,重新测量和布局,但测量时整个View的宽高并没有发生变化。

现在把windowSoftInputMode设置为adjustResize,重复上面那个操作,Log如下:

07-09 20:03:04.250 7486-7486/? I/Keyboard: KRelativeLayout  onMeasure   width= 540  height= 838
07-09 20:03:04.312 7486-7486/? I/Keyboard: KRelativeLayout  onMeasure   width= 540  height= 410
07-09 20:03:04.318 7486-7486/? I/Keyboard: KRelativeLayout  onSizeChanged   width= 540  height= 410  oldWidth= 540  oldHeight= 838
07-09 20:03:04.320 7486-7486/? I/Keyboard: KRelativeLayout  onLayout   left= 0  top= 0  right= 540  bottom= 410
07-09 20:03:06.710 7486-7486/? I/Keyboard: KRelativeLayout  onMeasure   width= 540  height= 410
07-09 20:03:06.751 7486-7486/? I/Keyboard: KRelativeLayout  onMeasure   width= 540  height= 838
07-09 20:03:06.753 7486-7486/? I/Keyboard: KRelativeLayout  onSizeChanged   width= 540  height= 838  oldWidth= 540  oldHeight= 410
07-09 20:03:06.753 7486-7486/? I/Keyboard: KRelativeLayout  onLayout   left= 0  top= 0  right= 540  bottom= 838

从上面的Log可以看出,当windowSoftInputMode设置成adjustResize时,界面最顶层的View会重新测量和布局,另外还会回调onSizeChanged方法,View的高度从838 变成了540,这两个的差值对应的就是键盘的高度。

另外,adjustResize有一个坑,就是设置全屏的主题时不起作用。

监听键盘的弹起和收起,获取键盘的高度

有时候需求需要监听键盘的弹起和收起,获取键盘的高度,但是Android并没有直接提供API,所以只能采用间接的方式来实现。从上面的Log我们可以看出,当windowSoftInputMode设置成adjustResize,最顶层的View的高度会发生变化,这就是Android监听键盘的弹起和收起、获取键盘高度的原理。
因此,如果需要监听键盘的弹起和收起,获取键盘的高度,那么windowSoftInputMode必须设置成adjustResize

监听键盘的弹起和收起,获取键盘的高度可能在多处用到,因此最好封装一下,用起来比较方便,我在stackoverflow找到比较好的封装:

public class SoftKeyboardStateWatcher implements ViewTreeObserver.OnGlobalLayoutListener {

    public interface SoftKeyboardStateListener {
        void onSoftKeyboardOpened(int keyboardHeightInPx);
        void onSoftKeyboardClosed();
    }

    private final List listeners = new LinkedList();
    private final View activityRootView;
    private int        lastSoftKeyboardHeightInPx;
    private boolean    isSoftKeyboardOpened;

    public SoftKeyboardStateWatcher(View activityRootView) {
        this(activityRootView, false);
    }

    public SoftKeyboardStateWatcher(View activityRootView, boolean isSoftKeyboardOpened) {
        this.activityRootView     = activityRootView;
        this.isSoftKeyboardOpened = isSoftKeyboardOpened;
        activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    @Override
    public void onGlobalLayout() {
        final Rect r = new Rect();
        //r will be populated with the coordinates of your view that area still visible.
        activityRootView.getWindowVisibleDisplayFrame(r);

        final int heightDiff = activityRootView.getRootView().getHeight() - (r.bottom - r.top);
        if (!isSoftKeyboardOpened && heightDiff > 100) { // if more than 100 pixels, its probably a keyboard...
            isSoftKeyboardOpened = true;
            notifyOnSoftKeyboardOpened(heightDiff);
        } else if (isSoftKeyboardOpened && heightDiff < 100) {
            isSoftKeyboardOpened = false;
            notifyOnSoftKeyboardClosed();
        }
    }

    public void setIsSoftKeyboardOpened(boolean isSoftKeyboardOpened) {
        this.isSoftKeyboardOpened = isSoftKeyboardOpened;
    }

    public boolean isSoftKeyboardOpened() {
        return isSoftKeyboardOpened;
    }

    /**
     * Default value is zero {@code 0}.
     *
     * @return last saved keyboard height in px
     */
    public int getLastSoftKeyboardHeightInPx() {
        return lastSoftKeyboardHeightInPx;
    }

    public void addSoftKeyboardStateListener(SoftKeyboardStateListener listener) {
        listeners.add(listener);
    }

    public void removeSoftKeyboardStateListener(SoftKeyboardStateListener listener) {
        listeners.remove(listener);
    }

    private void notifyOnSoftKeyboardOpened(int keyboardHeightInPx) {
        this.lastSoftKeyboardHeightInPx = keyboardHeightInPx;

        for (SoftKeyboardStateListener listener : listeners) {
            if (listener != null) {
                listener.onSoftKeyboardOpened(keyboardHeightInPx);
            }
        }
    }

    private void notifyOnSoftKeyboardClosed() {
        for (SoftKeyboardStateListener listener : listeners) {
            if (listener != null) {
                listener.onSoftKeyboardClosed();
            }
        }
    }
}

使用起来很方便:

final SoftKeyboardStateWatcher softKeyboardStateWatcher = new SoftKeyboardStateWatcher (findViewById(R.id.mRootLayout));

        // Add listener
        softKeyboardStateWatcher.addSoftKeyboardStateListener(new KeyboardWatcher.SoftKeyboardStateListener() {
            @Override
            public void onSoftKeyboardOpened(int keyboardHeightInPx) {

            }

            @Override
            public void onSoftKeyboardClosed() {

            }
        });
    }

自定义View,不受键盘弹出的影响

windowSoftInputMode设置成adjustResize时,键盘弹出,整个窗口的高度会被压缩,界面所有的View的重新测量、布局,View高度都会改变。可有时我们有这样的需求,当键盘弹出来时,我们希望某个View高度不要改变。假设视频播放的界面由两层布局组成,底下的一层是播放的View,上面一层是弹幕和输入框。我们希望用户评论键盘弹出时,底下播放的View位置,高度不变,而上面弹幕则随着键盘弹出上移。
这时就需要自定义一个ViewGroup,当键盘弹出时,让它的高度不改变:

public class KeyboardLayout extends RelativeLayout {
    private int mHeight;
    private int mWidth;
    private Rect mRect;
    private int mWidthMeasureSpec;
    private int mHeightMeasureSpec;

    public KeyboardLayout(Context context) {
        super(context);
        this.mHeight = 0;
        this.mWidth = 0;
        this.mRect = new Rect();
    }

    public KeyboardLayout(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
        this.mHeight = 0;
        this.mWidth = 0;
        this.mRect = new Rect();
    }


    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        this.getWindowVisibleDisplayFrame(this.mRect);
        if (this.mWidth == 0 && this.mHeight == 0) {
            this.mWidth = this.getRootView().getWidth();
            this.mHeight = this.getRootView().getHeight();
        }

        int windowHeight = this.mRect.bottom - this.mRect.top;
        if ( this.mHeight - windowHeight >  this.mHeight / 4) {
            //键盘弹出,默认用保存的测量值
            super.onMeasure(this.mWidthMeasureSpec, this.mHeightMeasureSpec);
        } else {
            this.mWidthMeasureSpec = widthMeasureSpec;
            this.mHeightMeasureSpec = heightMeasureSpec;
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
}

上面的自定义View继承RelativeLayout ,可以满足键盘弹出时,布局不被压缩,第一次测量时,先保存测量的宽高,当满足键盘弹出的条件时:

 if ( this.mHeight - windowHeight >  this.mHeight / 4) {
            //键盘弹出,默认用保存的测量值
            super.onMeasure(this.mWidthMeasureSpec, this.mHeightMeasureSpec);
        }

则不参照父View传给它的widthMeasureSpec和heightMeasureSpec来测量自己的宽高,而是用上次保存的mWidthMeasureSpec和mHeightMeasureSpec来测量,这样保证键盘弹出,KeyboardLayout 位置高度不会改变。
注意,上面这个自定义View来只是提供了键盘弹出View高度,位置不变的一种方法,实际开发中在View测量时需要考虑更多的东西,如横竖屏的切换。

你可能感兴趣的:(Android开发)