Android-軟鍵盤(pán)一招搞定(原理篇)

前言

上篇文章分析了軟鍵盤(pán)彈出揪垄、關(guān)閉穷吮、獲取軟鍵盤(pán)高度、常用屬性展示等饥努。這部分也是網(wǎng)上涉及軟鍵盤(pán)文章的重點(diǎn)捡鱼,\color{Red}{但是幾乎沒(méi)人將原理剖析過(guò)},導(dǎo)致對(duì)常用屬性的理解止于Demo酷愧,對(duì)一些問(wèn)題的了解似是而非驾诈。因此,本篇文章將分析常用屬性生效原理伟墙。
本系列文章:

Android 軟鍵盤(pán)一招搞定(實(shí)踐篇)
Android 軟鍵盤(pán)一招搞定(原理篇)

通過(guò)本篇文章翘鸭,你將了解到:

1、SOFT_INPUT_ADJUST_RESIZE 原理及其使用
2戳葵、SOFT_INPUT_ADJUST_PAN 原理及其使用
3就乓、SOFT_INPUT_ADJUST_UNSPECIFIED 原理及其使用
4、SOFT_INPUT_ADJUST_NOTHING 原理及其使用
5拱烁、getWindowVisibleDisplayFrame(Rect outRect) 如何獲取可見(jiàn)區(qū)域

1生蚁、SOFT_INPUT_ADJUST_RESIZE 原理及其使用

一個(gè)小Demo

先設(shè)置softInputMode 為SOFT_INPUT_ADJUST_RESIZE。
再來(lái)看看Activity布局文件:

第一個(gè)Demo

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/myviewgroup"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:layout_gravity="center_vertical"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/iv"
        android:src="@drawable/test"
        android:background="@color/colorGreen"
        android:layout_marginTop="20dp"
        android:layout_marginBottom="20dp"
        android:layout_width="match_parent"
        android:layout_height="300dp">
    </ImageView>

    <EditText
        android:hint="輸入框2"
        android:id="@+id/et2"
        android:layout_marginTop="100dp"
        android:background="@drawable/bg"
        android:layout_gravity="bottom"
        android:layout_marginHorizontal="10dp"
        android:layout_width="match_parent"
        android:layout_height="40dp">
    </EditText>

</LinearLayout>

運(yùn)行效果如下:


device-2020-10-16-001127.gif

可以看出戏自,界面沒(méi)有變動(dòng)啊邦投,似乎SOFT_INPUT_ADJUST_RESIZE失效了。
將布局文件稍微更改一下:

第二個(gè)Demo

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/myviewgroup"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:layout_gravity="center_vertical"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/iv"
        android:src="@drawable/test"
        android:background="@color/colorGreen"
        android:layout_marginTop="20dp"
        android:layout_marginBottom="20dp"
        android:layout_width="match_parent"
        android:layout_weight="1"
        android:layout_height="0dp">
    </ImageView>

    <EditText
        android:hint="輸入框2"
        android:id="@+id/et2"
        android:layout_marginTop="100dp"
        android:background="@drawable/bg"
        android:layout_gravity="bottom"
        android:layout_marginHorizontal="10dp"
        android:layout_width="match_parent"
        android:layout_height="40dp">
    </EditText>

</LinearLayout>

對(duì)比前后布局文件的改變:只是更改了ImageView的高度擅笔。更改后運(yùn)行效果如下:


device-2020-10-17-190503.gif

可以看出志衣,ImageView高度變小了屯援,EditText也被頂上去了。

綜合以上兩個(gè)效果念脯,我們猜測(cè)結(jié)論:

  • SOFT_INPUT_ADJUST_RESIZE 設(shè)置后影響了布局的高度狞洋,至于是哪個(gè)布局現(xiàn)在不確定
  • 由于第一個(gè)Demo里ImageView高度是固定的,因此即使其父布局高度變小了也不會(huì)影響ImageView的展示绿店。而第二個(gè)Demo里ImageView高度跟隨父布局高度變化吉懊,因此當(dāng)父布局高度變化時(shí),ImageView也隨著變化假勿。

問(wèn)題的關(guān)鍵轉(zhuǎn)變?yōu)椋?img class="math-inline" src="https://math.jianshu.com/math?formula=%5Ccolor%7BRed%7D%7B%E5%93%AA%E4%B8%AA%E5%B8%83%E5%B1%80%E7%9A%84%E9%AB%98%E5%BA%A6%E6%94%B9%E5%8F%98%E4%BA%86%EF%BC%9F%E6%98%AF%E5%A6%82%E4%BD%95%E6%94%B9%E5%8F%98%E7%9A%84%EF%BC%9F%7D" alt="\color{Red}{哪個(gè)布局的高度改變了借嗽?是如何改變的?}" mathimg="1">

SOFT_INPUT_ADJUST_RESIZE 原理剖析

由以上的猜測(cè)知道一定是ViewTree里的某個(gè)ViewGroup高度變化了转培,而鍵盤(pán)彈出是個(gè)Window的展示恶导,Window的彈出導(dǎo)致了ViewTree的變化,那么自然想到Window和ViewTree的聯(lián)系:ViewRootImpl.java堡距。
鍵盤(pán)彈出影響了Activity Window的窗口大小甲锡,在ViewRootImpl里有接收WMS事件變化的地方:

#ViewRootImpl.java
    final class ViewRootHandler extends Handler {
        ...
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                ...
                //接收窗口變化事件
                case MSG_RESIZED: {
                    //args記錄了各個(gè)區(qū)域大大小
                    SomeArgs args = (SomeArgs) msg.obj;
                    //arg1---->Window的尺寸
                    //arg2---->內(nèi)容區(qū)域限定邊界
                    //arg3----->可見(jiàn)區(qū)域的限定邊界
                    //arg6----->固定區(qū)域的限定邊界
                    //------------------------------------>(1)
                    if (mWinFrame.equals(args.arg1)
                            && mPendingOverscanInsets.equals(args.arg5)
                            && mPendingContentInsets.equals(args.arg2)
                            && mPendingStableInsets.equals(args.arg6)
                            && mPendingDisplayCutout.get().equals(args.arg9)
                            && mPendingVisibleInsets.equals(args.arg3)
                            && mPendingOutsets.equals(args.arg7)
                            && mPendingBackDropFrame.equals(args.arg8)
                            && args.arg4 == null
                            && args.argi1 == 0
                            && mDisplay.getDisplayId() == args.argi3) {
                        //各個(gè)區(qū)域大小都沒(méi)變化,則不作任何操作
                        break;
                    }
                }
                case MSG_RESIZED_REPORT:
                    if (mAdded) {
                        SomeArgs args = (SomeArgs) msg.obj;
                        ...

                        final boolean framesChanged = !mWinFrame.equals(args.arg1)
                                || !mPendingOverscanInsets.equals(args.arg5)
                                || !mPendingContentInsets.equals(args.arg2)
                                || !mPendingStableInsets.equals(args.arg6)
                                || !mPendingDisplayCutout.get().equals(args.arg9)
                                || !mPendingVisibleInsets.equals(args.arg3)
                                || !mPendingOutsets.equals(args.arg7);

                        //重新設(shè)置Window 尺寸
                        setFrame((Rect) args.arg1);
                        //將值記錄到各個(gè)成員變量里
                        mPendingOverscanInsets.set((Rect) args.arg5);
                        mPendingContentInsets.set((Rect) args.arg2);
                        mPendingStableInsets.set((Rect) args.arg6);
                        mPendingDisplayCutout.set((DisplayCutout) args.arg9);
                        mPendingVisibleInsets.set((Rect) args.arg3);
                        mPendingOutsets.set((Rect) args.arg7);
                        mPendingBackDropFrame.set((Rect) args.arg8);
                        mForceNextWindowRelayout = args.argi1 != 0;
                        mPendingAlwaysConsumeSystemBars = args.argi2 != 0;

                        args.recycle();

                        if (msg.what == MSG_RESIZED_REPORT) {
                            reportNextDraw();
                        }

                        if (mView != null && (framesChanged || configChanged)) {
                            //尺寸發(fā)生變化羽戒,強(qiáng)制走layout+draw過(guò)程-----------(2)
                            forceLayout(mView);
                        }
                        //重新layout--------------(3)
                        requestLayout();
                    }
                    break;
                    ...
            }
        }
    }

上面代碼列出了三個(gè)重點(diǎn),分別來(lái)看看虎韵。
(1)

//arg1---->Window的尺寸
//arg2---->內(nèi)容區(qū)域限定邊界
//arg3----->可見(jiàn)區(qū)域的限定邊界
//arg6----->固定區(qū)域的限定邊界
arg 是Rect類型

"限定邊界"是什么意思呢易稠?以我測(cè)試機(jī)為例:


image.png

屏幕尺寸1080*1920
當(dāng)鍵盤(pán)彈出時(shí):

arg1---->Rect(0, 0 - 1080, 1920)
arg2---->Rect(0, 63 - 0, 972)
arg3---->Rect(0, 63 - 0, 972)
arg6---->Rect(0, 63 - 0, 126)

可以看出,所謂的"限定邊界"實(shí)際上就是上面矩形區(qū)域包蓝。
當(dāng)鍵盤(pán)收起后:

arg1---->Rect(0, 0 - 1080, 1920)
arg2---->Rect(0, 63 - 0, 126)
arg3---->Rect(0, 63 - 0, 126)
arg6---->Rect(0, 63 - 0, 126)

看到此驶社,大家都明白了:
arg1表示的屏幕尺寸
arg6表示的是狀態(tài)欄和導(dǎo)航欄的高度,arg6賦值給了mPendingStableInsets测萎,從名字可以看出亡电,這值是不變的。
無(wú)論鍵盤(pán)彈出還是關(guān)閉硅瞧,這兩個(gè)值都不變份乒,變的是arg2和arg3,而arg2賦值給了mPendingContentInsets腕唧,arg3賦值給了mPendingVisibleInsets或辖。
好了,現(xiàn)在arg2枣接、arg3颂暇、arg6都記錄到成員變量里了。

(2)(3)
尺寸發(fā)生了變化后調(diào)用:
forceLayout(mView)--->ViewTree里每個(gè)View/ViewGroup打上layout但惶、draw標(biāo)記耳鸯,也就是說(shuō)每個(gè)View/ViewGroup 最后都會(huì)執(zhí)行三大流程湿蛔。
requestLayout()--->觸發(fā)執(zhí)行三大流程

既然記錄了尺寸的變化,繼續(xù)跟蹤這些值怎么使用县爬。調(diào)用requestLayout()將會(huì)觸發(fā)執(zhí)行performTraversals()方法:

#ViewRootImpl.java
    private void performTraversals() {
        if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
            ...
            boolean hwInitialized = false;
            //內(nèi)容邊界是否發(fā)生變化
            boolean contentInsetsChanged = false;
            try {
                ...
                //內(nèi)容區(qū)域變化----------->1
                contentInsetsChanged = !mPendingContentInsets.equals(
                        mAttachInfo.mContentInsets);

                if (contentInsetsChanged || mLastSystemUiVisibility !=
                        mAttachInfo.mSystemUiVisibility || mApplyInsetsRequested
                        || mLastOverscanRequested != mAttachInfo.mOverscanRequested
                        || outsetsChanged) {
                    ...
                    //分發(fā)Inset----------->2
                    dispatchApplyInsets(host);
                    contentInsetsChanged = true;
                }
                ...
            } catch (RemoteException e) {
            }
            ...
        }
        ...
    }

還是列出兩個(gè)重點(diǎn):
(1)
內(nèi)容區(qū)域發(fā)生變化阳啥。
當(dāng)設(shè)置SOFT_INPUT_ADJUST_RESIZE,鍵盤(pán)彈起時(shí)內(nèi)容區(qū)域發(fā)生變化捌省,因此會(huì)執(zhí)行dispatchApplyInsets()苫纤。
當(dāng)設(shè)置SOFT_INPUT_ADJUST_PAN,鍵盤(pán)彈起時(shí)內(nèi)容部區(qū)域不變纲缓,因此不會(huì)執(zhí)行dispatchApplyInsets()卷拘。
(2)
分發(fā)Inset。
這些記錄的值會(huì)存儲(chǔ)在AttachInfo對(duì)應(yīng)的變量里祝高。
該方法調(diào)用棧如下:

image.png

dispatchApplyWindowInsets(WindowInsets insets)里的insets構(gòu)成是通過(guò)計(jì)算之前記錄在mPendingXX里的邊界值栗弟。
最終調(diào)用fitSystemWindowsInt():

#View.java
    private boolean fitSystemWindowsInt(Rect insets) {
        //FITS_SYSTEM_WINDOWS 為xml里設(shè)置     android:fitsSystemWindows="true"
        //對(duì)于DecorView的子布局LinearLayout來(lái)說(shuō),默認(rèn)fitsSystemWindows=true
        if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
            ...
            //設(shè)置View的padding
            internalSetPadding(localInsets.left, localInsets.top,
                    localInsets.right, localInsets.bottom);
            return res;
        }
        return false;
    }

    protected void internalSetPadding(int left, int top, int right, int bottom) {
        ...
        if (mPaddingLeft != left) {
            changed = true;
            mPaddingLeft = left;
        }
        if (mPaddingTop != top) {
            changed = true;
            mPaddingTop = top;
        }
        if (mPaddingRight != right) {
            changed = true;
            mPaddingRight = right;
        }
        if (mPaddingBottom != bottom) {
            changed = true;
            mPaddingBottom = bottom;
        }

        if (changed) {
            requestLayout();
            invalidateOutline();
        }
    }

看到這答案就呼之欲出了工闺,DecorView的子布局LinearLayout設(shè)置padding乍赫,最終會(huì)影響LinearLayout子布局的高度,一層層傳遞下去陆蟆,就會(huì)影響到Demo里的Activity 布局文件的高度雷厂。
小結(jié)

1、當(dāng)設(shè)置SOFT_INPUT_ADJUST_RESIZE 時(shí)叠殷,DecorView的子布局padding會(huì)改變改鲫,最后影響子孫布局的高度。
2林束、父布局高度的變化并不一定會(huì)讓子布局重新布局像棘,因此針對(duì)上面的第一個(gè)Demo,我們需要監(jiān)聽(tīng)鍵盤(pán)的變化從而調(diào)整輸入框的位置壶冒。而對(duì)于上面的第二個(gè)Demo缕题,不需要手動(dòng)調(diào)整,父布局會(huì)自動(dòng)調(diào)整胖腾。

最后用圖展示這種效果:


image.png

注意烟零,沒(méi)有特意標(biāo)注狀態(tài)欄和導(dǎo)航欄

2、SOFT_INPUT_ADJUST_PAN 原理及其使用

一個(gè)小Demo

先設(shè)置softInputMode 為SOFT_INPUT_ADJUST_PAN胸嘁。
在上述的第一個(gè)Demo基礎(chǔ)上修改布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/myviewgroup"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:fitsSystemWindows="true"
    android:layout_gravity="center_vertical"
    tools:context=".MainActivity">

    <EditText
        android:hint="輸入框1"
        android:id="@+id/et1"
        android:layout_marginTop="10dp"
        android:background="@drawable/bg"
        android:layout_marginHorizontal="10dp"
        android:layout_width="match_parent"
        android:layout_height="40dp">
    </EditText>

    <ImageView
        android:id="@+id/iv"
        android:src="@drawable/test"
        android:background="@color/colorGreen"
        android:layout_marginTop="20dp"
        android:layout_marginBottom="20dp"
        android:layout_width="match_parent"
        android:layout_height="300dp">
    </ImageView>

    <EditText
        android:hint="輸入框2"
        android:id="@+id/et2"
        android:layout_marginTop="100dp"
        android:background="@drawable/bg"
        android:layout_gravity="bottom"
        android:layout_marginHorizontal="10dp"
        android:layout_width="match_parent"
        android:layout_height="40dp">
    </EditText>

</LinearLayout>

實(shí)際上就是再增加了個(gè)EditText瓶摆。
分別點(diǎn)擊EditText1、EditText2效果如下:


device-2020-10-17-222959.gif

當(dāng)點(diǎn)擊輸入框1的時(shí)候性宏,界面沒(méi)有移動(dòng)群井,當(dāng)點(diǎn)擊輸入框2的時(shí)候,界面向上移動(dòng)了毫胜。接下來(lái)將分析為啥會(huì)有這樣的表現(xiàn)书斜。

SOFT_INPUT_ADJUST_PAN 原理剖析

SOFT_INPUT_ADJUST_PAN 和SOFT_INPUT_ADJUST_RESIZE 流程差不多诬辈,也是在ViewRootImpl里接收窗口變化的通知,不同的是:
當(dāng)鍵盤(pán)彈起時(shí):

arg1---->Rect(0, 0 - 1080, 1920)
arg2---->Rect(0, 63 - 0, 126)
arg3---->Rect(0, 63 - 0, 972)
arg6---->Rect(0, 63 - 0, 126)

可以看出arg2沒(méi)有變化荐吉,也就是內(nèi)容區(qū)域沒(méi)有變焙糟,最終不會(huì)執(zhí)行ViewRootImp-> dispatchApplyInsets(xx),當(dāng)然布局的高度就不會(huì)變样屠。

先來(lái)分析為什么點(diǎn)擊輸入框2能往上移動(dòng)穿撮。我們知道布局移動(dòng)無(wú)非就是坐標(biāo)發(fā)生改變,或者內(nèi)容滾動(dòng)了痪欲,不管是何種形式最終都需要通過(guò)對(duì)Canvas進(jìn)行位移才能實(shí)現(xiàn)移動(dòng)的效果悦穿。
當(dāng)窗口事件到來(lái)之后,發(fā)起View的三大繪制流程业踢,并且將限定邊界存儲(chǔ)到AttachInfo的成員變量里栗柒,有如下關(guān)系:

mPendingContentInsets-->mAttachInfo.mContentInsets;
mPendingVisibleInsets-->mAttachInfo.mVisibleInsets;

依舊是從三大流程開(kāi)啟的方法開(kāi)始分析。

#ViewRootImpl.java
    private void performTraversals() {
        ...
        //在執(zhí)行Draw過(guò)程之前執(zhí)行
        boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

        if (!cancelDraw) {
            ...
            //開(kāi)啟Draw過(guò)程
            performDraw();
        } else {
            ...
        }
    }

dispatchOnPreDraw()最終執(zhí)行了scrollToRectOrFocus(xx)方法:

#View.java
    boolean scrollToRectOrFocus(Rect rectangle, boolean immediate) {
        //窗口內(nèi)容區(qū)域
        final Rect ci = mAttachInfo.mContentInsets;
        //窗口可見(jiàn)區(qū)域
        final Rect vi = mAttachInfo.mVisibleInsets;
        //滾動(dòng)距離
        int scrollY = 0;
        boolean handled = false;

        if (vi.left > ci.left || vi.top > ci.top
                || vi.right > ci.right || vi.bottom > ci.bottom) {
            scrollY = mScrollY;
            //找到當(dāng)前有焦點(diǎn)的View------------>(1)
            final View focus = mView.findFocus();
            ...
            if (focus == lastScrolledFocus && !mScrollMayChange && rectangle == null) {
                //焦點(diǎn)沒(méi)有發(fā)生切換知举,不做操作
            } else {
                // We need to determine if the currently focused view is
                // within the visible part of the window and, if not, apply
                // a pan so it can be seen.
                mLastScrolledFocus = new WeakReference<View>(focus);
                mScrollMayChange = false;
                if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Need to scroll?");
                // Try to find the rectangle from the focus view.
                if (focus.getGlobalVisibleRect(mVisRect, null)) {
                    ...
                    //找到當(dāng)前焦點(diǎn)與可見(jiàn)區(qū)域的相交部分
                    //mVisRect 為當(dāng)前焦點(diǎn)在Window里的可見(jiàn)部分
                    if (mTempRect.intersect(mVisRect)) {
                        if (mTempRect.height() >
                                (mView.getHeight()-vi.top-vi.bottom)) {
                            ...
                        }
                        else if (mTempRect.top < vi.top) {
                            //如果當(dāng)前焦點(diǎn)位置在窗口可見(jiàn)區(qū)域上邊瞬沦,說(shuō)明焦點(diǎn)View應(yīng)該往下移動(dòng)到可見(jiàn)區(qū)域里邊
                            scrollY = mTempRect.top - vi.top;
                        } else if (mTempRect.bottom > (mView.getHeight()-vi.bottom)) {
                            //如果當(dāng)前焦點(diǎn)位置在窗口可見(jiàn)區(qū)域之下,說(shuō)明其應(yīng)該往上移動(dòng)到可見(jiàn)區(qū)域里邊------->(2)
                            scrollY = mTempRect.bottom - (mView.getHeight()-vi.bottom);
                        } else {
                            //無(wú)需滾動(dòng)------->(3)
                            scrollY = 0;
                        }
                        handled = true;
                    }
                }
            }
        }

        if (scrollY != mScrollY) {
            //滾動(dòng)距離發(fā)生變化
            if (!immediate) {
                if (mScroller == null) {
                    mScroller = new Scroller(mView.getContext());
                }
                //開(kāi)始設(shè)置滾動(dòng)----------->(4)
                mScroller.startScroll(0, mScrollY, 0, scrollY-mScrollY);
            } else if (mScroller != null) {
                mScroller.abortAnimation();
            }
            //賦值給成員變量
            mScrollY = scrollY;
        }
        return handled;
    }

(1)
對(duì)于上面的Demo來(lái)說(shuō)雇锡,當(dāng)前的焦點(diǎn)View就是EditText逛钻,點(diǎn)擊哪個(gè)EditText,哪個(gè)就獲得焦點(diǎn)锰提。

(2)
對(duì)于輸入框2來(lái)說(shuō)绣的,因?yàn)殒I盤(pán)彈出會(huì)遮住它,通過(guò)計(jì)算滿足"當(dāng)前焦點(diǎn)位置在窗口可見(jiàn)區(qū)域之下欲账,說(shuō)明其應(yīng)該往上移動(dòng)到可見(jiàn)區(qū)域里邊" 條件,因此srolly > 0芭概。

(3)
而對(duì)于輸入框1來(lái)說(shuō)赛不,當(dāng)鍵盤(pán)彈出時(shí),它沒(méi)有被鍵盤(pán)遮擋罢洲,走到else分支踢故,因此scrollY = 0。

(4)
滾動(dòng)是借助Scoller.java類完成的惹苗。

上面的操作實(shí)際上就是為了確認(rèn)滾動(dòng)值殿较,并記錄在成員變量mScrollY里,繼續(xù)來(lái)看如何使用滾動(dòng)值呢桩蓉?

#ViewRootImpl.java
    private boolean draw(boolean fullRedrawNeeded) {
        ...
        boolean animating = mScroller != null && mScroller.computeScrollOffset();
        final int curScrollY;
        //獲取當(dāng)前需要滾動(dòng)的scroll值
        if (animating) {
            curScrollY = mScroller.getCurrY();
        } else {
            curScrollY = mScrollY;
        }
        ...
        
        int xOffset = -mCanvasOffsetX;
        //記錄在yOffset里
        int yOffset = -mCanvasOffsetY + curScrollY;
        
        boolean useAsyncReport = false;
        if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
            if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
                ...
                //對(duì)于走硬件加速繪制
                if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) {
                    //記錄偏移量到mHardwareYOffset里
                    mHardwareYOffset = yOffset;
                    mHardwareXOffset = xOffset;
                    invalidateRoot = true;
                }
                ..
                mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
            } else {
                //軟件繪制
                //傳入yOffset
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
                }
            }
        }
        ...
        return useAsyncReport;
    }

滾動(dòng)值分別傳遞給了硬件加速繪制分支和軟件繪制分支淋纲,在各自的分支里對(duì)Canvas進(jìn)行平移,具體如何平移此處不細(xì)說(shuō)了院究,大家可以繼續(xù)跟蹤代碼分析洽瞬。
小結(jié)

1本涕、當(dāng)設(shè)置SOFT_INPUT_ADJUST_PAN時(shí),如果發(fā)現(xiàn)鍵盤(pán)遮住了當(dāng)前有焦點(diǎn)的View伙窃,那么會(huì)對(duì)RootView(此處Demo里DecorView作為RootView)的Canvas進(jìn)行平移菩颖,直至有焦點(diǎn)的View顯示到可見(jiàn)區(qū)域?yàn)橹埂?br> 2、這就是為什么點(diǎn)擊輸入框2的時(shí)候布局會(huì)整體向上移動(dòng)的原因为障。

同樣的最后用圖展示這種移動(dòng)效果:


image.png

注意晦闰,沒(méi)有特意標(biāo)注狀態(tài)欄和導(dǎo)航欄

3、SOFT_INPUT_ADJUST_UNSPECIFIED 原理及其使用

在上篇文章中通過(guò)Demo表明:設(shè)置了SOFT_INPUT_ADJUST_UNSPECIFIED鳍怨,其內(nèi)部最終使用SOFT_INPUT_ADJUST_PAN和SOFT_INPUT_ADJUST_RESIZE之一進(jìn)行展示呻右。接下來(lái)就來(lái)探究選擇的標(biāo)準(zhǔn)是什么。
softInputMode 沒(méi)有設(shè)值的時(shí)候京景,默認(rèn)是SOFT_INPUT_ADJUST_UNSPECIFIED模式窿冯。
還是從ViewRootImpl.java的performTraversals()方法開(kāi)始分析:

    private void performTraversals() {
        ...
        if (mFirst || mAttachInfo.mViewVisibilityChanged) {
            mAttachInfo.mViewVisibilityChanged = false;
            //先查看有沒(méi)有提前設(shè)置了模式
            int resizeMode = mSoftInputMode &
                    WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
            
            //如果沒(méi)有設(shè)置,那么默認(rèn)為0确徙,也就是SOFT_INPUT_ADJUST_UNSPECIFIED
            if (resizeMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED) {
                //查看mScrollContainers 數(shù)組有沒(méi)有元素(View 作為元素) -------->(1)
                final int N = mAttachInfo.mScrollContainers.size();
                for (int i=0; i<N; i++) {
                    if (mAttachInfo.mScrollContainers.get(i).isShown()) {
                        //如果有元素醒串,則設(shè)置為SOFT_INPUT_ADJUST_RESIZE 模式
                        resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
                    }
                }
                if (resizeMode == 0) {
                    //如果沒(méi)有設(shè)置為resize模式,則設(shè)置pan模式
                    resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
                }
                if ((lp.softInputMode &
                        WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) != resizeMode) {
                    lp.softInputMode = (lp.softInputMode &
                            ~WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) |
                            resizeMode;
                    //最后賦值給params鄙皇,讓W(xué)indow屬性生效
                    params = lp;
                }
            }
        }
        ...
    }

(1)
mAttachInfo.mScrollContainers 干嘛用的芜赌,什么時(shí)候添加元素進(jìn)去的呢?
View.java里有個(gè)方法:

public void setScrollContainer(boolean isScrollContainer){...}

意思就是說(shuō)該View是不是作為一個(gè)可以\color{Red}{滾動(dòng)}的容器伴逸,一般來(lái)說(shuō)缠沈,容器可以滾動(dòng)的話,那么它的高度可以伸縮的错蝴,既然可以伸縮洲愤,那么剛好符合SOFT_INPUT_ADJUST_RESIZE模式,因此此種情況下會(huì)設(shè)置為SOFT_INPUT_ADJUST_RESIZE模式顷锰。
調(diào)用該方法柬赐,將填充mAttachInfo.mScrollContainers 數(shù)組。
比如我們常用的RecyclerView:

    public RecyclerView(Context context, @android.annotation.Nullable AttributeSet attrs, int defStyle) {
        ...
        setScrollContainer(true);
        ...
    }

在構(gòu)造函數(shù)里默認(rèn)設(shè)置了該值官紫。
對(duì)于普通的View肛宋,如果想要設(shè)置該屬性有兩種方法:

1、代碼里:View.setScrollContainer(true)
2束世、xml里: android:isScrollContainer="true"

其中第二種方法在上篇"實(shí)踐篇"里有使用過(guò)酝陈。

4、SOFT_INPUT_ADJUST_NOTHING 原理及其使用

通過(guò)前面的分析毁涉,我們知道不論是SOFT_INPUT_ADJUST_RESIZE亦或是SOFT_INPUT_ADJUST_PAN沉帮,都是通過(guò)在ViewRootImpl里接收窗口變化的事件,最后做一系列調(diào)整產(chǎn)生對(duì)應(yīng)的效果的。
而當(dāng)設(shè)置了SOFT_INPUT_ADJUST_NOTHING時(shí)遇西,沒(méi)有事件發(fā)出馅精,當(dāng)然就沒(méi)有任何效果了。

5粱檀、getWindowVisibleDisplayFrame(Rect outRect) 如何獲取可見(jiàn)區(qū)域

看到此洲敢,你可能已經(jīng)明白了:狀態(tài)欄、導(dǎo)航欄茄蚯、屏幕可見(jiàn)區(qū)域压彭、內(nèi)容區(qū)域 限定邊界都是存儲(chǔ)在如下變量里:

AttachInfo.mStableInsets 狀態(tài)欄導(dǎo)航欄
AttachInfo.mContentInsets 內(nèi)容區(qū)域限定邊界
AttachInfo.mVisibleInsets 可見(jiàn)區(qū)域限定邊界

能獲取到上面的值,什么狀態(tài)欄渗常、導(dǎo)航欄壮不、鍵盤(pán)高度獲取不在話下。發(fā)現(xiàn)這些字段的訪問(wèn)權(quán)限是"default"皱碘,當(dāng)然你想到了反射询一,沒(méi)錯(cuò)反射是可以獲取這些值,但是在Android 10.0之后不能反射了癌椿。


image.png

上面方法走不通健蕊,還好Android還開(kāi)了個(gè)口子:getWindowVisibleDisplayFrame(xx):

    public void getWindowVisibleDisplayFrame(Rect outRect) {
        if (mAttachInfo != null) {
            try {
                //獲取Window尺寸,注意此處的尺寸是包含狀態(tài)欄踢俄、導(dǎo)航欄
                //與getWindowManager().getDefaultDisplay().getRealSize()尺寸一致;
                mAttachInfo.mSession.getDisplayFrame(mAttachInfo.mWindow, outRect);
            } catch (RemoteException e) {
                return;
            }
            ...
            final Rect insets = mAttachInfo.mVisibleInsets;
            //拿到可見(jiàn)區(qū)域(限定邊界)
            //運(yùn)算一下缩功,將可見(jiàn)區(qū)域記錄在outRect(相對(duì)于屏幕)
            outRect.left += insets.left;
            outRect.top += insets.top;
            outRect.right -= insets.right;
            outRect.bottom -= insets.bottom;
            return;
        }
        ...
    }

拿到的outRect就是可見(jiàn)區(qū)域的位置坐標(biāo)。
到此我們明白了都办,屏幕尺寸我們是知道的嫡锌,outRect我們也知道了,反推mAttachInfo.mVisibleInsets 也可以算出來(lái)了琳钉,這個(gè)值有了势木,鍵盤(pán)高度也就有了。
曲線救國(guó)之路至此完成了歌懒。跟压。


image.png

值得注意的是,getWindowVisibleDisplayFrame(xx)的計(jì)算依賴于mAttachInfo.mVisibleInsets歼培,而mAttachInfo.mVisibleInsets值發(fā)生變化的條件是設(shè)置了SOFT_INPUT_ADJUST_RESIZE或者SOFT_INPUT_ADJUST_PAN模式。

本文基于Android 10.0

您若喜歡茸塞,請(qǐng)點(diǎn)贊躲庄、關(guān)注,您的鼓勵(lì)是我前進(jìn)的動(dòng)力

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末钾虐,一起剝皮案震驚了整個(gè)濱河市噪窘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌效扫,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異矮慕,居然都是意外死亡犬性,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)工坊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事洽蛀。” “怎么了疟赊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵郊供,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我近哟,道長(zhǎng)驮审,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任吉执,我火速辦了婚禮疯淫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鼠证。我一直安慰自己峡竣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布量九。 她就那樣靜靜地躺著适掰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪荠列。 梳的紋絲不亂的頭發(fā)上类浪,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音肌似,去河邊找鬼费就。 笑死,一個(gè)胖子當(dāng)著我的面吹牛川队,可吹牛的內(nèi)容都是我干的力细。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼固额,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼眠蚂!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起斗躏,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤逝慧,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體笛臣,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡云稚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了沈堡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片静陈。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖踱蛀,靈堂內(nèi)的尸體忽然破棺而出窿给,到底是詐尸還是另有隱情,我是刑警寧澤率拒,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布崩泡,位于F島的核電站,受9級(jí)特大地震影響猬膨,放射性物質(zhì)發(fā)生泄漏角撞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一勃痴、第九天 我趴在偏房一處隱蔽的房頂上張望谒所。 院中可真熱鬧,春花似錦沛申、人聲如沸劣领。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)尖淘。三九已至,卻和暖如春著觉,著一層夾襖步出監(jiān)牢的瞬間村生,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工饼丘, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留趁桃,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓肄鸽,卻偏偏與公主長(zhǎng)得像卫病,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子典徘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容