全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的實(shí)現(xiàn)

目錄

狀體欄顏色設(shè)置原理與導(dǎo)航欄顏色設(shè)置原理
fitSystemWindow全屏及WindowInsets消費(fèi)原理
fitSystemWindow與padding不同層級的消費(fèi)
Theme中window屬性配置影響
SystemUi及狀體欄添加原理

前言

本文大部分基于6.0的版本

狀態(tài)欄與導(dǎo)航欄屬于SystemUi的管理范疇纸俭,雖然界面的UI會受到SystemUi的影響,但是南窗,APP并沒有直接繪制SystemUI的權(quán)限與必要揍很。APP端之所以能夠更改狀態(tài)欄的顏色郎楼、導(dǎo)航欄的顏色,其實(shí)還是操作自己的View更改UI窒悔∥卦可以這么理解:狀態(tài)欄與導(dǎo)航欄擁有自己獨(dú)立的窗口,而且這兩個(gè)窗口的優(yōu)先級較高简珠,會懸浮在所有窗口之上阶界,可以把系統(tǒng)自身的狀態(tài)欄與導(dǎo)航欄看做全透明的,之所有會有背景顏色聋庵,是因?yàn)橄聦语@示界面在被覆蓋的區(qū)域添加了顏色膘融,之后,通過SurfaceFlinger的圖層混合祭玉,好像是狀態(tài)欄氧映、導(dǎo)航欄自身有了背景色⊥鸦酰看一下一個(gè)普通的Activity展示的時(shí)候岛都,所對應(yīng)的Surface(或者說Window也可以)。

Surface圖
  • 第一個(gè)XXXXActivity振峻,大小是屏幕大小
  • 第二個(gè)狀態(tài)欄StatusBar臼疫,大小對應(yīng)頂部那一條
  • 第三個(gè)是底部虛擬導(dǎo)航欄NavigationBar,大小對應(yīng)底部那一條
  • HWC_FRAMEBUFFER_TARGET:是合成的目標(biāo)Layer铺韧,不參與合成

從上表可以看出多矮,雖然只展示了一個(gè)Activity,但是同時(shí)會有StatusBar哈打、NavigationBar塔逃、XXXXActivity可以看出Activity是在狀態(tài)欄與導(dǎo)航欄下面的,被覆蓋了料仗,它們共同參與顯示界面的合成湾盗,但是,StatusBar立轧、NavigationBar明顯不是屬于APP自身UI管理的范疇格粪。下面就來分析一下,APP層的API如何影響SystemUI的顯示的氛改,并一步步解開所謂沉浸式與全屏的原理帐萎,首先看一下如何更改狀態(tài)欄顏色。

狀態(tài)欄顏色更新原理

假設(shè)當(dāng)前的場景是默認(rèn)樣式的Activity胜卤,如果想要更新狀態(tài)欄顏色只需要如下代碼:

getWindow().setStatusBarColor(RED);

其實(shí)這里調(diào)用的是PhoneWindow的setStatusBarColor函數(shù)疆导,無論是Activity還是Dialog都是被抽象成PhoneWindow:

@Override
public void setStatusBarColor(int color) {
    mStatusBarColor = color;
    mForcedStatusBarColor = true;
    if (mDecor != null) {
        mDecor.updateColorViews(null, false /* animate */);
    }
}

最終調(diào)用的是DecorView的updateColorViews函數(shù),DecorView是屬于Activity的PhoneWindow的內(nèi)部對象葛躏,也就說澈段,更新的對象從所謂的Window進(jìn)入到了Activity自身的布局視圖中悠菜,接著看DecorView,這里只關(guān)注更改顏色:

 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其實(shí)就代表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);

如果當(dāng)前對應(yīng)Window的SystemUi設(shè)置了SYSTEM_UI_FLAG_FULLSCREEN后悔醋,就會隱藏狀態(tài)欄,那就不在需要為狀態(tài)欄設(shè)置背景兽叮,否則就設(shè)置:

  private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color,
                int size, boolean verticalBar, int rightMargin, boolean animate) {
                <!--關(guān)鍵點(diǎn)1 條件1-->
            state.present = size > 0 && (sysUiVis & state.systemUiHideFlag) == 0
                    && (getAttributes().flags & state.hideWindowFlag) == 0
                    && (getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
               <!--關(guān)鍵點(diǎn)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;
            <!--關(guān)鍵點(diǎn)3-->
                    LayoutParams lp = new LayoutParams(resolvedWidth, resolvedHeight,
                            resolvedGravity);
                    lp.rightMargin = rightMargin;
                    addView(view, lp);
                    updateColorViewTranslations();
                }}  
              ...}

先看下關(guān)鍵點(diǎn)1跟2 芬骄,這里是根據(jù)SystemUI的配置決定是否顯示狀態(tài)欄背景顏色,如果狀態(tài)欄都不顯示鹦聪,那就沒必要顯示背景色了德玫,其次,如果狀態(tài)欄顯示椎麦,但背景是透明色宰僧,也沒必要添加背景顏色,即不滿足(color & Color.BLACK) != 0观挎。最后看一下translucentFlag琴儿,默認(rèn)情況下,狀態(tài)欄背景色與translucent半透明效果互斥嘁捷,半透明就統(tǒng)一用半透明顏色造成,不會再添加額外顏色。最后雄嚣,再來看關(guān)鍵點(diǎn)3晒屎,其實(shí)很簡單,就是往DecorView上添加一個(gè)View缓升,原則上說DecorView也是一個(gè)FrameLayout鼓鲁,所以最終的實(shí)現(xiàn)就是在FrameLayout添加一個(gè)有背景色的View

導(dǎo)航欄顏色更新原理

更新導(dǎo)航欄顏色的原理同更新狀態(tài)欄的原理幾乎完全一致港谊,如下代碼

@Override
public void setNavigationBarColor(int color) {
    mNavigationBarColor = color;
    mForcedNavigationBarColor = true;
    if (mDecor != null) {
        mDecor.updateColorViews(null, false /* animate */);
    }
}

只不過在DecorView進(jìn)行顏色更新的時(shí)候骇吭,傳遞的對象是 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也有顯示的條件,如果設(shè)置了SYSTEM_UI_FLAG_HIDE_NAVIGATION歧寺、或者半透明燥狰、或者顏色為透明色,那同樣也不需要為導(dǎo)航欄添加背景色斜筐,具體不再重復(fù)龙致。改變狀體欄及導(dǎo)航欄的顏色的本質(zhì)是往DecorView中添加有顏色的View, 并放在狀態(tài)欄及導(dǎo)航欄下面顷链。

當(dāng)然目代,如果設(shè)置了隱藏狀態(tài)欄,或者導(dǎo)航欄,并且沒有讓布局隨著隱藏而動態(tài)變化的話像啼,就會看到被覆蓋的padding,默認(rèn)是白色潭苞,如下圖忽冻,隱藏狀態(tài)欄前后的對比:

沒隱藏狀態(tài)欄
隱藏了狀態(tài)欄

以上是DecorView對狀態(tài)欄的添加機(jī)制,總結(jié)出來就是一句話:只要狀態(tài)欄/導(dǎo)航欄不設(shè)置隱藏此疹,設(shè)置顏色就會有效僧诚。實(shí)際應(yīng)用中經(jīng)常將狀態(tài)欄或者導(dǎo)航欄設(shè)置為透明色:即想要沉浸式體驗(yàn),這個(gè)時(shí)候背景顏色View就不在被繪制蝗碎,但是湖笨,默認(rèn)樣式下DecorView的內(nèi)容繪制區(qū)域并未擴(kuò)展到狀態(tài)欄、或者導(dǎo)航欄下面(TRANSLUCENT半透明效果除外(5.0之上蹦骑,一般不會有TRANSLUCENT功能))慈省,結(jié)果就是會看到被覆蓋區(qū)域的一篇空白。想要解決這個(gè)問題眠菇,就牽扯到下面的fitsystemwindow的處理边败。

DecorView內(nèi)容區(qū)域的擴(kuò)展與fitsystemwindow的意義

fitSystemWindow屬性可以讓DecorView的內(nèi)容區(qū)域延伸到系統(tǒng)UI下方,防止在擴(kuò)展時(shí)被覆蓋捎废,達(dá)到全屏笑窜、沉浸等不同體驗(yàn)效果。這里牽扯到WindowInsets的消費(fèi)登疗,其實(shí)就是我們周圍一些系統(tǒng)的邊框padding的消耗排截,它分成不同的消耗層級:

  • DecorView層級的消費(fèi) :主要針對NavigationBar部分
  • DecorView根布局消費(fèi)(非用戶布局)
  • 用戶布局消費(fèi)

消費(fèi)層級的選擇是可控的,使用得當(dāng)辐益,就能在不同的場景得到想要的樣式断傲。接下來分析下不同層級控制與消費(fèi)的原理。

DecorView級別的WindowInsets消費(fèi)

默認(rèn)樣式Activity的狀態(tài)欄是有顏色的智政,如果內(nèi)容直接擴(kuò)展到狀態(tài)欄下方艳悔,一定會被覆蓋掉,系統(tǒng)默認(rèn)的實(shí)現(xiàn)是在DecorView的根布局上加了個(gè)padding女仰,那么用戶的UI視圖就不會被覆蓋猜年。不過,如果狀態(tài)欄被設(shè)置為透明疾忍,用戶就會看到狀態(tài)欄下方有一片空白乔外,這種體驗(yàn)肯定不好。這種情況下一罩,往往希望內(nèi)容能夠延伸到狀體欄下方杨幼,因此,就需要把空白的也留給內(nèi)容視圖。首先差购,分析下四瘫,默認(rèn)樣式的Activity為什么會有頂部的空白,看下一默認(rèn)情況下系統(tǒng)的根布局屬性欲逃,里面有我們要找的關(guān)鍵點(diǎn) android:fitsSystemWindows="true":

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    <!--關(guān)鍵點(diǎn)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在創(chuàng)建DecorView時(shí)候用到的找蜜,其中關(guān)鍵點(diǎn)1:android:fitsSystemWindows屬性是系統(tǒng)添加狀態(tài)欄padding的關(guān)鍵,為什么這樣呢稳析?看下ViewRootImpl的源碼洗做,在ViewRootImpl進(jìn)行布局與繪制的時(shí)候會選擇性調(diào)用dispatchApplyInsets,這個(gè)函數(shù)的作用是找到符合要求的View彰居,消費(fèi)掉WindowInsets:

 private void performTraversals() {
          ...
     host.fitSystemWindows(mFitSystemWindowsInsets);
 
<!--關(guān)鍵點(diǎn)1-->
 void dispatchApplyInsets(View host) {
    host.dispatchApplyWindowInsets(getWindowInsets(true /* forceConstruct */));
}

host其實(shí)就是DecorView對象诚纸,DecorView會回調(diào)View的onApplyWindowInsets函數(shù),不過DecorView重寫了該函數(shù):

@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    final WindowManager.LayoutParams attrs = mWindow.getAttributes();
    ...
    mFrameOffsets.set(insets.getSystemWindowInsets());
    <!--關(guān)鍵點(diǎn)1-->
    insets = updateColorViews(insets, true /* animate */);
    insets = updateStatusGuard(insets);
    updateNavigationGuard(insets);
    if (getForeground() != null) {
        drawableChanged();
    }
    return insets;
}

關(guān)鍵是調(diào)用updateColorViews函數(shù)陈惰,之前看過對顏色的處理畦徘,這里我們主要看下對于邊距的處理:

  private WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
        WindowManager.LayoutParams attrs = getAttributes();
        int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();
       if (!mIsFloating && ActivityManager.isHighEndGfx()) {
        ...
        <!--關(guān)鍵點(diǎn)1 :6.0代碼是否能夠擴(kuò)展到導(dǎo)航欄下面-->
    
     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;
        <!--關(guān)鍵點(diǎn)1 ,可以看到抬闯,根布局會根據(jù)消耗的狀況旧烧,來評估到底底部,右邊部分margin多少画髓,并設(shè)置進(jìn)去-->
        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);
               ..}
       <!--關(guān)鍵點(diǎn)2 重新計(jì)算消費(fèi)結(jié)果---->
            if (insets != null) {
                insets = insets.replaceSystemWindowInsets(
                        insets.getSystemWindowInsetLeft(),
                        insets.getSystemWindowInsetTop(),
                        insets.getSystemWindowInsetRight() - consumedRight,
                        insets.getSystemWindowInsetBottom() - consumedBottom);
            } }
         ...
        return insets;  
        }

在6.0對應(yīng)的源碼中掘剪,DecorView自身主要對NavigationBar那部分的Insets做了處理,并沒有對狀態(tài)欄做處理奈虾。并且?DecorView通過設(shè)置Margin的方式來處理Insets的消費(fèi)的:mContentRoot.setLayoutParams(lp);這里主要關(guān)心下consumingNavBar的條件:什么情況下DecorView會通過設(shè)置Margin來消費(fèi)掉導(dǎo)航欄那部分Padding夺谁,主要有三個(gè)條件:

  1. sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION == 0,沒強(qiáng)制要求內(nèi)容擴(kuò)展到導(dǎo)航欄下方
  2. (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 需要繪制系統(tǒng)導(dǎo)航欄
  3. sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION == 0 沒有設(shè)置隱藏導(dǎo)航欄

同時(shí)滿足以上三點(diǎn)肉微,Insets的bottom部分就會被DecorView利用Margin的方式消費(fèi)掉匾鸥,默認(rèn)樣式的Activity滿足上述三個(gè)條件,因此碉纳,底部導(dǎo)航欄部分默認(rèn)被DecorView消費(fèi)掉了勿负,如下圖:

系統(tǒng)默認(rèn)Activity中WindowInsets的消費(fèi)

非懸浮Activity的DecorView默認(rèn)是全屏的,圖中1劳曹、2代表著DecorView中添加狀體欄奴愉、導(dǎo)航欄對應(yīng)的顏色View,而DecorView的Content子View是一個(gè)LinearLayout铁孵,可以看出它并不是全屏锭硼,而是底部有一個(gè)Margin,正好對應(yīng)導(dǎo)航欄的高度蜕劝,頂部有個(gè)padding檀头,這個(gè)其實(shí)是由fitSystemWindow決定的轰异。

系統(tǒng)布局級別(非DecorView)的fitSystemWindow消費(fèi)

但是,如果僅僅設(shè)置了SYSTEM_UI_FLAG_HIDE_NAVIGATION暑始,DecorView根布局的fitsystemwindow就會生效搭独,并通過設(shè)置padding消費(fèi)掉,這里就是系統(tǒng)布局級別的消費(fèi)廊镜,不是用戶自己定義的View布局牙肝,設(shè)置代碼,

setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                    |View.SYSTEM_UI_FLAG_LAYOUT_STABLE);

View.SYSTEM_UI_FLAG_LAYOUT_STABLE為了保證內(nèi)容布局不隨著導(dǎo)航欄的消失而滾動期升,效果如下圖:

僅僅設(shè)置隱藏導(dǎo)航欄

上圖中由于設(shè)置了SYSTEM_UI_FLAG_HIDE_NAVIGATION,所以沒有導(dǎo)航欄View被添加互躬,DecorView中只有狀態(tài)欄背景(1)View與根內(nèi)容布局播赁,從圖中的點(diǎn)2可以看出,這里是通過設(shè)置DecorView中根內(nèi)容布局的padding來處理Insets消費(fèi)的(同時(shí)消費(fèi)了狀態(tài)欄與導(dǎo)航欄部分)吼渡。但是容为,不管何種方式,消費(fèi)了就是消費(fèi)了寺酪,被消費(fèi)的部分不能再次消費(fèi)坎背。6.0源碼中,DecorView并沒有對狀態(tài)欄進(jìn)行消費(fèi)寄雀,狀態(tài)欄的消費(fèi)都留給了DecorView子布局及孫子輩布局得滤,不過7.0在系統(tǒng)級別的配置上留了個(gè)入口(ForceWindowDrawsStatusBarBackground)。

分析下6.0的原理盒犹,DecorView處理自己調(diào)用updateColorViews懂更,還會遞歸調(diào)用ViewGroup的dispatchApplyWindowInset函數(shù),知道Inset被消費(fèi)急膀,ViewGroup會選擇性進(jìn)入設(shè)置了fitSystemWindow的View沮协,即設(shè)置了fitsSystemWindows:

    android:fitsSystemWindows="true"

并回調(diào)fitSystemWindows函數(shù)進(jìn)行處理,看下具體實(shí)現(xiàn)

  protected boolean fitSystemWindows(Rect insets) {
                if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) {
            ...
            <!--關(guān)鍵函數(shù)-->
            return fitSystemWindowsInt(insets);
    }

fitSystemWindowsInt是最為關(guān)鍵的消費(fèi)處理函數(shù)卓嫂,里面有當(dāng)前View能否消費(fèi)WindowInsets的判斷邏輯慷暂。

private boolean fitSystemWindowsInt(Rect insets) {
        <!--關(guān)鍵點(diǎn)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);
        }
       <!--關(guān)鍵點(diǎn)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;
}

先看關(guān)鍵點(diǎn)1,如果View設(shè)置了FITS_SYSTEM_WINDOWS晨雳,就通過關(guān)鍵點(diǎn)2 computeFitSystemWindows去計(jì)算是否能消費(fèi)行瑞,

protected boolean computeFitSystemWindows(Rect inoutInsets, Rect outLocalInsets) {
       // 這里已經(jīng)是滿足 FITS_SYSTEM_WINDOWS 標(biāo)志位
        // OPTIONAL_FITS_SYSTEM_WINDOWS 代表著是系統(tǒng)View 
        // SYSTEM_UI_LAYOUT_FLAGS 代表著是否要求全屏 SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION| SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
        //  如果是普通View可以直接消費(fèi),如果是系統(tǒng)View餐禁,要看看是不是設(shè)置了全屏      
        // 非系統(tǒng)的UI可以蘑辑,系統(tǒng)UI未設(shè)置全屏可以
        // 所有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設(shè)置的坠宴,入口只在PhoneWindow中洋魂,通過mDecor.makeOptionalFitsSystemWindows()設(shè)置

    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 {
            // 設(shè)置Window
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);  
            <!--關(guān)鍵點(diǎn)1-->      
            mDecor.makeOptionalFitsSystemWindows();
        ...
        }

在installDecor的時(shí)候,里面還未涉及用戶view,所以通過mDecor.makeOptionalFitsSystemWindows標(biāo)記的都是系統(tǒng)自己的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 代表沒有設(shè)置全屏之類的參數(shù)衔肢,如果設(shè)置了全屏,即設(shè)置了SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN任意一個(gè)豁翎,就只能讓用戶View去消費(fèi)角骤,系統(tǒng)View沒有權(quán)限,正如之前simple_screen.xml布局心剥,雖然根布局設(shè)置了fitSystemWindow為true邦尊,但是,如果你用來全屏參數(shù)优烧,根布局的fitSystemWindow就會無效蝉揍,

SYSTEM_UI_LAYOUT_FLAGS = SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
 | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

如果上面都沒有消費(fèi),就會轉(zhuǎn)換為用戶布局級別的消費(fèi)畦娄。

用戶布局級別的fitSystemWindow消費(fèi)

假設(shè)圖片瀏覽的場景:全屏又沾,導(dǎo)航欄與狀態(tài)欄透明,圖片瀏覽區(qū)伸展到整個(gè)屏幕熙卡,通過設(shè)置下面的配置就能達(dá)到效果:全屏杖刷,并且用戶布局與系統(tǒng)布局都不消費(fèi)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);
}
沉浸式全屏

如上圖:由于背景透明,所以狀態(tài)欄與導(dǎo)航欄背景色View都沒有被添加驳癌,其次滑燃,由于設(shè)置了View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,DecorView與系統(tǒng)布局都不會消費(fèi)WindowInsets颓鲜,而在用戶自己的布局中也沒有設(shè)置 android:fitsSystemWindows="true"不瓶,這樣不會有View消費(fèi)WindowInsets,達(dá)到全屏效果灾杰。

有一個(gè)小點(diǎn)需要注意下蚊丐,那就是Theme中也支持fitsSystemWindows的設(shè)置

  <item name="android:fitsSystemWindows">true</item>

默認(rèn)情況下上屬性為false,如果設(shè)置了True艳吠,就會被第一個(gè)未設(shè)置fitsSystemWindows的View消費(fèi)掉麦备。

  <item name="android:fitsSystemWindows">false</item>

遵守View默認(rèn)的消費(fèi)邏輯,被第一個(gè)FitSystemWindow=true的布局消費(fèi)掉昭娩,通過設(shè)置自己padding的方式凛篙。

如何獲取需要消費(fèi)的WindowInsets

前面說的消費(fèi)的WindowInsets 是怎么來的呢?其實(shí)是ViewRootImpl在relayout的時(shí)候請求WMS進(jìn)行計(jì)算出來的栏渺,計(jì)算成功后保存到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獲取對應(yīng)的Insets磕诊,其實(shí)是存在WindowState中的填物。這里不再深入纹腌,有興趣自己學(xué)習(xí)。

為何windowTranslucentStatus與statusBarColor不能同時(shí)生效

Android4.4的時(shí)候滞磺,加了個(gè)windowTranslucentStatus屬性升薯,實(shí)現(xiàn)了狀態(tài)欄導(dǎo)航欄半透明效果,而Android5.0之后以上狀態(tài)欄击困、導(dǎo)航欄支持顏色隨意設(shè)定涎劈,所以,5.0之后一般不使用需要使用該屬性阅茶,而且設(shè)置狀態(tài)欄顏色與windowTranslucentStatus是互斥的蛛枚。所以,默認(rèn)情況下android:windowTranslucentStatus是false脸哀。也就是說:‘windowTranslucentStatus’和‘windowTranslucentNavigation’設(shè)置為true后就再設(shè)置‘statusBarColor’和‘navigationBarColor’就沒有效果了蹦浦。。原因如下:

 boolean show = state.present
                && (color & Color.BLACK) != 0
                && ((mWindow.getAttributes().flags & state.translucentFlag) == 0  || force);

可以看到企蹭,添加背景View有一個(gè)必要條件

(mWindow.getAttributes().flags & state.translucentFlag) == 0 

也就是說一旦設(shè)置了

    <item name="android:windowTranslucentStatus">true</item>
    <item name="android:windowTranslucentNavigation">true</item>

相應(yīng)的狀態(tài)欄或者導(dǎo)航欄的顏色設(shè)置就不在生效白筹。不過它并不影響fitSystemWindow的邏輯智末。

SystemUi中系統(tǒng)狀態(tài)欄的添加邏輯

上面我們說過了谅摄,狀體欄、導(dǎo)航欄屬于系統(tǒng)窗口系馆,不在用戶管理的范疇內(nèi)送漠,由于牽扯到通知、圖標(biāo)之類的管理由蘑,還是挺復(fù)雜的闽寡,這里我們只關(guān)心 狀態(tài)欄的添加時(shí)機(jī),用來說明狀態(tài)欄視圖其實(shí)是不歸APP添加管理的尼酿。在系統(tǒng)啟動SystemServer的時(shí)候爷狈,就會創(chuàng)建SystemUiService ,關(guān)于狀體欄的如下:

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);
}

之后會調(diào)用SystemUIApplication的startServicesIfNeeded(),如果服務(wù)未啟動裳擎,就將相應(yīng)的服務(wù)啟動涎永,主要包含如下服務(wù)

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,
        ...
};

這只關(guān)心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創(chuàng)建BaseStatusBar,對于手機(jī)而言就是PhoneStatusBar 鹿响,最后會調(diào)用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);

}

所以從源碼很容易看出,其實(shí)狀體欄或者導(dǎo)航欄其實(shí)是在 com.android.systemui進(jìn)程中添加到WMS的惶我,跟用戶進(jìn)程沒關(guān)系妈倔。

總結(jié)

  • 狀態(tài)欄與導(dǎo)航欄顏色的設(shè)置與其顯示隱藏有關(guān)系,一旦隱藏绸贡,設(shè)置顏色就無效盯蝴,并且顏色是通過向DecorView根布局addView的方式來實(shí)現(xiàn)的毅哗。
  • 默認(rèn)樣式下DecorView消費(fèi)導(dǎo)航欄,利用其內(nèi)部Content的Margin來實(shí)現(xiàn)
  • fitsysytemwindow與UI的content的擴(kuò)展有關(guān)系结洼,如果設(shè)置了全屏之類的屬性黎做,WindowsInsets一定留給子View消費(fèi)
  • Translucent與設(shè)置顏色互斥,但是與fitSystemWindow不互斥
  • 設(shè)置顏色與擴(kuò)展布局是不互斥的兩種操作
  • fitSystemWindow只會通過padding方式來消費(fèi)WindowInsets

作者:看書的小蝸牛
原文鏈接: 全屏松忍、沉浸式蒸殿、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的實(shí)現(xiàn)

僅供參考,歡迎指正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸣峭,一起剝皮案震驚了整個(gè)濱河市宏所,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌摊溶,老刑警劉巖爬骤,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡蝶涩,警方通過查閱死者的電腦和手機(jī)焕毫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坷剧,“玉大人,你說我怎么就攤上這事喊暖”蛊螅” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵陵叽,是天一觀的道長狞尔。 經(jīng)常有香客問我,道長巩掺,這世上最難降的妖魔是什么偏序? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮胖替,結(jié)果婚禮上研儒,老公的妹妹穿的比我還像新娘。我一直安慰自己刊殉,他們只是感情好殉摔,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著记焊,像睡著了一般逸月。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上遍膜,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天碗硬,我揣著相機(jī)與錄音瓤湘,去河邊找鬼。 笑死恩尾,一個(gè)胖子當(dāng)著我的面吹牛弛说,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播翰意,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼木人,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了冀偶?” 一聲冷哼從身側(cè)響起醒第,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎进鸠,沒想到半個(gè)月后稠曼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡客年,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年霞幅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片量瓜。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡司恳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出榔至,到底是詐尸還是另有隱情抵赢,我是刑警寧澤欺劳,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布唧取,位于F島的核電站,受9級特大地震影響划提,放射性物質(zhì)發(fā)生泄漏枫弟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一鹏往、第九天 我趴在偏房一處隱蔽的房頂上張望淡诗。 院中可真熱鬧,春花似錦伊履、人聲如沸韩容。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽群凶。三九已至,卻和暖如春哄辣,著一層夾襖步出監(jiān)牢的瞬間请梢,已是汗流浹背赠尾。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留毅弧,地道東北人气嫁。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像够坐,于是被迫代替她去往敵國和親寸宵。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容