前言
上篇分析了DecorView创建过程,大致说了其子View构成,还剩下一些疑点,本篇继续分析。
上篇文章:Android DecorView 一窥全貌(上)
通过本篇文章,你将了解到:
1、DecorView各个子View具体布局内容
2、状态栏(背景)和导航栏(背景)如何添加到DecorView里
3、DecorView子View位置与大小的确定
4、常见的获取DecorView各个区块大小的方法
DecorView各个子View具体布局内容
照旧,打开Tools->Layout Inspector
此时,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方向是垂直的,有个属性android:fitsSystemWindows="true"(后续需要用到)
- ViewStub是占位用的,默认是Gone,先不管
- 有个id="content"的FrameLayout,是不是有点熟悉?
再来看看实际的layout展示:
正好和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里资源文件
R.layout.abc_screen_toolbar布局内容:
同样提取几个关键信息:
- 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
其内容为:
- 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只是给状态栏和导航栏预留位置,俗称背景,我们可以操作背景,但不能操作内容。真正的内容,比如电池图标、运营商图标等是靠系统填充上去的。
再用图表示状态栏、导航栏添加流程:
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看看其大小与位置:
从上图两个标红的矩形框分析:
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属性。
针对上面的布局,对应的用图说话:
常见的获取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