Android-DecorView-一窥全貌(下)

前言

上篇分析了DecorView创建过程,大致说了其子View构成,还剩下一些疑点,本篇继续分析。
上篇文章:Android DecorView 一窥全貌(上)

通过本篇文章,你将了解到:

1、DecorView各个子View具体布局内容
2、状态栏(背景)和导航栏(背景)如何添加到DecorView里
3、DecorView子View位置与大小的确定
4、常见的获取DecorView各个区块大小的方法

DecorView各个子View具体布局内容

照旧,打开Tools->Layout Inspector


image.png

此时,DecorView有三个子View,分别是LinearLayout、navigationBarBackground、statusBarBackground。

默认DecorView布局

先来看看LinearLayout,之前分析过加载DecorView时,根据不同的feature确定不同的布局,我们的demo加载的是默认布局:R.layout.screen_simple。
这是系统自带的布局文件,在哪找呢?

切换到Project模式——>找到External Libraries——>对应的编译API——>res library root——>layout文件夹下——>寻找对应的布局名

R.layout.screen_simple布局内容

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    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>

几点有价值的地方:

  • 该LinearLayout方向是垂直的,有个属性android:fitsSystemWindows="true"(后续需要用到)
  • ViewStub是占位用的,默认是Gone,先不管
  • 有个id="content"的FrameLayout,是不是有点熟悉?

再来看看实际的layout展示:


image.png

正好和LinearLayout对应,ViewStub也对得上,但是明明布局文件里的FrameLayout是没有子View的,实际怎么会有呢?当然是中途动态添加进去的。

SubDecor

之前分析过,DecorView创建成功后,又继续加载了一个布局:R.layout.abc_screen_toolbar,并赋予subDecor变量,最后将subDecor里的某个子View添加到DecorView里。那么该布局文件在哪找呢?按照上面的方法,你会发现layout里并没有对应的布局文件。

实际上加载R.layout.abc_screen_toolbar是由AppCompatDelegateImpl.java完成的,而该类属于androidx.appcompat.app包,因此该寻找androidx里资源文件

image.png

R.layout.abc_screen_toolbar布局内容:

<androidx.appcompat.widget.ActionBarOverlayLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/decor_content_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">
    <include layout="@layout/abc_screen_content_include"/>
    <androidx.appcompat.widget.ActionBarContainer
            android:id="@+id/action_bar_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            style="?attr/actionBarStyle"
            android:touchscreenBlocksFocus="true"
            android:gravity="top">
        <androidx.appcompat.widget.Toolbar
                android:id="@+id/action_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
         app:navigationContentDescription="@string/abc_action_bar_up_description"
                style="?attr/toolbarStyle"/>
        <androidx.appcompat.widget.ActionBarContextView
                android:id="@+id/action_context_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:visibility="gone"
                android:theme="?attr/actionBarTheme"
                style="?attr/actionModeStyle"/>
    </androidx.appcompat.widget.ActionBarContainer>
</androidx.appcompat.widget.ActionBarOverlayLayout>

同样提取几个关键信息:

  • ActionBarOverlayLayout 继承自ViewGroup,id="decor_content_parent",同样有个属性:android:fitsSystemWindows="true"
  • ActionBarContainer顾名思义是容纳ActionBar的,id="action_bar_container",android:gravity="top"。继承自FrameLayout。有两个子View,一个是ToolBar,另一个是ActionBar。现在高版本都使用ToolBar替代ActionBar。

ActionBarOverlayLayout还有个子View

<include layout="@layout/abc_screen_content_include"/>

其内容为:

<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <androidx.appcompat.widget.ContentFrameLayout
            android:id="@id/action_bar_activity_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:foregroundGravity="fill_horizontal|top"
            android:foreground="?android:attr/windowContentOverlay" />
</merge>
  • ContentFrameLayout继承自FrameLayout,id="action_bar_activity_content"

以上,DecorView默认布局文件和SubDecor布局文件已经分析完毕,接下来看看SubDecor如何添加到DecorView里。

        //寻找subDecor子布局,命名为contentView
        final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);
        //找到window里content布局,实际上找的是DecorView里名为content的布局
        final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        if (windowContentView != null) {
            //挨个移除windowContentView的子View,并将之添加到contentView里
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }
            //把windowContentView id去掉,之前名为content
            windowContentView.setId(View.NO_ID);
            //将"content"名赋予contentView
            contentView.setId(android.R.id.content);
        }
        //把subDecor添加为Window的contentView,实际上添加为DecorView的子View。该方法后面再具体分析
        mWindow.setContentView(subDecor);

1、首先从subDecor里寻找R.id.action_bar_activity_content,属于subDecor子View,其继承自FrameLayout。
2、再从DecorView里寻找android.R.id.content,是FrameLayout
3、移除android.R.id.content里的子View,并将其添加到R.id.action_bar_activity_content里(当然此时content没有子View)
4、把"android.R.id.content"这名替换掉R.id.action_bar_activity_content
5、最后将subDecor添加到FrameLayout里,对就是名字被换掉了的布局。

此时DecorView和subDecor已经结合了,并且android.R.id.content也存在,我们在setContentView(xx)里设置的layout会被添加到android.R.id.content里。

状态栏(背景)和导航栏(背景)

前面只是分析了LinearLayout及其子View的构造,而DecorView还有另外两个子View:状态栏(背景)/导航栏(背景)没有提及,接下来看看它们是如何关联上的。
既然是DecorView的子View,那么必然有个addView()的过程,搜索后确定如下方法:

DecorView.java
    private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color,
                                    int dividerColor, int size, boolean verticalBar, boolean seascape, int sideMargin,
                                    boolean animate, boolean force) {
        View view = state.view;
        //确定View的宽高
        int resolvedHeight = verticalBar ? LayoutParams.MATCH_PARENT : size;
        int resolvedWidth = verticalBar ? size : LayoutParams.MATCH_PARENT;
        //确定View的Gravity
        int resolvedGravity = verticalBar
                ? (seascape ? state.attributes.seascapeGravity : state.attributes.horizontalGravity)
                : state.attributes.verticalGravity;

        if (view == null) {
            if (showView) {
                //构造View
                state.view = view = new View(mContext);
                //设置View背景色
                setColor(view, color, dividerColor, verticalBar, seascape);
                //设置id
                view.setId(state.attributes.id);
                LayoutParams lp = new LayoutParams(resolvedWidth, resolvedHeight,
                        resolvedGravity);
                //添加到DecorView
                addView(view, lp);
            }
        } else {
            //省略...
        }
        //省略
    }

该方法根据条件添加子View到DecorView,调用该方法的地方有两处:

DecorView.java
    WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
        WindowManager.LayoutParams attrs = mWindow.getAttributes();
        //控制状态栏、导航栏标记
        int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();

        if (!mWindow.mIsFloating || isImeWindow) {
            //insets记录着状态栏、导航栏、高度
            if (insets != null) {
                //四个边界的偏移
                mLastTopInset = getColorViewTopInset(insets.getStableInsetTop(),
                        insets.getSystemWindowInsetTop());
                mLastBottomInset = getColorViewBottomInset(insets.getStableInsetBottom(),
                        insets.getSystemWindowInsetBottom());
                mLastRightInset = getColorViewRightInset(insets.getStableInsetRight(),
                        insets.getSystemWindowInsetRight());
                mLastLeftInset = getColorViewRightInset(insets.getStableInsetLeft(),
                        insets.getSystemWindowInsetLeft());
                //省略..
            }
            //省略
            //导航栏高度
            int navBarSize = getNavBarSize(mLastBottomInset, mLastRightInset, mLastLeftInset);
            //添加/设置导航栏
            updateColorViewInt(mNavigationColorViewState, sysUiVisibility,
                    calculateNavigationBarColor(), mWindow.mNavigationBarDividerColor, navBarSize,
                    navBarToRightEdge || navBarToLeftEdge, navBarToLeftEdge,
                    0 /* sideInset */, animate && !disallowAnimate,
                    mForceWindowDrawsBarBackgrounds);
            //添加设置状态栏
            updateColorViewInt(mStatusColorViewState, sysUiVisibility,
                    calculateStatusBarColor(), 0, mLastTopInset,
                    false /* matchVertical */, statusBarNeedsLeftInset, statusBarSideInset,
                    animate && !disallowAnimate,
                    mForceWindowDrawsBarBackgrounds);
        }
        //省略 主要和和全屏、隐藏等属性相关的
        
        //mContentRoot是DecorView的第一个子View
        //也即是LinearLayout,根据状态栏、导航栏高度调整LinearLayout高度
        if (mContentRoot != null
                && mContentRoot.getLayoutParams() instanceof MarginLayoutParams) {
            MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams();
            if (lp.topMargin != consumedTop || lp.rightMargin != consumedRight
                    || lp.bottomMargin != consumedBottom || lp.leftMargin != consumedLeft) {
                lp.topMargin = consumedTop;
                lp.rightMargin = consumedRight;
                lp.bottomMargin = consumedBottom;
                lp.leftMargin = consumedLeft;
                mContentRoot.setLayoutParams(lp);
            }
        }
        return insets;
    }

提取要点如下:

  • 状态栏、导航栏是属于View,而不是ViewGroup。因此不能再添加任何子View,这也就是为什么称为:状态栏背景,导航栏背景的原因。实际上,DecorView里设置的这两个背景是为了占位使用的。
  • 状态栏、导航栏高度是系统确定的,在ViewRootImpl->setView(xx),获取到其边界属性。
  • DecorView有三个子View,LinearLayout(内容)、状态栏、导航栏。LinearLayout根据后两者状态调整自身的LayoutParms。比如此时LinearLayout bottomMargin=126(导航栏高度)。
  • 重点:DecorView只是给状态栏和导航栏预留位置,俗称背景,我们可以操作背景,但不能操作内容。真正的内容,比如电池图标、运营商图标等是靠系统填充上去的

再用图表示状态栏、导航栏添加流程:

image.png

ViewRootImpl相关请查看:Android Activity创建到View的显示过程

状态栏/导航栏 如何确定位置呢?

    public static final ColorViewAttributes STATUS_BAR_COLOR_VIEW_ATTRIBUTES =
            new ColorViewAttributes(SYSTEM_UI_FLAG_FULLSCREEN, FLAG_TRANSLUCENT_STATUS,
                    Gravity.TOP, Gravity.LEFT, Gravity.RIGHT,
                    Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME,
                    com.android.internal.R.id.statusBarBackground,
                    FLAG_FULLSCREEN);

    public static final ColorViewAttributes NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES =
            new ColorViewAttributes(
                    SYSTEM_UI_FLAG_HIDE_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION,
                    Gravity.BOTTOM, Gravity.RIGHT, Gravity.LEFT,
                    Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME,
                    com.android.internal.R.id.navigationBarBackground,
                    0 /* hideWindowFlag */);

预先设置属性,在updateColorViewInt(xx)设置View的Gravity。
导航栏:Gravity.BOTTOM
状态栏:Gravity.TOP
这样,导航栏和状态栏在DecorView里的位置确定了。

DecorView子View位置与大小的确定

DecorView三个直接子View添加流程已经确定,通过Layout Inspector看看其大小与位置:


image.png

从上图两个标红的矩形框分析:
LinearLayout 上边界是顶到屏幕,而下边界的与导航栏的顶部平齐,而状态栏是盖在LinearLayout上的,这也就是为什么我们可以设置沉浸式状态栏的原因。
ContentFrameLayout包含了内容区域,ContentFrameLayout上边界与标题栏底部对齐,下边界充满父控件。
来看看代码里如何确定LinearLayout和FrameLayout位置:

#View.java
    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
        if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {
            //fitSystemWindows(xx)里面调用fitSystemWindowsInt(xx)
            if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {
                return insets.consumeSystemWindowInsets();
            }
        } else {
            if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {
                return insets.consumeSystemWindowInsets();
            }
        }
        return insets;
    }

    private boolean fitSystemWindowsInt(Rect insets) {
        //对应属性android:fitsSystemWindows="true"
        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);
            }
            boolean res = computeFitSystemWindows(insets, localInsets);
            mUserPaddingLeftInitial = localInsets.left;
            mUserPaddingRightInitial = localInsets.right;
            //最终根据insets来设定该View的padding
            //设置padding,这里是设置paddingTop
            internalSetPadding(localInsets.left, localInsets.top,
                    localInsets.right, localInsets.bottom);
            return res;
        }
        return false;
    }

LinearLayout设置了android:fitsSystemWindows="true",当状态栏展示的时候,需要将LinearLayout设置为适配状态栏,此处设置paddingTop="状态栏高度"
加上之前设置的marginBottom="导航栏高度”,这就确定了LinearLayout位置。

ContentFrameLayout父控件是ActionBarOverlayLayout,因此它的位置受父控件控制,ActionBarOverlayLayout计算标题栏占的位置,而后设置ContentFrameLayout marginTop属性。

针对上面的布局,对应的用图说话:


image.png

常见的获取DecorView各个区块大小的方法

既然知道了DecorView各个子View的布局,当然就有相应的方法获取其大小。

DecorView的尺寸

只要能获取到DecorView对象,一切都不在话下。
常见的通过Activity或者View获取:
Activity:

getWindow().getDecorView()

View:

getRootView()

导航栏/状态栏尺寸:

导航栏/状态栏高度是由系统确定的,固化在资源字段里:

    public static int getStatusBarHeight(Context context) {
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        return context.getResources().getDimensionPixelSize(resourceId);
    }
    public static int getNavigationBarHeight(Context context) {
        Resources resources = context.getResources();
        int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
        int height = resources.getDimensionPixelSize(resourceId);
        return height;
    }

总结

两篇文章分析了DecorView创建到展示一些布局细节。了解了DecorView的构成,我们做出一些效果更得心应手,如:状态栏沉浸/隐藏、Activity侧滑关闭、自定义通用标题栏等。
注:以上关于DecorView、subDecor、标题栏、布局文件和区块尺寸的选择是基于当前的demo的。可能你所使用的主题、设置的属性和本文不同导致布局效果差异,请注意甄别
源码基于:Android 10.0

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容