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

看書的小蝸牛發表於2017-06-20

目錄

狀體欄顏色設定原理
導航欄顏色設定原理
fitSystemWindow全屏及WindowInsets消費原理
fitSystemWindow及Padding不同層級的消費
Theme中window屬性配置影響
SystemUi及狀體欄新增原理

前言

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

全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的實現
Surface圖

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

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

狀態列顏色更新原理

假設當前的場景是預設樣式的Activity,如果想要更新狀態列顏色只需要如下程式碼:

getWindow().setStatusBarColor(RED);複製程式碼

其實這裡呼叫的是PhoneWindow的setStatusBarColor函式,無論是Activity還是Dialog都是被抽象成PhoneWindow:

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

 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新增一個有背景色的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 */);複製程式碼

同樣mNavigationColorViewState也有顯示的條件,如果設定了SYSTEM_UI_FLAG_HIDE_NAVIGATION、或者半透明、或者顏色為透明色,那同樣也不需要為導航欄新增背景色,具體不再重複。改變狀體欄及導航欄的顏色的本質是往DecorView中新增有顏色的View, 並放在狀態列及導航欄下面

當然,如果設定了隱藏狀態列,或者導航欄,並且沒有讓佈局隨著隱藏而動態變化的話,就會看到被覆蓋的padding,預設是白色,如下圖,隱藏狀態列前後的對比:

全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的實現
沒隱藏狀態列

全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的實現
隱藏了狀態列

以上是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消費掉了,如下圖:

全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的實現
系統預設Activity中WindowInsets的消費

非懸浮Activity的DecorView預設是全屏的,圖中1、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為了保證內容佈局不隨著導航欄的消失而滾動,效果如下圖:

全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的實現
僅僅設定隱藏導航欄

上圖中由於設定了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);
}複製程式碼

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

如上圖:由於背景透明,所以狀態列與導航欄背景色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

僅供參考,歡迎指正

相關文章