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

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

前言


本文大部分基于6.0的版本

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


image.png

  • 第一个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,默认是白色,如下图,隐藏状态栏前后的对比:


image.png

image.png以上是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消费掉了,如下图:

image.png


非悬浮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为了保证内容布局不随着导航栏的消失而滚动,效果如下图:

image.png

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

image.png

如上图:由于背景透明,所以状态栏与导航栏背景色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


目录
相关文章
|
2月前
|
vr&ar 图形学 API
Unity与VR控制器交互全解:从基础配置到力反馈应用,多角度提升虚拟现实游戏的真实感与沉浸体验大揭秘
【8月更文挑战第31天】虚拟现实(VR)技术迅猛发展,Unity作为主流游戏开发引擎,支持多种VR硬件并提供丰富的API,尤其在VR控制器交互设计上具备高度灵活性。本文详细介绍了如何在Unity中配置VR支持、设置控制器、实现按钮交互及力反馈,结合碰撞检测和物理引擎提升真实感,助力开发者创造沉浸式体验。
133 0
|
3月前
|
开发者 图形学 C#
深度解密:Unity游戏开发中的动画艺术——Mecanim状态机如何让游戏角色栩栩如生:从基础设置到高级状态切换的全面指南,助你打造流畅自然的游戏动画体验
【8月更文挑战第31天】Unity动画系统是游戏开发的关键部分,尤其适用于复杂角色动画。本文通过具体案例讲解Mecanim动画状态机的使用方法及原理。我们创建一个游戏角色并设计行走、奔跑和攻击动画,详细介绍动画状态机设置及脚本控制。首先导入动画资源并添加Animator组件,然后创建Animator Controller并设置状态间的转换条件。通过编写C#脚本(如PlayerMovement)控制动画状态切换,实现基于玩家输入的动画过渡。此方法不仅适用于游戏角色,还可用于任何需动态动画响应的对象,增强游戏的真实感与互动性。
88 0
|
3月前
|
API UED 开发者
如何在Uno Platform中轻松实现流畅动画效果——从基础到优化,全方位打造用户友好的动态交互体验!
【8月更文挑战第31天】在开发跨平台应用时,确保用户界面流畅且具吸引力至关重要。Uno Platform 作为多端统一的开发框架,不仅支持跨系统应用开发,还能通过优化实现流畅动画,增强用户体验。本文探讨了Uno Platform中实现流畅动画的多个方面,包括动画基础、性能优化、实践技巧及问题排查,帮助开发者掌握具体优化策略,提升应用质量与用户满意度。通过合理利用故事板、减少布局复杂性、使用硬件加速等技术,结合异步方法与预设缓存技巧,开发者能够创建美观且流畅的动画效果。
77 0
|
3月前
|
开发者 图形学 前端开发
绝招放送:彻底解锁Unity UI系统奥秘,五大步骤教你如何缔造令人惊叹的沉浸式游戏体验,从Canvas到动画,一步一个脚印走向大师级UI设计
【8月更文挑战第31天】随着游戏开发技术的进步,UI成为提升游戏体验的关键。本文探讨如何利用Unity的UI系统创建美观且功能丰富的界面,包括Canvas、UI元素及Event System的使用,并通过具体示例代码展示按钮点击事件及淡入淡出动画的实现过程,助力开发者打造沉浸式的游戏体验。
87 0
|
3月前
|
API UED 开发者
超实用技巧大放送:彻底革新你的WinForms应用,从流畅动画到丝滑交互设计,全面解析如何在保证性能的同时大幅提升用户体验,让软件操作变得赏心悦目不再是梦!
【8月更文挑战第31天】在Windows平台上,使用WinForms框架开发应用程序时,如何在保持性能的同时提升用户界面的吸引力和响应性是一个常见挑战。本文探讨了在不牺牲性能的前提下实现流畅动画与交互设计的最佳实践,包括使用BackgroundWorker处理耗时任务、利用Timer控件创建简单动画,以及使用Graphics类绘制自定义图形。通过具体示例代码展示了这些技术的应用,帮助开发者显著改善用户体验,使应用程序更加吸引人和易于使用。
71 0
|
人工智能 物联网 5G
云渲染助力展厅播放3D应用内容,如何实现便捷操控?
随着5G网络发展、移动设备的普及,云渲染为数字孪生、智慧城市的部署提供了新的解决方案。云渲染助力数字孪生应用打破对终端的限制,实现终端轻量化,能够更好的展示大型3D应用内容。 基于以上,大型的智慧城市、数字孪生应用具备了可以在各大展厅及大屏设备上展示的基础,那么如何更便捷的管理大屏及播放内容呢? 展厅中控管理系统可以实现展厅讲解人员手持安卓/iOS平板实时控制展厅内的大屏,让屏幕便捷地展示云渲染系统内的内容。
云渲染助力展厅播放3D应用内容,如何实现便捷操控?
|
人工智能 人机交互 vr&ar
元宇宙系统开发简述:打造沉浸式虚拟体验
元宇宙系统开发简述:打造沉浸式虚拟体验
|
传感器 vr&ar 图形学
虚拟现实技术:开启新的沉浸式体验时代
虚拟现实(Virtual Reality,简称VR)技术作为一种颠覆性的技术,在近年来引起了广泛关注。它通过模拟或重建真实世界的环境,使用户能够身临其境地体验并与虚拟场景进行交互。本文将探讨虚拟现实技术的发展趋势以及它对我们的生活和工作带来的影响。
142 0
社交app源码技术屏幕的两大实用功能
很多人就会去选择去社交app软件,这也促使了社交app源码搭建平台的火爆,但是要想搭建出一个令用户满意的社交app平台,就要去了解用户需要什么样的社交app源码技术功能,今天我要讲的也是用户需要的,关于屏幕的两大实用功能:屏幕共享与屏幕录制!
社交app源码技术屏幕的两大实用功能
|
传感器 机器学习/深度学习 搜索推荐