1. 程式人生 > >Android鍵盤彈出的研究

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的影響

這裡寫圖片描述

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

<?xml version="1.0" encoding="utf-8"?>
<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<SoftKeyboardStateListener> listeners = new LinkedList<SoftKeyboardStateListener>();
    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測量時需要考慮更多的東西,如橫豎屏的切換。