android6.0 SystemUi分析
http://www.jianshu.com/p/28f1954812b3
前言
狀態欄與導航欄屬於SystemUi的管理范疇,雖然界面的UI會受到SystemUi的影響,但是,APP並沒有直接繪制SystemUI的權限與必要。APP端之所以能夠更改狀態欄的顏色、導航欄的顏色,其實還是操作自己的View更改UI。可以這么理解:狀態欄與導航欄擁有自己獨立的窗口,而且這兩個窗口的優先級較高,會懸浮在所有窗口之上,可以把系統自身的狀態欄與導航欄看做全透明的,之所以會有背景顏色,是因為下層顯示界面在被覆蓋的區域添加了顏色,之后,通過SurfaceFlinger的圖層混合,好像是狀態欄、導航欄自身有了背景色。看一下一個普通的Activity展示的時候,所對應的Surface(或者說Window也可以)。如下Surface圖:
- 第一個XXXXActivity,大小是屏幕大小
- 第二個狀態欄StatusBar,大小對應頂部那一條
- 第三個是底部虛擬導航欄NavigationBar,大小對應底部那一條
- HWC_FRAMEBUFFER_TARGET:是合成的目標Layer,不參與合成
從上表可以看出,雖然只展示了一個Activity,但是同時會有StatusBar、NavigationBar、XXXXActivity可以看出Activity是在狀態欄與導航欄下面的,被覆蓋了,它們共同參與顯示界面的合成,但是,StatusBar、NavigationBar明顯不是屬於APP自身UI管理的范疇。下面就來分析一下,APP層的API如何影響SystemUI的顯示的,並一步步解開所謂沉浸式與全屏的原理,首先看一下如何更改狀態欄顏色。
查看布局
tool->android->layout inspector
WindowInsets介紹
https://blog.csdn.net/yuanjw2014/article/details/78363353
https://www.jianshu.com/p/756e94fa2e09
inset的直譯是插入物,理解為特定屏幕區域更合適一些。WindowInsets的三個成員變量mSystemWindowInsets,mWindowDecorInsets,mStableInsets表示了三種屏幕區域。
- mSystemWindowInsets
The system window inset represents the area of a full-screen window that is partially or fully obscured by the status bar, navigation bar, IME or other system windows.
代表着整個屏幕窗口上,狀態欄,導航欄,輸入法等系統窗口占用的區域
- mWindowDecorInsets
The window decor inset represents the area of the window content area that is partially or fully obscured by decorations within the window provided by the framework. This can include action bars, title bars, toolbars, etc.
代表着內容區域被系統框架提供的action bars, title bars, toolbars這些組件占用的區域。
- mStableInsets
The stable inset represents the area of a full-screen window that may be partially or fully obscured(被遮蔽的) by the system UI elements. This value does not change based on the visibility state of those system UI elements; for example, if the status bar is normally shown, but temporarily hidden, the stable inset will still provide the inset associated with the status bar being shown.
SystemBar setColor支持
Android 5.0之前activity默認是在statusbar下邊,navigationbar上邊,
而在Android5.0開始,activity真正的全屏,只不過內容布局還會空出statusbar,navigationbar空間(除非設置了SYSTEM_UI_FLAG_xx),statusbar和navigationbar處加入了有顏色的view。
Android在API 21的時候為Window添加了setNavigationBarColor、setStatusBarColor,進一步提升SystemBar用戶體驗。
PhoneWindow繼承Window具體實現了setNavigationBarColor、setStatusBarColor,具體代碼如下:
public void setStatusBarColor(int color) { mStatusBarColor = color; mForcedStatusBarColor = true; if (mDecor != null) { mDecor.updateColorViews(null, false /* animate */); } } public void setNavigationBarColor(int color) { mNavigationBarColor = color; mForcedNavigationBarColor = true; if (mDecor != null) { mDecor.updateColorViews(null, false /* animate */); mDecor.updateNavigationGuardColor(); }} }
不難發現主要是DecorView的updateColorViews在work,通過查看代碼,可以明白是DecorView在SystemBar的位置add了對應的ColorStateView,這個有點類似PhoneWindowManager里邊的WindowState,之后對ColotStateView里邊的view進行操作即可,比如說setBackground來改變其顏色。
狀態欄顏色更新原理
假設當前的場景是默認樣式的Activity,如果想要更新狀態欄顏色只需要如下代碼:
getWindow().setStatusBarColor(RED);
其實這里調用的是PhoneWindow的setStatusBarColor函數,無論是Activity還是Dialog都是被抽象成PhoneWindow:
@Overrider public void setStatusBarColor(int color) { mStatusBarColor = color; mForcedStatusBarColor = true; if (mDecor != null) { mDecor.updateColorViews(null, false /* animate */); } }
最終調用的是DecorView的updateColorViews函數,DecorView是屬於Activity的PhoneWindow的內部對象,也就說,更新的對象從所謂的Window進入到了Activity自身的布局視圖中,接着看DecorView,這里只關注更改顏色:
private WindowInsets updateColorViews(WindowInsets insets, boolean animate) { WindowManager.LayoutParams attrs = getAttributes(); int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility(); if (!mIsFloating && ActivityManager.isHighEndGfx()) { boolean disallowAnimate = !isLaidOut(); disallowAnimate |= ((mLastWindowFlags ^ attrs.flags) & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; mLastWindowFlags = attrs.flags; ... boolean navBarToRightEdge = isNavBarToRightEdge(mLastBottomInset, mLastRightInset); boolean navBarToLeftEdge = isNavBarToLeftEdge(mLastBottomInset, mLastLeftInset); int navBarSize = getNavBarSize(mLastBottomInset, mLastRightInset, mLastLeftInset); <!--更新NavigatioColor--> updateColorViewInt(mNavigationColorViewState, sysUiVisibility, mWindow.mNavigationBarColor, navBarSize, navBarToRightEdge || navBarToLeftEdge, navBarToLeftEdge, 0 /* sideInset */, animate && !disallowAnimate, false /* force */); boolean statusBarNeedsRightInset = navBarToRightEdge && mNavigationColorViewState.present; int statusBarRightInset = statusBarNeedsRightInset ? mLastRightInset : 0; <!--更新StatusColor--> updateColorViewInt(mStatusColorViewState, sysUiVisibility, mStatusBarColor, mLastTopInset, false /* matchVertical */, statusBarRightInset, animate && !disallowAnimate); } ... }
這里mStatusColorViewState其實就代表StatusBar的背景顏色對象,主要屬性包括顯示的條件以及顏色值:
private final ColorViewState mStatusColorViewState = new ColorViewState( SYSTEM_UI_FLAG_FULLSCREEN, FLAG_TRANSLUCENT_STATUS, Gravity.TOP, Gravity.LEFT, STATUS_BAR_BACKGROUND_TRANSITION_NAME, com.android.internal.R.id.statusBarBackground, FLAG_FULLSCREEN); 構造函數: ColorViewState(int systemUiHideFlag, int translucentFlag, int verticalGravity, int horizontalGravity, String transitionName, int id, int hideWindowFlag)
如果當前對應Window的SystemUi(下邊的sysUiVis)設置了SYSTEM_UI_FLAG_FULLSCREEN后,就會隱藏狀態欄,那就不需要為狀態欄設置背景,否則就設置背景。
private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color, int size, boolean verticalBar, int rightMargin, boolean animate) { <!--關鍵點1 條件1--> state.present = size > 0 && (sysUiVis & state.systemUiHideFlag) == 0 && (getAttributes().flags & state.hideWindowFlag) == 0 && (getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; <!--關鍵點2 條件2--> boolean show = state.present && (color & Color.BLACK) != 0 && (getAttributes().flags & state.translucentFlag) == 0; boolean visibilityChanged = false; View view = state.view; int resolvedHeight = verticalBar ? LayoutParams.MATCH_PARENT : size; int resolvedWidth = verticalBar ? size : LayoutParams.MATCH_PARENT; int resolvedGravity = verticalBar ? state.horizontalGravity : state.verticalGravity; if (view == null) { if (show) { state.view = view = new View(mContext); view.setBackgroundColor(color); view.setTransitionName(state.transitionName); view.setId(state.id); visibilityChanged = true; view.setVisibility(INVISIBLE); state.targetVisibility = VISIBLE; <!--關鍵點3--> LayoutParams lp = new LayoutParams(resolvedWidth, resolvedHeight, resolvedGravity); lp.rightMargin = rightMargin; addView(view, lp); updateColorViewTranslations(); } } ... }
先看下關鍵點1跟2 ,這里是根據SystemUI的配置決定是否顯示狀態欄背景顏色,
1.如果狀態欄都不顯示,那就沒必要顯示背景色了,
2.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
指示此窗口要負責進行system bar的繪制,繪制顏色是由Window.getStatusBarColor()/getNavigationBarColor()獲取。
3.其次,如果狀態欄顯示,但背景是透明色,也沒必要添加背景顏色,即不滿足(color & Color.BLACK) != 0。
4.最后看一下translucentFlag,默認情況下,狀態欄背景色與translucent半透明效果互斥,半透明就統一用半透明顏色,不會再添加額外顏色。
最后,再來看關鍵點3,其實很簡單,就是往DecorView上添加一個View,原則上說DecorView也是一個FrameLayout,所以最終的實現就是在FrameLayout添加一個有背景色的View。
導航欄顏色更新原理
更新導航欄顏色的原理同更新狀態欄的原理幾乎完全一致,如下代碼
@Override public void setNavigationBarColor(int color) { mNavigationBarColor = color; mForcedNavigationBarColor = true; if (mDecor != null) { mDecor.updateColorViews(null, false /* animate */); } }
只不過在DecorView進行顏色更新的時候,傳遞的對象是 mNavigationColorViewState
private final ColorViewState mNavigationColorViewState = new ColorViewState( SYSTEM_UI_FLAG_HIDE_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION, Gravity.BOTTOM, Gravity.RIGHT, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME, com.android.internal.R.id.navigationBarBackground, 0 /* hideWindowFlag */); 構造函數: ColorViewState(int systemUiHideFlag, int translucentFlag, int verticalGravity, int horizontalGravity, String transitionName, int id, int hideWindowFlag)
同樣mNavigationColorViewState也有顯示的條件,如果設置了SYSTEM_UI_FLAG_HIDE_NAVIGATION、或者半透明、或者顏色為透明色,那同樣也不需要為導航欄添加背景色,具體不再重復。改變狀體欄及導航欄的顏色的本質是往DecorView中添加有顏色的View, 並放在狀態欄及導航欄下面。
當然,如果設置了隱藏狀態欄,或者導航欄,並且沒有讓布局隨着隱藏而動態變化的話,就會看到被覆蓋的padding,默認是白色,如下圖,隱藏狀態欄前后的對比:
沒隱藏狀態欄
隱藏了狀態欄
以上是DecorView對狀態欄的添加機制,總結出來就是一句話:只要狀態欄/導航欄不設置隱藏,設置顏色就會有效。實際應用中經常將狀態欄或者導航欄設置為透明色:即想要沉浸式體驗,這個時候背景顏色View就不再被繪制。
但是,默認樣式下DecorView的 內容繪制區域 並未擴展到狀態欄、或者導航欄下面(TRANSLUCENT半透明效果除外(5.0之上,一般不會有TRANSLUCENT功能)),結果就是會看到被覆蓋區域的一篇空白。想要解決這個問題,就牽扯到下面的fitsystemwindow的處理。
DecorView內容區域的擴展與fitsystemwindow的意義
fitSystemWindow屬性 當DecorView的內容區域延伸到系統UI下方時,防止在擴展時被覆蓋,達到全屏、沉浸等不同體驗效果。這里牽扯到WindowInsets的消費,其實就是周圍一些系統的邊框padding的消耗,它分成不同的消耗層級:
- DecorView層級的消費 :主要針對NavigationBar部分
- DecorView根布局消費(非用戶布局)
- 用戶布局消費
消費層級的選擇是可控的,使用得當,就能在不同的場景得到想要的樣式。接下來分析下不同層級控制與消費的原理。
DecorView級別的WindowInsets消費
看下ViewRootImpl的源碼,在ViewRootImpl進行布局與繪制的時候會選擇性調用dispatchApplyInsets,這個函數的作用是找到符合要求的View,消費掉WindowInsets:
ViewRootImpl:
private void performTraversals() { ... dispatchApplyInsets(host); ... } <!--關鍵點1--> void dispatchApplyInsets(View host) { host.dispatchApplyWindowInsets(getWindowInsets(true /* forceConstruct */)); }
host其實就是DecorView對象
/* package */ WindowInsets getWindowInsets(boolean forceConstruct) { if (mLastWindowInsets == null || forceConstruct) { mDispatchContentInsets.set(mAttachInfo.mContentInsets); mDispatchStableInsets.set(mAttachInfo.mStableInsets); Rect contentInsets = mDispatchContentInsets; Rect stableInsets = mDispatchStableInsets; // For dispatch we preserve old logic, but for direct requests from Views we allow to // immediately use pending insets. if (!forceConstruct && (!mPendingContentInsets.equals(contentInsets) || !mPendingStableInsets.equals(stableInsets))) { contentInsets = mPendingContentInsets; stableInsets = mPendingStableInsets; } Rect outsets = mAttachInfo.mOutsets; if (outsets.left > 0 || outsets.top > 0 || outsets.right > 0 || outsets.bottom > 0) { contentInsets = new Rect(contentInsets.left + outsets.left, contentInsets.top + outsets.top, contentInsets.right + outsets.right, contentInsets.bottom + outsets.bottom); } mLastWindowInsets = new WindowInsets(contentInsets, null /* windowDecorInsets */, stableInsets, mContext.getResources().getConfiguration().isScreenRound(), mAttachInfo.mAlwaysConsumeNavBar); } return mLastWindowInsets; } public WindowInsets(Rect systemWindowInsets, Rect windowDecorInsets, Rect stableInsets, boolean isRound, boolean alwaysConsumeNavBar)
ViewGroup:
@Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { insets = super.dispatchApplyWindowInsets(insets); if (!insets.isConsumed()) { final int count = getChildCount(); for (int i = 0; i < count; i++) { insets = getChildAt(i).dispatchApplyWindowInsets(insets); if (insets.isConsumed()) { break; } } } return insets; }
先自己消費,之后會把剩余的交給子view消費。
View:
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { try { mPrivateFlags3 |= PFLAG3_APPLYING_INSETS; if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) { return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets); } else { return onApplyWindowInsets(insets); } } finally { mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS; } }
DecorView最終會回調View的onApplyWindowInsets函數,不過DecorView重寫了該函數:
DecorView:
@Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { final WindowManager.LayoutParams attrs = mWindow.getAttributes(); ... mFrameOffsets.set(insets.getSystemWindowInsets()); <!--關鍵點1--> insets = updateColorViews(insets, true /* animate */); insets = updateStatusGuard(insets); updateNavigationGuard(insets); if (getForeground() != null) { drawableChanged(); } return insets; }
關鍵是調用updateColorViews函數,之前看過對顏色的處理,這里我們主要看下對於邊距的處理:
DecorView:
private WindowInsets updateColorViews(WindowInsets insets, boolean animate) { WindowManager.LayoutParams attrs = getAttributes(); int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility(); if (!mIsFloating && ActivityManager.isHighEndGfx()) { ...//設置statusbar和navigationbar顏色view } <!--關鍵點1 :6.0代碼 判斷是否能夠擴展到導航欄下面--> boolean consumingNavBar = (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0 && (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; int consumedRight = consumingNavBar ? mLastRightInset : 0; int consumedBottom = consumingNavBar ? mLastBottomInset : 0; <!--關鍵點1 ,可以看到,根布局會根據消耗的狀況,來評估到底底部,右邊部分margin多少,並設置進去--> if (mContentRoot != null && mContentRoot.getLayoutParams() instanceof MarginLayoutParams) { MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams(); if (lp.rightMargin != consumedRight || lp.bottomMargin != consumedBottom) { lp.rightMargin = consumedRight; lp.bottomMargin = consumedBottom; mContentRoot.setLayoutParams(lp); ... } <!--關鍵點2 重新計算消費結果----> if (insets != null) { insets = insets.replaceSystemWindowInsets( insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight() - consumedRight, insets.getSystemWindowInsetBottom() - consumedBottom); } } if (insets != null) { insets = insets.consumeStableInsets(); } return insets; }
mContentRoot是DecorView的直接子view,就是個linearlayout。
在6.0對應的源碼中,DecorView自身主要對NavigationBar那部分的Insets做了處理,並沒有對狀態欄(消費)做處理。
並且DecorView通過設置Margin的方式來處理Insets的消費的:mContentRoot.setLayoutParams(lp);
這里主要關心下consumingNavBar的條件,什么情況下DecorView會通過設置Margin來消費掉導航欄那部分Padding,
主要有三個條件:
- sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION == 0 不設置隱藏導航欄
- sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION == 0,導航欄顯示時,內容不能擴展到導航欄下方
- (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 不使用系統背景
同時滿足以上三點,Insets的bottom部分就會被DecorView利用Margin的方式消費掉,默認樣式的Activity滿足上述三個條件,因此,底部導航欄部分Insets默認被DecorView消費掉了,如下圖:
非懸浮Activity的DecorView默認是全屏的,圖中1、2代表着DecorView中添加狀體欄、導航欄對應的顏色View,而DecorView的Content是一個LinearLayout,可以看出它並不是全屏,而是底部有一個Margin,正好對應導航欄的高度,頂部有個padding這個其實是由fitSystemWindow決定的。
系統布局級別(非DecorView)的fitSystemWindow消費
默認樣式Activity的狀態欄是有顏色的,如果內容直接擴展到狀態欄下方,一定會被覆蓋掉,系統默認的實現是在DecorView的根布局上加了個padding,那么用戶的UI視圖就不會被覆蓋。不過,如果狀態欄被設置為透明,用戶就會看到狀態欄下方有一片空白,這種體驗肯定不好。這種情況下,往往希望內容能夠延伸到狀體欄下方,因此,就需要把空白的也留給內容視圖。
首先,分析下,默認樣式的Activity為什么會有頂部的空白,看下一默認情況下系統的根布局屬性,里面有我們要找的關鍵點 android:fitsSystemWindows="true":
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" <!--關鍵點1--> android:fitsSystemWindows="true" android:orientation="vertical"> <ViewStub android:id="@+id/action_mode_bar_stub" android:inflatedId="@+id/action_mode_bar" android:layout="@layout/action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="?attr/actionBarTheme" /> <FrameLayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:foregroundInsidePadding="false" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" /> </LinearLayout>
上面的布局是DecorView的直接子view,在DecorView中叫mContentRoot,其中關鍵點1:android:fitsSystemWindows屬性是系統添加狀態欄padding的關鍵,為什么這樣呢?
由上邊decorview對navigationbar的消費可知,如果想要讓內容布局mContentRoot進行消費,那么需要設置SYSTEM_UI_FLAG_HIDE_NAVIGATION,就意味着DecorView沒有消耗SystemWindowInsets(主要是bottom,即導航欄高度),mContentRoot的fitsystemwindow就會生效,並通過設置padding消費掉,這里就是系統布局級別的消費(不是用戶自己定義的View布局),設置代碼,
setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
View.SYSTEM_UI_FLAG_LAYOUT_STABLE為了保證內容布局不隨着導航欄的消失而把內容擴展到導航欄位置,
效果如下圖:
上圖中由於設置了SYSTEM_UI_FLAG_HIDE_NAVIGATION,所以沒有導航欄View被添加,DecorView中只有狀態欄背景1 .View與根內容布局,從圖中的點2可以看出,這里是通過設置mContentRoot的padding來處理Insets消費的(同時消費了狀態欄與導航欄部分)。但是,不管何種方式,消費了就是消費了,被消費的部分不能再次消費。
6.0源碼中,DecorView並沒有對狀態欄進行消費,狀態欄的消費都留給了DecorView子布局及孫子輩布局,不過7.0在系統級別的配置上留了個入口(ForceWindowDrawsStatusBarBackground)。
接着上邊DecorView消費完后會把WindowInsets 傳遞給子view進行處理。
ViewGroup:
@Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { insets = super.dispatchApplyWindowInsets(insets); if (!insets.isConsumed()) { final int count = getChildCount(); for (int i = 0; i < count; i++) { insets = getChildAt(i).dispatchApplyWindowInsets(insets); if (insets.isConsumed()) { break; } } } return insets; }
先自己消費,之后會把剩余的交給子view消費。
View:
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { try { mPrivateFlags3 |= PFLAG3_APPLYING_INSETS; if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) { return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets); } else { return onApplyWindowInsets(insets); } } finally { mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS; } } public WindowInsets onApplyWindowInsets(WindowInsets insets) { if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) { // We weren't called from within a direct call to fitSystemWindows, // call into it as a fallback in case we're in a class that overrides it // and has logic to perform. if (fitSystemWindows(insets.getSystemWindowInsets())) { // 如果能消費,則全都消費完。 return insets.consumeSystemWindowInsets(); } } else { // We were called from within a direct call to fitSystemWindows. if (fitSystemWindowsInt(insets.getSystemWindowInsets())) { return insets.consumeSystemWindowInsets(); } } return insets; } protected boolean fitSystemWindows(Rect insets) { if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) { if (insets == null) { // Null insets by definition have already been consumed. // This call cannot apply insets since there are none to apply, // so return false. return false; } // If we're not in the process of dispatching the newer apply insets call, // that means we're not in the compatibility path. Dispatch into the newer // apply insets path and take things from there. try { mPrivateFlags3 |= PFLAG3_FITTING_SYSTEM_WINDOWS; return dispatchApplyWindowInsets(new WindowInsets(insets)).isConsumed(); } finally { mPrivateFlags3 &= ~PFLAG3_FITTING_SYSTEM_WINDOWS; } } else { // We're being called from the newer apply insets path. // Perform the standard fallback behavior. <!--關鍵函數--> return fitSystemWindowsInt(insets); } }
fitSystemWindowsInt是最為關鍵的消費處理函數,里面有當前View能否消費WindowInsets的判斷邏輯。
View:
private boolean fitSystemWindowsInt(Rect insets) { <!--關鍵點1--> if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) { mUserPaddingStart = UNDEFINED_PADDING; mUserPaddingEnd = UNDEFINED_PADDING; Rect localInsets = sThreadLocal.get(); if (localInsets == null) { localInsets = new Rect(); sThreadLocal.set(localInsets); } <!--關鍵點2--> boolean res = computeFitSystemWindows(insets, localInsets); mUserPaddingLeftInitial = localInsets.left; mUserPaddingRightInitial = localInsets.right; internalSetPadding(localInsets.left, localInsets.top, localInsets.right, localInsets.bottom); return res; } return false; }
先看關鍵點1,如果View設置了FITS_SYSTEM_WINDOWS,就通過關鍵點2 的computeFitSystemWindows去計算是否能消費,
接着看computeFitSystemWindows
protected boolean computeFitSystemWindows(Rect inoutInsets, Rect outLocalInsets) { if ((mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) == 0 || mAttachInfo == null || ((mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS) == 0 && !mAttachInfo.mOverscanRequested)) { outLocalInsets.set(inoutInsets); inoutInsets.set(0, 0, 0, 0); return true; } ... }
- (mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) == 0 代表是用戶的UI(contentView),因為OPTIONAL_FITS_SYSTEM_WINDOWS只有除了contentView之外的view才會設置,而這些view是系統預定義的view,下邊有解析。
- 如果是普通View可以直接消費,如果是系統View,要看看是不是設置了SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION、 SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS == 0 代表沒有設置SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION、 SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN的參數,
如果設置了任意一個flag,就只能讓用戶View去消費,正如之前decorview布局simple_screen.xml布局,雖然根布局設置了fitSystemWindow為true,但是,如果你用了全屏參數,根布局的fitSystemWindow就會無效,
如果上面都沒有消費,就會轉換為用戶布局級別的消費。
View.internalSetPadding:
protected void internalSetPadding(int left, int top, int right, int bottom) { mUserPaddingLeft = left; mUserPaddingRight = right; mUserPaddingBottom = bottom; boolean changed = false; ... 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(); } }
OPTIONAL_FITS_SYSTEM_WINDOWS設置
OPTIONAL_FITS_SYSTEM_WINDOWS是通過 makeOptionalFitsSystemWindows設置的,入口只在PhoneWindow中,
通過mDecor.makeOptionalFitsSystemWindows()設置:
@Override public void setContentView(View view, ViewGroup.LayoutParams params) { // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { view.setLayoutParams(params); final Scene newScene = new Scene(mContentParent, view); transitionTo(newScene); } else { mContentParent.addView(view, params); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true; } private void installDecor() { mForceDecorInstall = false; if (mDecor == null) { mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } else { // 設置Window mDecor.setWindow(this); } if (mContentParent == null) { mContentParent = generateLayout(mDecor); <!--關鍵點1--> mDecor.makeOptionalFitsSystemWindows(); ... } }
在installDecor()中,mDecor.makeOptionalFitsSystemWindows的時候,里面還未涉及用戶view,所以標記的都是系統自己的View布局
ViewGroup:
public void makeOptionalFitsSystemWindows() { super.makeOptionalFitsSystemWindows(); final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { children[i].makeOptionalFitsSystemWindows(); } }
View:
public void makeOptionalFitsSystemWindows() { setFlags(OPTIONAL_FITS_SYSTEM_WINDOWS, OPTIONAL_FITS_SYSTEM_WINDOWS); }
用戶布局級別的fitSystemWindow消費
想要用戶布局消費則需要讓系統布局mContentRoot不消費才行。
可以在上邊看到,設置了SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION、 SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN這其中一個就可以讓mContentRoot不消費。
如果用戶布局中設置了fitSystemWindow="true",那么消費邏輯跟系統布局mContentRoot消費邏輯是一樣的,所以就不再分析。
如果想要實現全屏效果的話,假設圖片瀏覽的場景:全屏,導航欄與狀態欄透明,圖片瀏覽區伸展到整個屏幕,通過設置下面的配置就能達到效果:全屏,並且用戶布局與系統布局都不消費WindowInsets:
getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getWindow().setStatusBarColor(Color.TRANSPARENT); getWindow().setNavigationBarColor(Color.TRANSPARENT); }
- 由於StatusBarColor和NavigationBarColor都設置的是透明的,所以狀態欄與導航欄背景色View都沒有被添加,
- 其次,由於設置了View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,DecorView與 系統布局mContentRoot 都不會消費WindowInsets,
- 而在用戶自己的布局中也沒有設置 android:fitsSystemWindows="true"的話,這樣不會有View消費WindowInsets,達到全屏效果。
如下圖所示:
有一個小點需要注意下,那就是Theme中也支持fitsSystemWindows的設置
<item name="android:fitsSystemWindows">true</item>
默認情況下上屬性為false,如果設置了True,就會被第一個未設置fitsSystemWindows的View消費掉。
遵守View默認的消費邏輯,被第一個FitSystemWindow=true的布局通過設置自己padding的方式消費掉。
setSystemUiVisibility流程
View
public void setSystemUiVisibility(int visibility) { if (visibility != mSystemUiVisibility) { mSystemUiVisibility = visibility; if (mParent != null && mAttachInfo != null && !mAttachInfo.mRecomputeGlobalAttributes) { mParent.recomputeViewAttributes(this); } } }
ViewGrou
@Override public void recomputeViewAttributes(View child) { if (mAttachInfo != null && !mAttachInfo.mRecomputeGlobalAttributes) { ViewParent parent = mParent; if (parent != null) parent.recomputeViewAttributes(this); } }
ViewRootImpl
Override public void recomputeViewAttributes(View child) { checkThread(); if (mView == child) { mAttachInfo.mRecomputeGlobalAttributes = true; if (!mWillDrawSoon) { scheduleTraversals(); } } }
ViewRootImpl
在performTraversals和setView時都會調用collectViewAttributes來收集一下子孫view設置的setSystemUiVisibility
private boolean collectViewAttributes() { if (mAttachInfo.mRecomputeGlobalAttributes) { //Log.i(mTag, "Computing view hierarchy attributes!"); mAttachInfo.mRecomputeGlobalAttributes = false; boolean oldScreenOn = mAttachInfo.mKeepScreenOn; mAttachInfo.mKeepScreenOn = false; mAttachInfo.mSystemUiVisibility = 0; mAttachInfo.mHasSystemUiListeners = false; mView.dispatchCollectViewAttributes(mAttachInfo, 0); mAttachInfo.mSystemUiVisibility &= ~mAttachInfo.mDisabledSystemUiVisibility; WindowManager.LayoutParams params = mWindowAttributes; mAttachInfo.mSystemUiVisibility |= getImpliedSystemUiVisibility(params); if (mAttachInfo.mKeepScreenOn != oldScreenOn || mAttachInfo.mSystemUiVisibility != params.subtreeSystemUiVisibility || mAttachInfo.mHasSystemUiListeners != params.hasSystemUiListeners) { applyKeepScreenOnFlag(params); params.subtreeSystemUiVisibility = mAttachInfo.mSystemUiVisibility; params.hasSystemUiListeners = mAttachInfo.mHasSystemUiListeners; mView.dispatchWindowSystemUiVisiblityChanged(mAttachInfo.mSystemUiVisibility); return true; } } return false; }
如何獲取需要消費的WindowInsets
前面說的消費的WindowInsets 是怎么來的呢?其實是ViewRootImpl在relayout的時候請求WMS進行計算出來的,計算成功后保存到mAttachInfo中,並不為APP所控制。這里的contentInsets作為systemInsets
ViewRootImpl.java
int relayoutResult = mWindowSession.relayout( mWindow, mSeq, params, (int) (mView.getMeasuredWidth() * appScale + 0.5f), (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets, mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingConfiguration, mSurface);
WindowManagerService.java
public int relayoutWindow(Session session, IWindow client, int seq, WindowManager.LayoutParams attrs, int requestedWidth, int requestedHeight, int viewVisibility, int flags, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets, Rect outVisibleInsets, Rect outStableInsets, Rect outOutsets, Rect outBackdropFrame, Configuration outConfig, Surface outSurface) {
最終通過WindowManagerService獲取對應的Insets,其實是存在WindowState中的。這里不再深入,有興趣自己學習。
為何windowTranslucentStatus與statusBarColor不能同時生效
Android4.4的時候,加了個windowTranslucentStatus屬性,實現了狀態欄導航欄半透明效果,而Android5.0之后以上狀態欄、導航欄支持顏色隨意設定,所以,5.0之后一般不需要使用windowTranslucentStatus,而且設置狀態欄顏色與windowTranslucentStatus是互斥的。所以,默認情況下android:windowTranslucentStatus是false。也就是說:'windowTranslucentStatus'和'windowTranslucentNavigation'設置為true后就再設置'statusBarColor'和'navigationBarColor'就沒有效果了。。
原因是在decorview添加狀態欄view時有如下判斷:
boolean show = state.present && (color & Color.BLACK) != 0 && ((mWindow.getAttributes().flags & state.translucentFlag) == 0 || force);
可以看到,添加背景View有一個必要條件
(mWindow.getAttributes().flags & state.translucentFlag) == 0
也就是說一旦設置了
<item name="android:windowTranslucentStatus">true</item> <item name="android:windowTranslucentNavigation">true</item>
相應的狀態欄或者導航欄的顏色設置就不在生效。不過它並不影響fitSystemWindow的邏輯。
總結
- 狀態欄與導航欄顏色的設置與其顯示隱藏有關系,一旦隱藏,設置顏色就無效,並且顏色是通過向DecorView根布局addView的方式來實現的。
- 默認樣式下DecorView消費導航欄,利用其內部Content的Margin來實現
- fitsysytemwindow與UI的content的擴展有關系,如果設置了全屏之類的屬性,WindowsInsets一定留給子View消費
- Translucent與設置顏色互斥,但是與fitSystemWindow不互斥
- 設置顏色與擴展布局是不互斥的兩種操作
- fitSystemWindow只會通過padding方式來消費WindowInsets