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

2017/03/31 Android

目录

状体栏颜色设置原理
导航栏颜色设置原理
fitSystemWindow全屏及WindowInsets消费原理
fitSystemWindow及Padding不同层级的消费 Theme中window属性配置影响
SystemUi及状体栏添加原理

前言

状态栏与导航栏属于SystemUi的管理范畴,虽然界面的UI会受到SystemUi的影响,但是,APP并没有直接绘制SystemUI的权限与必要。APP端之所以能够更改状态栏的颜色、导航栏的颜色,其实还是操作自己的View更改UI。可以这么理解:状态栏与导航栏拥有自己独立的窗口,而且这两个窗口的优先级较高,会悬浮在所有窗口之上,可以把系统自身的状态栏与导航栏看做全透明的,之所有会有背景颜色,是因为下层显示界面在被覆盖的区域添加了颜色,之后,通过SurfaceFlinger的图层混合,好像是状态栏、导航栏自身有了背景色。看一下一个普通的Activity展示的时候,所对应的Surface(或者说Window也可以)。

type   |  handle  | hint | blnd |   format    |     source crop (l,t,r,b)      |          frame         | name  -----------+----------+------+------+-------------+--------------------------------+------------------------+------
   HWC | b66d63c0 | 0002 | 0100 | RGBA_8888   |    0.0,    0.0, 1080.0, 1920.0 |    0,    0, 1080, 1920 | XXXXActivity
   HWC | b66d6550 | 0002 | 0105 | RGBA_8888   |    0.0,    0.0, 1080.0,   72.0 |    0,    0, 1080,   72 | StatusBar
   HWC | b66d6690 | 0002 | 0105 | RGBA_8888   |    0.0,    0.0, 1080.0,  144.0 |    0, 1776, 1080, 1920 | NavigationBar  FB TARGET | b6a51c10 | 0000 | 0105 | RGBA_8888   |    0.0,    0.0, 1080.0, 1920.0 |    0,    0, 1080, 1920 | HWC_FRAMEBUFFER_TARGET
  • 第一个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:

<!--满足一定条件,才能设置状态栏颜色-->
/**
 * Sets the color of the status bar to {@code color}.
 *
 * For this to take effect,
 * the window must be drawing the system bar backgrounds with
 * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
 * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS} must not be set.
 *
 * If {@code color} is not opaque, consider setting
 * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
 * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}.
 * <p>
 * The transitionName for the view background will be "android:status:background".
 * </p>
 */
 
@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,默认是白色,如下图,隐藏状态栏前后的对比:

没隐藏状态栏

隐藏了状态栏

以上是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()) {
        ...
 			<!--关键点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);
            } }
         ...
        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默认是全屏的,图中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为了保证内容布局不随着导航栏的消失而滚动,效果如下图:

仅仅设置隐藏导航栏

上图中由于设置了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

仅供参考,欢迎指正

参考文档

Android沉浸式UI实现及原理
WindowInsets和StatusBar
Android窗口管理服务WindowManagerService计算Activity窗口大小的过程分析
View绘制流程及源码解析(一)——performTraversals()源码分析 android:fitSystemWindows详解
Using Immersive Full-Screen Mode

Search

    Table of Contents