前言
上篇文章分析了軟鍵盤(pán)彈出揪垄、關(guān)閉穷吮、獲取軟鍵盤(pán)高度、常用屬性展示等饥努。這部分也是網(wǎng)上涉及軟鍵盤(pán)文章的重點(diǎn)捡鱼,,導(dǎo)致對(duì)常用屬性的理解止于Demo酷愧,對(duì)一些問(wè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)行效果如下:
可以看出戏自,界面沒(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)行效果如下:
可以看出志衣,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ī)為例:
屏幕尺寸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)用棧如下:
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)整胖腾。
最后用圖展示這種效果:
注意烟零,沒(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效果如下:
當(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)效果:
注意晦闰,沒(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è)可以的容器伴逸,一般來(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之后不能反射了癌椿。
上面方法走不通健蕊,還好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ó)之路至此完成了歌懒。跟压。
值得注意的是,getWindowVisibleDisplayFrame(xx)的計(jì)算依賴于mAttachInfo.mVisibleInsets歼培,而mAttachInfo.mVisibleInsets值發(fā)生變化的條件是設(shè)置了SOFT_INPUT_ADJUST_RESIZE或者SOFT_INPUT_ADJUST_PAN模式。
本文基于Android 10.0