android6.0 SystemUi分析

android6.0 SystemUi分析

http://www.jianshu.com/p/28f1954812b3java

前言

狀態欄與導航欄屬於SystemUi的管理範疇,雖然界面的UI會受到SystemUi的影響,可是,APP並無直接繪製SystemUI的權限與必要。APP端之因此可以更改狀態欄的顏色、導航欄的顏色,其實仍是操做本身的View更改UI。能夠這麼理解:狀態欄與導航欄擁有本身獨立的窗口,並且這兩個窗口的優先級較高,會懸浮在全部窗口之上,能夠把系統自身的狀態欄與導航欄看作全透明的,之因此會有背景顏色,是由於下層顯示界面在被覆蓋的區域添加了顏色,以後,經過SurfaceFlinger的圖層混合,好像是狀態欄、導航欄自身有了背景色。看一下一個普通的Activity展現的時候,所對應的Surface(或者說Window也能夠)。以下Surface圖:android

  1. 第一個XXXXActivity,大小是屏幕大小
  2. 第二個狀態欄StatusBar,大小對應頂部那一條
  3. 第三個是底部虛擬導航欄NavigationBar,大小對應底部那一條
  4. HWC_FRAMEBUFFER_TARGET:是合成的目標Layer,不參與合成

 

從上表能夠看出,雖然只展現了一個Activity,可是同時會有StatusBar、NavigationBar、XXXXActivity能夠看出Activity是在狀態欄與導航欄下面的,被覆蓋了,它們共同參與顯示界面的合成,可是,StatusBar、NavigationBar明顯不是屬於APP自身UI管理的範疇。下面就來分析一下,APP層的API如何影響SystemUI的顯示的,並一步步解開所謂沉浸式與全屏的原理,首先看一下如何更改狀態欄顏色。windows

 

查看佈局

tool->android->layout inspectorsession

 

WindowInsets介紹

https://blog.csdn.net/yuanjw2014/article/details/78363353app

https://www.jianshu.com/p/756e94fa2e09框架

 

inset的直譯是插入物,理解爲特定屏幕區域更合適一些。WindowInsets的三個成員變量mSystemWindowInsets,mWindowDecorInsets,mStableInsets表示了三種屏幕區域。ide

 

  • 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.post

表明着內容區域被系統框架提供的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的消耗,它分紅不一樣的消耗層級:

  1. DecorView層級的消費 :主要針對NavigationBar部分
  2. DecorView根佈局消費(非用戶佈局)
  3. 用戶佈局消費

消費層級的選擇是可控的,使用得當,就能在不一樣的場景獲得想要的樣式。接下來分析下不一樣層級控制與消費的原理。

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,

主要有三個條件:

  1. sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION == 0 不設置隱藏導航欄
  2. sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION == 0,導航欄顯示時,內容不能擴展到導航欄下方
  3. (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 不使用系統背景

 

同時知足以上三點,Insets的bottom部分就會被DecorView利用Margin的方式消費掉,默認樣式的Activity知足上述三個條件,所以,底部導航欄部分Insets默認被DecorView消費掉了,以下圖:

非懸浮Activity的DecorView默認是全屏的,圖中一、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的邏輯。

總結

  1. 狀態欄與導航欄顏色的設置與其顯示隱藏有關係,一旦隱藏,設置顏色就無效,而且顏色是經過向DecorView根佈局addView的方式來實現的。
  2. 默認樣式下DecorView消費導航欄,利用其內部Content的Margin來實現
  3. fitsysytemwindow與UI的content的擴展有關係,若是設置了全屏之類的屬性,WindowsInsets必定留給子View消費
  4. Translucent與設置顏色互斥,可是與fitSystemWindow不互斥
  5. 設置顏色與擴展布局是不互斥的兩種操做
  6. fitSystemWindow只會經過padding方式來消費WindowInsets
相關文章
相關標籤/搜索