全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制「沉浸式」的實現

目錄

狀體欄顏色設置原理
導航欄顏色設置原理
fitSystemWindow全屏及WindowInsets消費原理
fitSystemWindow及Padding不一樣層級的消費
Theme中window屬性配置影響
SystemUi及狀體欄添加原理 javascript

前言

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

Surface圖

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

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

狀態欄顏色更新原理

假設當前的場景是默認樣式的Activity,若是想要更新狀態欄顏色只須要以下代碼:session

getWindow().setStatusBarColor(RED);複製代碼

其實這裏調用的是PhoneWindow的setStatusBarColor函數,不管是Activity仍是Dialog都是被抽象成PhoneWindow:app

@Override
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,這裏只關注更改顏色:ide

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 statusBarNeedsRightInset = navBarToRightEdge
                    && mNavigationColorViewState.present;
            int statusBarRightInset = statusBarNeedsRightInset ? mLastRightInset : 0;
            <!--更新Color--> 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);複製代碼

若是當前對應Window的SystemUi設置了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的配置決定是否顯示狀態欄背景顏色,若是狀態欄都不顯示,那就不必顯示背景色了,其次,若是狀態欄顯示,但背景是透明色,也不必添加背景顏色,即不知足(color & Color.BLACK) != 0。最後看一下translucentFlag,默認狀況下,狀態欄背景色與translucent半透明效果互斥,半透明就統一用半透明顏色,不會再添加額外顏色。最後,再來看關鍵點3,其實很簡單,就是往DecorView上添加一個View,原則上說DecorView也是一個FrameLayout,因此最終的實現就是在FrameLayout添加一個有背景色的Viewpost

導航欄顏色更新原理

更新導航欄顏色的原理同更新狀態欄的原理幾乎徹底一致,以下代碼學習

@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 */);複製代碼

一樣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消費

默認樣式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>複製代碼

上面的佈局是PhoneWindow在建立DecorView時候用到的,其中關鍵點1:android:fitsSystemWindows屬性是系統添加狀態欄padding的關鍵,爲何這樣呢?看下ViewRootImpl的源碼,在ViewRootImpl進行佈局與繪製的時候會選擇性調用dispatchApplyInsets,這個函數的做用是找到符合要求的View,消費掉WindowInsets:

private void performTraversals() {
          ...
     host.fitSystemWindows(mFitSystemWindowsInsets);

<!--關鍵點1--> void dispatchApplyInsets(View host) { host.dispatchApplyWindowInsets(getWindowInsets(true /* forceConstruct */)); }複製代碼

host其實就是DecorView對象,DecorView會回調View的onApplyWindowInsets函數,不過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函數,以前看過對顏色的處理,這裏咱們主要看下對於邊距的處理:

private WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
        WindowManager.LayoutParams attrs = getAttributes();
        int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();
       if (!mIsFloating && ActivityManager.isHighEndGfx()) {
        ...
         <!--關鍵點16.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); } } ... return insets; }複製代碼

在6.0對應的源碼中,DecorView自身主要對NavigationBar那部分的Insets作了處理,並無對狀態欄作處理。而且DecorView經過設置Margin的方式來處理Insets的消費的:mContentRoot.setLayoutParams(lp);這裏主要關心下consumingNavBar的條件:什麼狀況下DecorView會經過設置Margin來消費掉導航欄那部分Padding,主要有三個條件:

  1. sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION == 0,沒強制要求內容擴展到導航欄下方
  2. (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 沒有強制使用系統背景
  3. sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION == 0 沒有設置隱藏導航欄

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

系統默認Activity中WindowInsets的消費

非懸浮Activity的DecorView默認是全屏的,圖中一、2表明着DecorView中添加狀體欄、導航欄對應的顏色View,而DecorView的Content子View是一個LinearLayout,能夠看出它並非全屏,而是底部有一個Margin,正好對應導航欄的高度,頂部有個padding,這個實際上是由fitSystemWindow決定的。

系統佈局級別(非DecorView)的fitSystemWindow消費

可是,若是僅僅設置了SYSTEM_UI_FLAG_HIDE_NAVIGATION,DecorView根佈局的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能夠看出,這裏是經過設置DecorView中根內容佈局的padding來處理Insets消費的(同時消費了狀態欄與導航欄部分)。可是,無論何種方式,消費了就是消費了,被消費的部分不能再次消費。6.0源碼中,DecorView並無對狀態欄進行消費,狀態欄的消費都留給了DecorView子佈局及孫子輩佈局,不過7.0在系統級別的配置上留了個入口(ForceWindowDrawsStatusBarBackground)。

分析下6.0的原理,DecorView處理本身調用updateColorViews,還會遞歸調用ViewGroup的dispatchApplyWindowInset函數,知道Inset被消費,ViewGroup會選擇性進入設置了fitSystemWindow的View,即設置了fitsSystemWindows:

android:fitsSystemWindows="true"複製代碼

並回調fitSystemWindows函數進行處理,看下具體實現

protected boolean fitSystemWindows(Rect insets) {
                if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) {
            ...
            <!--關鍵函數-->
            return fitSystemWindowsInt(insets);
    }複製代碼

fitSystemWindowsInt是最爲關鍵的消費處理函數,裏面有當前View可否消費WindowInsets的判斷邏輯。

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去計算是否能消費,

protected boolean computeFitSystemWindows(Rect inoutInsets, Rect outLocalInsets) {
       // 這裏已是知足 FITS_SYSTEM_WINDOWS 標誌位
        // OPTIONAL_FITS_SYSTEM_WINDOWS 表明着是系統View 
        // SYSTEM_UI_LAYOUT_FLAGS 表明着是否要求全屏 SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION| SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
         // 若是是普通View能夠直接消費,若是是系統View,要看看是否是設置了全屏 
        // 非系統的UI能夠,系統UI未設置全屏能夠
        // 全部View公用mAttachInfo.mSystemUiVisibility
            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,OPTIONAL_FITS_SYSTEM_WINDOWS是經過 makeOptionalFitsSystemWindows設置的,入口只在PhoneWindow中,經過mDecor.makeOptionalFitsSystemWindows()設置

public void makeOptionalFitsSystemWindows() {
    setFlags(OPTIONAL_FITS_SYSTEM_WINDOWS, OPTIONAL_FITS_SYSTEM_WINDOWS);
}

 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的時候,裏面還未涉及用戶view,因此經過mDecor.makeOptionalFitsSystemWindows標記的都是系統本身的View佈局 ,接着往下看

@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;
    }複製代碼

而mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS == 0 表明沒有設置全屏之類的參數,若是設置了全屏,即設置了SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN任意一個,就只能讓用戶View去消費,系統View沒有權限,正如以前simple_screen.xml佈局,雖然根佈局設置了fitSystemWindow爲true,可是,若是你用來全屏參數,根佈局的fitSystemWindow就會無效,

SYSTEM_UI_LAYOUT_FLAGS = SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
 | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN複製代碼

若是上面都沒有消費,就會轉換爲用戶佈局級別的消費。

用戶佈局級別的fitSystemWindow消費

假設圖片瀏覽的場景:全屏,導航欄與狀態欄透明,圖片瀏覽區伸展到整個屏幕,經過設置下面的配置就能達到效果:全屏,而且用戶佈局與系統佈局都不消費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);
}複製代碼

沉浸式全屏

如上圖:因爲背景透明,因此狀態欄與導航欄背景色View都沒有被添加,其次,因爲設置了View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,DecorView與系統佈局都不會消費WindowInsets,而在用戶本身的佈局中也沒有設置 android:fitsSystemWindows="true",這樣不會有View消費WindowInsets,達到全屏效果。

有一個小點須要注意下,那就是Theme中也支持fitsSystemWindows的設置

<item name="android:fitsSystemWindows">true</item>複製代碼

默認狀況下上屬性爲false,若是設置了True,就會被第一個未設置fitsSystemWindows的View消費掉。

<item name="android:fitsSystemWindows">false</item>複製代碼

遵照View默認的消費邏輯,被第一個FitSystemWindow=true的佈局消費掉,經過設置本身padding的方式。

如何獲取須要消費的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是互斥的。因此,默認狀況下android:windowTranslucentStatus是false。也就是說:‘windowTranslucentStatus’和‘windowTranslucentNavigation’設置爲true後就再設置‘statusBarColor’和‘navigationBarColor’就沒有效果了。。緣由以下:

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的邏輯。

SystemUi中系統狀態欄的添加邏輯

上面咱們說過了,狀體欄、導航欄屬於系統窗口,不在用戶管理的範疇內,因爲牽扯到通知、圖標之類的管理,仍是挺複雜的,這裏咱們只關心 狀態欄的添加時機,用來講明狀態欄視圖實際上是不歸APP添加管理的。在系統啓動SystemServer的時候,就會建立SystemUiService ,關於狀體欄的以下:

SystemServer.java

static final void startSystemUi(Context context) {
    Intent intent = new Intent();
    intent.setComponent(new ComponentName("com.android.systemui",
                "com.android.systemui.SystemUIService"));
    intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
    context.startServiceAsUser(intent, UserHandle.SYSTEM);
}複製代碼

以後會調用SystemUIApplication的startServicesIfNeeded(),若是服務未啓動,就將相應的服務啓動,主要包含以下服務

private final Class<?>[] SERVICES = new Class[] {
        com.android.systemui.tuner.TunerService.class,
        ...
        com.android.systemui.statusbar.SystemBars.class,
        com.android.systemui.usb.StorageNotification.class,
        com.android.systemui.power.PowerUI.class,
        ...
};複製代碼

這隻關心com.android.systemui.statusbar.SystemBars.class

private void startServicesIfNeeded(Class<?>[] services) {
    ...
    final int N = services.length;
    for (int i=0; i<N; i++) {
        Class<?> cl = services[i];
        if (DEBUG) Log.d(TAG, "loading: " + cl);
        try {
            Object newService = SystemUIFactory.getInstance().createInstance(cl);
            mServices[i] = (SystemUI) ((newService == null) ? cl.newInstance() : newService);
        }...
        mServices[i].mContext = this;
        mServices[i].mComponents = mComponents;
        mServices[i].start();
       if (mBootCompleted) {
            mServices[i].onBootCompleted();
        }
    }
    mServicesStarted = true;
}複製代碼

SystemBars會經過 createStatusBarFromConfig建立BaseStatusBar,對於手機而言就是PhoneStatusBar ,最後會調用PhoneStatusBar 的start添加到WMS中去,具體再也不一步步的跟,有興趣本身看:

private void addStatusBarWindow() {
final int height = getStatusBarHeight();
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        height,
        WindowManager.LayoutParams.TYPE_STATUS_BAR,
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
        PixelFormat.TRANSLUCENT);

lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
lp.gravity = getStatusBarGravity();
lp.setTitle("StatusBar");
lp.packageName = mContext.getPackageName();
makeStatusBarView();
mWindowManager.addView(mStatusBarWindow, lp);複製代碼

}

因此從源碼很容易看出,其實狀體欄或者導航欄實際上是在 com.android.systemui進程中添加到WMS的,跟用戶進程不要緊。

總結

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

僅供參考,歡迎指正

相關文章
相關標籤/搜索