窗口大小的計算
一個應用窗口,除了應用程序本身的內容外,還有狀態欄,可能還有輸入法窗口,狀態欄的大小是固定的,輸入法窗口可以在AndroidManifest.xml中配置,相關屬性如下:
以state開頭的表示當Activity成為焦點時軟鍵盤是隱藏還是可見,以adjust開頭的表示如何調整Activity窗口以容納軟鍵盤。
frameworks/base/core/res/res/values/Attrs.xml
ViewRootImpl中的performTraversals方法,是計算應用窗口大小的起點,在程序運行過程中這個方法會被調用多次。
如果是第一次(mFirst==true)執行遍歷,或者窗口內容有變化會調用dispatchApplyInsets,然后會調用到View的onApplyWindowInsets,進一步調用到fitSystemWindows(Rectinsets),來通知一個窗口的insets發生了改變,讓View有機會適應最新的變化,他會沿着ViewTree從上到下傳遞給各個View對象。通常不需要處理這個函數,但是如果使用SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION這兩個屬性除外,因為應用窗口的內容可能會在系統元素之下。
如果mFirst==false,不是第一次調用這個函數(performTraversals),那么:desiredWindowWidth=frame.width();desiredWindowHeight =frame.height();這兩個值是期望的寬高,和最終的值可能有差異。frame也就是mWinFrame它是用於記錄WMS提供的寬高,當Activity的窗口大小需要改變時,WMS會通過dispatchResized,調用Iwindow的resized函數(也就是ViewRootImpl.java的內部類W的resized函數)。如果(desiredWindowWidth!= mWidth || desiredWindowHeight !=mHeight),就是說WMS的期望值跟當前真實的寬高(mWidth,mHeight),那么變量mFullRedrawNeeded= true;mLayoutRequested= true;windowSizeMayChange =true;這三個變量在后面的流程中可能觸發invalidate,或者觸發layout請求。
performTraversals繼續往下走,如果(mFirst|| windowShouldResize|| insetsChanged ||viewVisibilityChanged ||params != null ||mForceNextWindowRelayout)條件滿足,說明窗口屬性發生了變化,會通過調用relayoutWindow,進一步調用mWindowSession.relayout,請求WMS計算窗口屬性,WMS計算的計算的結果將通過relayout的參數帶回。這些參數包括:
mWinFrame:WMS得出的應用窗口的大小,對應WMS的relayoutWindow中的outFrame。
mPendingContentInsets:WMS得出的contentinsets,對應WMS的relayoutWindow中的outContentInsets。
mPendingVisibleInsets:WMS得出的visibleinsets,對應WMS的relayoutWindow中的outVisibleInsets。
mPendingConfiguration:WMS得出的visibleinsets,對應WMS的relayoutWindow中的outConfig。
mSurface:WMS申請的有效的Surface對象,對應WMS的relayoutWindow中的outSurface。
WMS的relayoutWindow中的這些參數都是out開頭的,表明他們是出參。
WMS的relayoutWindow有關窗口大小的計算,調用流程會走到WindowSurfacePlacer.java中的applySurfaceChangesTransaction。
private void applySurfaceChangesTransaction(boolean recoveringMemory, int numDisplays,
int defaultDw, int defaultDh) @WindowSurfacePlacer.java{
int repeats = 0;
do {
repeats++;
//循環6次退出。
if (repeats > 6) {
displayContent.layoutNeeded = false;
break;
}
//重新計算config。
if (isDefaultDisplay
&& (displayContent.pendingLayoutChanges & FINISH_LAYOUT_REDO_CONFIG) != 0) {
displayContent.layoutNeeded = true;
mService.mH.sendEmptyMessage(SEND_NEW_CONFIGURATION);
}
//重新layout。
if ((displayContent.pendingLayoutChanges & FINISH_LAYOUT_REDO_LAYOUT) != 0) {
displayContent.layoutNeeded = true;
}
//循環小於4次,每次都會調用這個函數,窗口大小計算的關鍵函數。
if (repeats < LAYOUT_REPEAT_THRESHOLD) {
performLayoutLockedInner(displayContent, repeats == 1,false /* updateInputWindows */);
}
//對每個窗口應用窗口策略。
WindowState w = windows.get(i);
if (w.mHasSurface) {
mService.mPolicy.applyPostLayoutPolicyLw(w, w.mAttrs, w.mAttachedWindow);
}
// finishPostLayoutPolicyLw返回值賦給displayContent.pendingLayoutChanges ,
會影響到是退出while循環,還是根據pendingLayoutChanges 的值再次執行layout,或者重新計算configuration。
displayContent.pendingLayoutChanges |=mService.mPolicy.finishPostLayoutPolicyLw();
//這個while循環退出的另一個條件是 displayContent.pendingLayoutChanges ==0,
}while (displayContent.pendingLayoutChanges != 0);
}
看下displayContent.pendingLayoutChanges的值都有那些情況,有注釋不再翻譯了:
WindowManagerPolicy.java
/** Layout state may havechanged (soanother layout will be performed) */
static final intFINISH_LAYOUT_REDO_LAYOUT =0x0001;
/** Configuration statemay have changed */
static final intFINISH_LAYOUT_REDO_CONFIG =0x0002;
/** Wallpaper may need tomove */
static finalintFINISH_LAYOUT_REDO_WALLPAPER = 0x0004;
/** Need to recomputeanimations */
static final intFINISH_LAYOUT_REDO_ANIM =0x0008;
再次執行layout的核心代碼是performLayoutLockedInner:
final void performLayoutLockedInner(final DisplayContent displayContent,
boolean initial, boolean updateInputWindows) @WindowSurfacePlacer.java{
//所有的窗口列表。
WindowList windows = displayContent.getWindowList();
//顯示屏相關的信息。
boolean isDefaultDisplay = displayContent.isDefaultDisplay;
DisplayInfo displayInfo = displayContent.getDisplayInfo();
final int dw = displayInfo.logicalWidth;
final int dh = displayInfo.logicalHeight;
final int N = windows.size();
//循環處理每個窗口前,對顯示屏相關的變量進行初始化。
mService.mPolicy.beginLayoutLw(isDefaultDisplay, dw, dh, mService.mRotation,
mService.mCurConfiguration.uiMode);
//首先執行layout的是根窗口(沒有附加到別的窗口的那些窗口)。
int topAttached = -1;
for (i = N-1; i >= 0; i--) {
final WindowState win = windows.get(i);
//如果一個窗口不可見,就不用浪費時間去計算了。
final boolean gone = (behindDream && mService.mPolicy.canBeForceHidden(win, win.mAttrs))
|| win.isGoneForLayoutLw();
// gone表示這個窗口是不是還存在, mHaveFrame表示窗口是否已經執行過computeFrameLw,
if (!gone || !win.mHaveFrame || win.mLayoutNeeded
|| ((win.isConfigChanged() || win.setReportResizeHints())
&& !win.isGoneForLayoutLw() &&
((win.mAttrs.privateFlags & PRIVATE_FLAG_KEYGUARD) != 0 ||
(win.mHasSurface && win.mAppToken != null &&
win.mAppToken.layoutConfigChanges)))) {
if (!win.mLayoutAttached) {
//win.mLayoutAttached 為false是跟窗口,執行具體的計算。
win.prelayout();
mService.mPolicy.layoutWindowLw(win, null);
win.mLayoutSeq = seq;
}else{
//這里是子窗口的情況。
if (topAttached < 0) topAttached = i;
}
}
}
//從上一步計算的結果topAttached 開始,第一個有 attached窗口的情況,計算它的父窗口。
for (i = topAttached; i >= 0; i--) {
final WindowState win = windows.get(i);
if (win.mLayoutAttached) {
mService.mPolicy.layoutWindowLw(win, win.mAttachedWindow);
}
}
//結束layout。
mService.mPolicy.finishLayoutLw();
}
窗口的計算過程是:
這三個方法跟具體的窗口有關,手機默認的窗口策略是PhoneWindowManager.java
mService.mPolicy.beginLayoutLw(….);
mService.mPolicy.layoutWindowLw(win,win.mAttachedWindow);
mService.mPolicy.finishLayoutLw();
beginLayoutLw會做跟窗口大小相關的變量的初始化。比如:
PhoneWindowManager.java
public void beginLayoutLw(boolean isDefaultDisplay, int displayWidth, int displayHeight,
int displayRotation, int uiMode) @PhoneWindowManager.java{
//overscan是過掃描,根據屏幕的角度賦值。有些顯示屏可能存在失真現象,且越靠近邊緣越嚴重,為了避開這個問題,廠商可能把掃描調整到畫面的5%-10%,這樣造成的結果是畫面可能顯示不全,損失這部分成為overscan。
final int overscanLeft, overscanTop, overscanRight, overscanBottom;
//屏幕的真實大小,包含了overscan區域。
mOverscanScreenLeft = mRestrictedOverscanScreenLeft = 0;
mOverscanScreenTop = mRestrictedOverscanScreenTop = 0;
mOverscanScreenWidth = mRestrictedOverscanScreenWidth = displayWidth;
mOverscanScreenHeight = mRestrictedOverscanScreenHeight = displayHeight;
//所有可見的系統UI元素區域。
mSystemLeft = 0;
mSystemTop = 0;
mSystemRight = displayWidth;
mSystemBottom = displayHeight;
//屏幕的真實大小,不管狀態欄是否可見,不包含overscan區域。
mUnrestrictedScreenLeft = overscanLeft;
mUnrestrictedScreenTop = overscanTop;
mUnrestrictedScreenWidth = displayWidth - overscanLeft - overscanRight;
mUnrestrictedScreenHeight = displayHeight - overscanTop – overscanBottom;
//跟mOverscanScreen類似,適當的時候可以移動到overscan區域。
int mRestrictedOverscanScreenLeft, mRestrictedOverscanScreenTop;
int mRestrictedOverscanScreenWidth, mRestrictedOverscanScreenHeight;
//屏幕大小,如果狀態欄不隱藏,他和其他的變量是不同的。
mRestrictedScreenLeft = mUnrestrictedScreenLeft;
mRestrictedScreenTop = mUnrestrictedScreenTop;
mRestrictedScreenWidth = mSystemGestures.screenWidth = mUnrestrictedScreenWidth;
mRestrictedScreenHeight = mSystemGestures.screenHeight = mUnrestrictedScreenHeight;
//包括狀態欄、輸入法窗口在內的外圍尺寸。
int mCurLeft, mCurTop, mCurRight, mCurBottom;
//內容區域。
int mContentLeft, mContentTop, mContentRight, mContentBottom;
//輸入法窗口區域。
mDockLeft = mContentLeft = mVoiceContentLeft = mStableLeft = mStableFullscreenLeft
= mCurLeft = mUnrestrictedScreenLeft;
mDockTop = mContentTop = mVoiceContentTop = mStableTop = mStableFullscreenTop
= mCurTop = mUnrestrictedScreenTop;
mDockRight = mContentRight = mVoiceContentRight = mStableRight = mStableFullscreenRight
= mCurRight = displayWidth - overscanRight;
mDockBottom = mContentBottom = mVoiceContentBottom = mStableBottom = mStableFullscreenBottom
= mCurBottom = displayHeight – overscanBottom;
//計算導航欄的大小。
boolean updateSysUiVisibility = layoutNavigationBar(displayWidth, displayHeight,
displayRotation, uiMode, overscanLeft, overscanRight, overscanBottom, dcf, navVisible, navTranslucent,
navAllowedHidden, statusBarExpandedNotKeyguard);
//計算狀態欄的大小。
updateSysUiVisibility |= layoutStatusBar(pf, df, of, vf, dcf, sysui, isKeyguardShowing);
if (updateSysUiVisibility) {
updateSystemUiVisibilityLw();
}
}
初始化后,根據具體的配置執行layoutWindowLw,計算父窗口大小,屏幕大小,可見區域,內容區域,overscan區域,decor區域等。
public void layoutWindowLw(WindowState win, WindowState attached) @PhoneWindowManager.java{
//在beginLayout時已經處理過導航欄、狀態欄,如果狀態欄收到input事件,就需要再次layout以適應輸入法窗口。
if ((win == mStatusBar && !canReceiveInput(win)) || win == mNavigationBar) {
return;
}
final WindowManager.LayoutParams attrs = win.getAttrs();
//窗口屬性。
final int fl = PolicyControl.getWindowFlags(win, attrs);
final int sysUiFl = PolicyControl.getSystemUiVisibility(win, null);
//主要計算就是下面這幾個變量。
final Rect pf = mTmpParentFrame;
final Rect df = mTmpDisplayFrame;
final Rect of = mTmpOverscanFrame;
final Rect cf = mTmpContentFrame;
final Rect vf = mTmpVisibleFrame;
final Rect dcf = mTmpDecorFrame;
final Rect sf = mTmpStableFrame;
//是否有導航條,手機底部有物理按鍵的,通常就不用導航條了。
final boolean hasNavBar = (isDefaultDisplay && mHasNavigationBar
&& mNavigationBar != null && mNavigationBar.isVisibleLw());
只關注應用窗口的計算,其他窗口如輸入法、狀態欄、wallpaper等略過。
final boolean isAppWindow =
attrs.type >= WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW &&
attrs.type <= WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
if (isAppWindow && !inheritTranslucentDecor && !topAtRest) {
//設置了全屏,確保decorView包含狀態欄。
if ((sysUiFl & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0
&& (fl & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0
&& (fl & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) == 0
&& (fl & WindowManager.LayoutParams.
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0
&& (pfl & PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND) == 0) {
// Ensure policy decor includes status bar
dcf.top = mStableTop;
}
//設置了導航欄透明,確保DecorView包含導航欄。
if ((fl & WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) == 0
&& (sysUiFl & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0
&& (fl & WindowManager.LayoutParams.
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0) {
// Ensure policy decor includes navigation bar
dcf.bottom = mStableBottom;
dcf.right = mStableRight;
}
}
//這是常見的Activity窗口的情況,想要覆蓋所有的屏幕空間,要移動窗口的內容以適應屏幕裝飾條在內的系統元素。
if ((fl & (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR))
== (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)) {
//如果是子窗口,各個變量的值跟其父窗口一致
if (attached != null) {
setAttachedWindowFrames(win, fl, adjust, attached, true, pf, df, of, cf, vf);
}else{ //非子窗口的情況
//狀態欄的下拉面板,這是唯一可以在狀態欄之上的窗口,他們受STATUS_BAR_SERVICE權限保護,跟狀態欄有相同的優先級。
if (attrs.type == TYPE_STATUS_BAR_PANEL
|| attrs.type == TYPE_STATUS_BAR_SUB_PANEL) {
pf.left = df.left = of.left = hasNavBar
? mDockLeft : mUnrestrictedScreenLeft;
pf.top = df.top = of.top = mUnrestrictedScreenTop;
pf.right = df.right = of.right = hasNavBar
? mRestrictedScreenLeft+mRestrictedScreenWidth
: mUnrestrictedScreenLeft + mUnrestrictedScreenWidth;
pf.bottom = df.bottom = of.bottom = hasNavBar
? mRestrictedScreenTop+mRestrictedScreenHeight
: mUnrestrictedScreenTop + mUnrestrictedScreenHeight;
}
}else if ((fl & FLAG_LAYOUT_IN_OVERSCAN) != 0
&& attrs.type >= WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW
&& attrs.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
//要求占據overscan區域,所以給定的是真正不受限制的區域。
pf.left = df.left = of.left = mOverscanScreenLeft;
pf.top = df.top = of.top = mOverscanScreenTop;
pf.right = df.right = of.right = mOverscanScreenLeft + mOverscanScreenWidth;
pf.bottom = df.bottom = of.bottom = mOverscanScreenTop
+ mOverscanScreenHeight;
}else if (canHideNavigationBar()
&& (sysUiFl & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0
&& attrs.type >= WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW
&& attrs.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
//假設導航條是隱藏的,讓應用窗口可以布局到無限制的overscan區域。只對應用窗口這么做,是要確保沒有別的窗口能在導航條之上。
pf.left = df.left = mOverscanScreenLeft;
pf.top = df.top = mOverscanScreenTop;
pf.right = df.right = mOverscanScreenLeft + mOverscanScreenWidth;
pf.bottom = df.bottom = mOverscanScreenTop + mOverscanScreenHeight;
//我們需要告知應用程序過掃描的內側在哪里,以便根據這個值嵌入它的內容,而不至於實際的超出overscan的范圍。
of.left = mUnrestrictedScreenLeft;
of.top = mUnrestrictedScreenTop;
of.right = mUnrestrictedScreenLeft + mUnrestrictedScreenWidth;
of.bottom = mUnrestrictedScreenTop + mUnrestrictedScreenHeight;
}else{
//然后是其他情況。
}
}else if ((fl & FLAG_LAYOUT_IN_SCREEN) != 0 || (sysUiFl
& (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)) != 0) {
//窗口希望全屏顯示,可能是有系統UI的全屏,也可能是沒有系統UI的全屏。
}else if (attached != null) {
//子窗口的情況。
setAttachedWindowFrames(win, fl, adjust, attached, false, pf, df, of, cf, vf);
}else{
//其他的窗口類型,如:TYPE_STATUS_BAR_PANEL,TYPE_VOLUME_OVERLAY,TYPE_TOAST,TYPE_SYSTEM_ALERT等。
}
// pf, df, of, cf, vf, dcf, sf, osf這些都是layoutWindowLw函數中的臨時變量,最終還有通過windowState的 computeFrameLw的確認。
win.computeFrameLw(pf, df, of, cf, vf, dcf, sf, osf);
}
layoutWindowLw中的pf,df, of, cf, vf, dcf, sf, osf這些都是臨時變量,最終還有通過windowState的computeFrameLw的確認。也就是分別設置到WindowState中的變量mOverscanFrame,mContentFrame,mVisibleFrame,mDecorFrame,mStableFrame中,最后把WindowState中的這些變量在relayoutWindow(WindowManagerService.java)函數中通過outFrame.set(win.mCompatFrame);outOverscanInsets.set(win.mOverscanInsets);outContentInsets.set(win.mContentInsets);outVisibleInsets.set(win.mVisibleInsets);outStableInsets.set(win.mStableInsets);outOutsets.set(win.mOutsets);這些出參告知ViewRootImpl。