前言
上篇分析了DecorView創(chuàng)建過(guò)程轻要,大致說(shuō)了其子View構(gòu)成链嘀,還剩下一些疑點(diǎn),本篇繼續(xù)分析。
上篇文章:Android DecorView 一窺全貌(上)
通過(guò)本篇文章聂沙,你將了解到:
1、DecorView各個(gè)子View具體布局內(nèi)容
2初嘹、狀態(tài)欄(背景)和導(dǎo)航欄(背景)如何添加到DecorView里
3及汉、DecorView子View位置與大小的確定
4、常見(jiàn)的獲取DecorView各個(gè)區(qū)塊大小的方法
DecorView各個(gè)子View具體布局內(nèi)容
照舊屯烦,打開(kāi)Tools->Layout Inspector
此時(shí)坷随,DecorView有三個(gè)子View,分別是LinearLayout驻龟、navigationBarBackground温眉、statusBarBackground。
默認(rèn)DecorView布局
先來(lái)看看LinearLayout翁狐,之前分析過(guò)加載DecorView時(shí)类溢,根據(jù)不同的feature確定不同的布局,我們的demo加載的是默認(rèn)布局:R.layout.screen_simple露懒。
這是系統(tǒng)自帶的布局文件闯冷,在哪找呢?
切換到Project模式——>找到External Libraries——>對(duì)應(yīng)的編譯API——>res library root——>layout文件夾下——>尋找對(duì)應(yīng)的布局名
R.layout.screen_simple布局內(nèi)容
<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>
幾點(diǎn)有價(jià)值的地方:
- 該LinearLayout方向是垂直的懈词,有個(gè)屬性android:fitsSystemWindows="true"(后續(xù)需要用到)
- ViewStub是占位用的蛇耀,默認(rèn)是Gone,先不管
- 有個(gè)id="content"的FrameLayout坎弯,是不是有點(diǎn)熟悉纺涤?
再來(lái)看看實(shí)際的layout展示:
正好和LinearLayout對(duì)應(yīng),ViewStub也對(duì)得上抠忘,但是明明布局文件里的FrameLayout是沒(méi)有子View的撩炊,實(shí)際怎么會(huì)有呢?當(dāng)然是中途動(dòng)態(tài)添加進(jìn)去的褐桌。
SubDecor
之前分析過(guò),DecorView創(chuàng)建成功后象迎,又繼續(xù)加載了一個(gè)布局:R.layout.abc_screen_toolbar荧嵌,并賦予subDecor變量,最后將subDecor里的某個(gè)子View添加到DecorView里砾淌。那么該布局文件在哪找呢啦撮?按照上面的方法,你會(huì)發(fā)現(xiàn)layout里并沒(méi)有對(duì)應(yīng)的布局文件汪厨。
實(shí)際上加載R.layout.abc_screen_toolbar是由AppCompatDelegateImpl.java完成的赃春,而該類屬于androidx.appcompat.app包,因此該尋找androidx里資源文件
R.layout.abc_screen_toolbar布局內(nèi)容:
<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>
同樣提取幾個(gè)關(guān)鍵信息:
- ActionBarOverlayLayout 繼承自ViewGroup劫乱,id="decor_content_parent"织中,同樣有個(gè)屬性:android:fitsSystemWindows="true"
- ActionBarContainer顧名思義是容納ActionBar的锥涕,id="action_bar_container",android:gravity="top"狭吼。繼承自FrameLayout层坠。有兩個(gè)子View,一個(gè)是ToolBar刁笙,另一個(gè)是ActionBar∑苹ǎ現(xiàn)在高版本都使用ToolBar替代ActionBar。
ActionBarOverlayLayout還有個(gè)子View
<include layout="@layout/abc_screen_content_include"/>
其內(nèi)容為:
<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默認(rèn)布局文件和SubDecor布局文件已經(jīng)分析完畢,接下來(lái)看看SubDecor如何添加到DecorView里摘悴。
//尋找subDecor子布局峭梳,命名為contentView
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
//找到window里content布局,實(shí)際上找的是DecorView里名為content的布局
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
//挨個(gè)移除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,實(shí)際上添加為DecorView的子View叉橱。該方法后面再具體分析
mWindow.setContentView(subDecor);
1挫以、首先從subDecor里尋找R.id.action_bar_activity_content,屬于subDecor子View窃祝,其繼承自FrameLayout掐松。
2、再?gòu)腄ecorView里尋找android.R.id.content粪小,是FrameLayout
3大磺、移除android.R.id.content里的子View,并將其添加到R.id.action_bar_activity_content里(當(dāng)然此時(shí)content沒(méi)有子View)
4探膊、把"android.R.id.content"這名替換掉R.id.action_bar_activity_content
5杠愧、最后將subDecor添加到FrameLayout里,對(duì)就是名字被換掉了的布局逞壁。
此時(shí)DecorView和subDecor已經(jīng)結(jié)合了流济,并且android.R.id.content也存在,我們?cè)趕etContentView(xx)里設(shè)置的layout會(huì)被添加到android.R.id.content里腌闯。
狀態(tài)欄(背景)和導(dǎo)航欄(背景)
前面只是分析了LinearLayout及其子View的構(gòu)造绳瘟,而DecorView還有另外兩個(gè)子View:狀態(tài)欄(背景)/導(dǎo)航欄(背景)沒(méi)有提及,接下來(lái)看看它們是如何關(guān)聯(lián)上的姿骏。
既然是DecorView的子View糖声,那么必然有個(gè)addView()的過(guò)程,搜索后確定如下方法:
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) {
//構(gòu)造View
state.view = view = new View(mContext);
//設(shè)置View背景色
setColor(view, color, dividerColor, verticalBar, seascape);
//設(shè)置id
view.setId(state.attributes.id);
LayoutParams lp = new LayoutParams(resolvedWidth, resolvedHeight,
resolvedGravity);
//添加到DecorView
addView(view, lp);
}
} else {
//省略...
}
//省略
}
該方法根據(jù)條件添加子View到DecorView,調(diào)用該方法的地方有兩處:
DecorView.java
WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
WindowManager.LayoutParams attrs = mWindow.getAttributes();
//控制狀態(tài)欄蘸泻、導(dǎo)航欄標(biāo)記
int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();
if (!mWindow.mIsFloating || isImeWindow) {
//insets記錄著狀態(tài)欄琉苇、導(dǎo)航欄、高度
if (insets != null) {
//四個(gè)邊界的偏移
mLastTopInset = getColorViewTopInset(insets.getStableInsetTop(),
insets.getSystemWindowInsetTop());
mLastBottomInset = getColorViewBottomInset(insets.getStableInsetBottom(),
insets.getSystemWindowInsetBottom());
mLastRightInset = getColorViewRightInset(insets.getStableInsetRight(),
insets.getSystemWindowInsetRight());
mLastLeftInset = getColorViewRightInset(insets.getStableInsetLeft(),
insets.getSystemWindowInsetLeft());
//省略..
}
//省略
//導(dǎo)航欄高度
int navBarSize = getNavBarSize(mLastBottomInset, mLastRightInset, mLastLeftInset);
//添加/設(shè)置導(dǎo)航欄
updateColorViewInt(mNavigationColorViewState, sysUiVisibility,
calculateNavigationBarColor(), mWindow.mNavigationBarDividerColor, navBarSize,
navBarToRightEdge || navBarToLeftEdge, navBarToLeftEdge,
0 /* sideInset */, animate && !disallowAnimate,
mForceWindowDrawsBarBackgrounds);
//添加設(shè)置狀態(tài)欄
updateColorViewInt(mStatusColorViewState, sysUiVisibility,
calculateStatusBarColor(), 0, mLastTopInset,
false /* matchVertical */, statusBarNeedsLeftInset, statusBarSideInset,
animate && !disallowAnimate,
mForceWindowDrawsBarBackgrounds);
}
//省略 主要和和全屏蟋恬、隱藏等屬性相關(guān)的
//mContentRoot是DecorView的第一個(gè)子View
//也即是LinearLayout翁潘,根據(jù)狀態(tài)欄、導(dǎo)航欄高度調(diào)整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;
}
提取要點(diǎn)如下:
- 狀態(tài)欄歼争、導(dǎo)航欄是屬于View拜马,而不是ViewGroup。因此不能再添加任何子View沐绒,這也就是為什么稱為:狀態(tài)欄背景俩莽,導(dǎo)航欄背景的原因。實(shí)際上乔遮,DecorView里設(shè)置的這兩個(gè)背景是為了占位使用的扮超。
- 狀態(tài)欄、導(dǎo)航欄高度是系統(tǒng)確定的蹋肮,在ViewRootImpl->setView(xx)出刷,獲取到其邊界屬性。
- DecorView有三個(gè)子View坯辩,LinearLayout(內(nèi)容)馁龟、狀態(tài)欄、導(dǎo)航欄漆魔。LinearLayout根據(jù)后兩者狀態(tài)調(diào)整自身的LayoutParms坷檩。比如此時(shí)LinearLayout bottomMargin=126(導(dǎo)航欄高度)。
- 重點(diǎn):DecorView只是給狀態(tài)欄和導(dǎo)航欄預(yù)留位置改抡,俗稱背景矢炼,我們可以操作背景,但不能操作內(nèi)容阿纤。真正的內(nèi)容句灌,比如電池圖標(biāo)、運(yùn)營(yíng)商圖標(biāo)等是靠系統(tǒng)填充上去的欠拾。
再用圖表示狀態(tài)欄胰锌、導(dǎo)航欄添加流程:
ViewRootImpl相關(guān)請(qǐng)查看:Android Activity創(chuàng)建到View的顯示過(guò)程
狀態(tài)欄/導(dǎo)航欄 如何確定位置呢?
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 */);
預(yù)先設(shè)置屬性清蚀,在updateColorViewInt(xx)設(shè)置View的Gravity匕荸。
導(dǎo)航欄:Gravity.BOTTOM
狀態(tài)欄:Gravity.TOP
這樣爹谭,導(dǎo)航欄和狀態(tài)欄在DecorView里的位置確定了枷邪。
DecorView子View位置與大小的確定
DecorView三個(gè)直接子View添加流程已經(jīng)確定,通過(guò)Layout Inspector看看其大小與位置:
從上圖兩個(gè)標(biāo)紅的矩形框分析:
LinearLayout 上邊界是頂?shù)狡聊唬逻吔绲呐c導(dǎo)航欄的頂部平齊东揣,而狀態(tài)欄是蓋在LinearLayout上的践惑,這也就是為什么我們可以設(shè)置沉浸式狀態(tài)欄的原因。
ContentFrameLayout包含了內(nèi)容區(qū)域嘶卧,ContentFrameLayout上邊界與標(biāo)題欄底部對(duì)齊尔觉,下邊界充滿父控件。
來(lái)看看代碼里如何確定LinearLayout和FrameLayout位置:
#View.java
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {
//fitSystemWindows(xx)里面調(diào)用fitSystemWindowsInt(xx)
if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {
return insets.consumeSystemWindowInsets();
}
} else {
if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {
return insets.consumeSystemWindowInsets();
}
}
return insets;
}
private boolean fitSystemWindowsInt(Rect insets) {
//對(duì)應(yīng)屬性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;
//最終根據(jù)insets來(lái)設(shè)定該View的padding
//設(shè)置padding芥吟,這里是設(shè)置paddingTop
internalSetPadding(localInsets.left, localInsets.top,
localInsets.right, localInsets.bottom);
return res;
}
return false;
}
LinearLayout設(shè)置了android:fitsSystemWindows="true"侦铜,當(dāng)狀態(tài)欄展示的時(shí)候,需要將LinearLayout設(shè)置為適配狀態(tài)欄钟鸵,此處設(shè)置paddingTop="狀態(tài)欄高度"
加上之前設(shè)置的marginBottom="導(dǎo)航欄高度”钉稍,這就確定了LinearLayout位置。
ContentFrameLayout父控件是ActionBarOverlayLayout棺耍,因此它的位置受父控件控制贡未,ActionBarOverlayLayout計(jì)算標(biāo)題欄占的位置,而后設(shè)置ContentFrameLayout marginTop屬性蒙袍。
針對(duì)上面的布局俊卤,對(duì)應(yīng)的用圖說(shuō)話:
常見(jiàn)的獲取DecorView各個(gè)區(qū)塊大小的方法
既然知道了DecorView各個(gè)子View的布局,當(dāng)然就有相應(yīng)的方法獲取其大小害幅。
DecorView的尺寸
只要能獲取到DecorView對(duì)象消恍,一切都不在話下。
常見(jiàn)的通過(guò)Activity或者View獲冉孟蕖:
Activity:
getWindow().getDecorView()
View:
getRootView()
導(dǎo)航欄/狀態(tài)欄尺寸:
導(dǎo)航欄/狀態(tài)欄高度是由系統(tǒng)確定的哺哼,固化在資源字段里:
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;
}
總結(jié)
兩篇文章分析了DecorView創(chuàng)建到展示一些布局細(xì)節(jié)。了解了DecorView的構(gòu)成叼风,我們做出一些效果更得心應(yīng)手取董,如:狀態(tài)欄沉浸/隱藏、Activity側(cè)滑關(guān)閉无宿、自定義通用標(biāo)題欄等茵汰。
注:以上關(guān)于DecorView、subDecor孽鸡、標(biāo)題欄蹂午、布局文件和區(qū)塊尺寸的選擇是基于當(dāng)前的demo的”蚣睿可能你所使用的主題豆胸、設(shè)置的屬性和本文不同導(dǎo)致布局效果差異,請(qǐng)注意甄別巷疼。
源碼基于:Android 10.0