問題描述
在最近的項(xiàng)目中,遇到一個(gè)奇葩問題。起初為了解決打開app白屏或者黑屏問題,在SplashActivity的Theme里面添加屬性:
<item name="android:windowBackground">@drawable/splash_activity_launch _bg</item>
drawable設(shè)置背景色為白色,并在中間放置了一張圖片
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/splash_logo" />
</item>
</layer-list>
為了不顯得突兀,在SplashActivity的布局activity_splash.xml中間也放置了一張相同的圖片
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rl_splash_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_splash_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/splash_logo" />
</RelativeLayout>
但奇怪的是兩張圖片居然錯(cuò)位了(兩張圖片為什么會(huì)一起顯示是由于背景色導(dǎo)致的,此次不必追究)。具體效果看圖:

這里不妨大膽猜測activity_splash.xml所代表的區(qū)域和windowBackground所代表的區(qū)域并不一致,那他們各自所代表的區(qū)域是什么呢,下面我們就跟隨源碼一步步的分析。
分析步驟
activity_splash.xml即通常說的內(nèi)容區(qū)域,繪制的控件都顯示在其中,一般我們通過setContentView()添加到activity中(必須繼承Activity,AppCompatActivity源碼不同),點(diǎn)擊去看一下源碼。里面調(diào)用了抽象類Window的抽象方法setContentView()
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public abstract class Window {
public abstract void setContentView(@LayoutRes int layoutResID);
}
搜索一下實(shí)現(xiàn)類,可以看到實(shí)現(xiàn)類為PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
去看一下實(shí)現(xiàn)類PhoneWindow的setContentView()方法,這里主要完成了兩件事,調(diào)用installDecor()方法和將傳入的layoutResID(就是activity根布局)布局添加到mContentParent中。mContentParent還不知道是什么,但是它肯定在installDecor()方法中被初始化了。
@Override
public void setContentView(int layoutResID) {
//第一次mContentParent是空值,執(zhí)行installDecor()方法
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//將傳入的布局layoutResID布局添加到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
installDecor()主要調(diào)用了兩個(gè)方法generateDecor()和generateLayout(),generateDecor()方法的返回值是mDecor ,generateLayout(mDecor)方法的返回值是mContentParent 。
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//第一次mDecor為空
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//此時(shí)mContentParent 被初始化
mContentParent = generateLayout(mDecor);
先看一下generateDecor()方法,這里直接new了一個(gè)DecorView,DecorView繼承FrameLayout 。
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
private final int mFeatureId;
private final Rect mDrawingBounds = new Rect();
private final Rect mBackgroundPadding = new Rect();
回過頭繼續(xù)看一下generateLayout()方法,此方法主要是根據(jù)樣式選擇相應(yīng)的布局、將此布局添加到mDecor中,并初始化mContentParent。留意下面的"ID_ANDROID_CONTENT "
protected ViewGroup generateLayout(DecorView decor) {
//獲取設(shè)置的Window樣式,這里說明設(shè)置全屏、隱藏標(biāo)題欄等必須在setContentView()之前
TypedArray a = getWindowStyle();
........
//下面的代碼,主要是根據(jù)樣式和屬性選擇對應(yīng)的布局,這個(gè)布局是什么,待會(huì)解釋;
int layoutResource;
int features = getLocalFeatures();
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
layoutResource = R.layout.screen_progress;
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
//含有ActionBar的布局
R.layout.screen_action_bar);
} else {
//含有TitleBar的布局
layoutResource = R.layout.screen_title;
}
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
//最常用布局
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
//將layoutResource 轉(zhuǎn)化為View
View in = mLayoutInflater.inflate(layoutResource, null);
//將View添加到Decor中
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
//注意這個(gè)ID: public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
//這里說明mContentParent對象就是ID_ANDROID_CONTENT代表的布局
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
上面提到了根據(jù)樣式Style選擇相應(yīng)的布局,但是這個(gè)布局到底是什么呢??梢杂肧earchEverything搜索R.layout.screen_simple、R.layout.screen_title、R.layout.screen_action_bar幾個(gè)常用的布局看一下。
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
<!--記住這個(gè)ID-->
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>
screen_title:含有TitleBar的布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:fitsSystemWindows="true">
<!-- Popout bar for action modes -->
<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:layout_width="match_parent"
android:layout_height="?android:attr/windowTitleSize"
style="?android:attr/windowTitleBackgroundStyle">
<TextView android:id="@android:id/title"
style="?android:attr/windowTitleStyle"
android:background="@null"
android:fadingEdge="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<FrameLayout
<!--記住這個(gè)ID-->
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
screen_action_bar:含有ActionBar的布局
<com.android.internal.widget.ActionBarOverlayLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/decor_content_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:splitMotionEvents="false"
android:theme="?attr/actionBarTheme">
<FrameLayout
<!--記住這個(gè)ID-->
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.android.internal.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:transitionName="android:action_bar"
android:touchscreenBlocksFocus="true"
android:keyboardNavigationCluster="true"
android:gravity="top">
<com.android.internal.widget.ActionBarView
android:id="@+id/action_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/actionBarStyle" />
<com.android.internal.widget.ActionBarContextView
android:id="@+id/action_context_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
style="?attr/actionModeStyle" />
</com.android.internal.widget.ActionBarContainer>
<com.android.internal.widget.ActionBarContainer android:id="@+id/split_action_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/actionBarSplitStyle"
android:visibility="gone"
android:touchscreenBlocksFocus="true"
android:keyboardNavigationCluster="true"
android:gravity="center"/>
</com.android.internal.widget.ActionBarOverlayLayout>
上面三個(gè)分別代表了常用布局、含有TitleBar的布局、含有ActionBar的布局,原來TitleBar、ActionBar這些也是寫在布局文件中的,其實(shí)一點(diǎn)也不神奇。來看一下這些布局的共性,會(huì)發(fā)現(xiàn)這些布局的根布局都是LinearLayout,且都有一個(gè)id為"@android:id/content"的FrameLayout,其實(shí)mContentParent其實(shí)就是布局里面的FrameLayout
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
現(xiàn)在來梳理一下:
- layoutResID----------添加到----------mContentParent
mLayoutInflater.inflate(layoutResID, mContentParent);
- mContentParent----------等于----------FrameLayout
//調(diào)用setContentView()方法就是把布局添加到FrameLayout中
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
- FrameLayout----------添加到----------mDecor
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
DecorView什么時(shí)候被添加到Window中呢?這里就不一步步的看了,在ActivityThread.java的handleResumeActivity()方法中。
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
if (!checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) {
return;
}
unscheduleGcIdler();
mSomeActivitiesChanged = true;
r = performResumeActivity(token, clearHide, reason);
if (r != null) {
final Activity a = r.activity;
if (localLOGV) Slog.v(
final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
try {
willBeVisible = ActivityManager.getService().willActivityBeVisible(
a.getActivityToken());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);//將DecorView添加到Window中
} else {
a.onWindowAttributesChanged(l);
}
}
DecorView為整個(gè)Window界面的最頂層View,且只含有一個(gè)子元素LinearLayout。也就是FrameLayout的根元素,如果不信的話,可以嘗試打開更多的布局,結(jié)果無一另外全是LinearLayout(ActionBar的根布局ActionBarOverlayLayout繼承LinearLayout)。

下面來區(qū)分幾個(gè)activity界面的常用概念,以便理解。

綠色區(qū)域:狀態(tài)欄StatusBar,高度計(jì)算如下:
public static int getStatusBarHeight(Context context) {
int statusBarHeight = 0;
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
}
return statusBarHeight;
}
紫色部分:ActionBar或者TitleBar ,高度計(jì)算如下
public static int getTitleBarHeight(Activity context) {
int top = context.getWindow().findViewById(Window.ID_ANDROID_CONTENT).getTop();
return top > getStatusBarHeight(context) ? top - getStatusBarHeight(context) : 0;
}
黃色部分:RootView(也叫內(nèi)容區(qū)域),高度計(jì)算如下
public static int getRootView(Activity context) {
return context.getWindow().findViewById(Window.ID_ANDROID_CONTENT).getHeight();
}
紅色部分:導(dǎo)航欄NavigationBar,高度計(jì)算如下
public static int getNavigationBarHeight(Context context) {
int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
return context.getResources().getDimensionPixelSize(resourceId);
}
應(yīng)用區(qū)域:內(nèi)容區(qū)域+紫色區(qū)域(RootView+TitleBar/ActionBar)
public static int getContentViewHeight(Activity context) {
Rect outRect = new Rect();
context.getWindow().getDecorView().getWindowVisibleDisplayFrame(outRect);
return outRect.height();
}
這里要重點(diǎn)說明一下,通常getDisplayMetrics().heightPixels方法拿到的分辨率的高度不一定是真的分辨率高度,具體詳情查看Android手機(jī)獲取屏幕分辨率高度因虛擬導(dǎo)航欄帶來的問題
到了這里我們就可以大致推斷DecorView的高度了
DecorViewHeight=RootView+TitleBar/ActionBar+StatusBarHeight;
或者
DecorViewHeight=RootView+TitleBar/ActionBar+StatusBarHeight+NavigationBarHeight;
這里是不是很困惑了?高度怎么把StatusBarHeight和NavigationBar算進(jìn)去了。原來StatusBar和NavigationBar都是系統(tǒng)UI,每一個(gè)Activity在繪制的時(shí)候都會(huì)預(yù)留空間給StatusBar和NavigationBar,占據(jù)DecorView空間但不屬于DecorView本身。那為什么DecorView高度有時(shí)包括NavigationBar有時(shí)不包括呢,這主要是由各個(gè)系統(tǒng)版本和Style決定的,具體源碼在何處現(xiàn)在沒有去分析。總之DecorView高度并不是固定的是可以動(dòng)態(tài)變化的,舉個(gè)栗子吧!
例子
1、首先隱藏StatusBar和NavigationBar
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
setContentView(R.layout.activity_splash);
2、在onWindowFocusChanged()方法中打印DecorView高度
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
Log.e("decorViewHeight ", getWindow().getDecorView().getHeight() + "");
Log.e("DisplayHeight ", UIUtil.getDisplayHeight(this) + "");
Log.e("RealMetricsHeight ", UIUtil.getRealMetrics(this) + "");
Log.e("stateBarHeight ", UIUtil.getStatusBarHeight(this) + "");
Log.e("TitleBarHeight ", UIUtil.getTitleBarHeight(this) + "");
Log.e("ActionBarHeight ", UIUtil.getActionBarHeight(this) + "");
Log.e("NavigationBarHeight ", UIUtil.getNavigationBarHeight(this) + "");
Log.e("rootViewHeight ", UIUtil.getRootView(this) + "");
Log.e("contentViewHeight", UIUtil.getContentViewHeight(this) + "");
}
3、打印日志如下
07-08 20:33:43.568 5731-5731/paradise.decoarview E/decorViewHeight: 1280
07-08 20:33:43.568 5731-5731/paradise.decoarview E/DisplayHeight: 1244
07-08 20:33:43.578 5731-5731/paradise.decoarview E/RealMetricsHeight: 1280
07-08 20:33:43.578 5731-5731/paradise.decoarview E/stateBarHeight: 38
07-08 20:33:43.578 5731-5731/paradise.decoarview E/TitleBarHeight: 46
07-08 20:33:43.578 5731-5731/paradise.decoarview E/ActionBarHeight: 0
07-08 20:33:43.578 5731-5731/paradise.decoarview E/NavigationBarHeight: 36
07-08 20:33:43.578 5731-5731/paradise.decoarview E/rootViewHeight: 1158
07-08 20:33:43.578 5731-5731/paradise.decoarview E/contentViewHeight: 1242
4、點(diǎn)擊一下屏幕,喚起NavigationBar、按返回鍵,打印日志如下
07-08 20:36:22.548 5731-5731/paradise.decoarview E/decorViewHeight: 1244
07-08 20:36:22.548 5731-5731/paradise.decoarview E/DisplayHeight: 1244
07-08 20:36:22.548 5731-5731/paradise.decoarview E/RealMetricsHeight: 1280
07-08 20:36:22.548 5731-5731/paradise.decoarview E/stateBarHeight: 38
07-08 20:36:22.548 5731-5731/paradise.decoarview E/TitleBarHeight: 46
07-08 20:36:22.548 5731-5731/paradise.decoarview E/ActionBarHeight: 0
07-08 20:36:22.548 5731-5731/paradise.decoarview E/NavigationBarHeight: 36
07-08 20:36:22.548 5731-5731/paradise.decoarview E/rootViewHeight: 1122
07-08 20:36:22.548 5731-5731/paradise.decoarview E/contentViewHeight: 1206
DecorView高度由1280變成了1244,而這個(gè)高度正好是NavigationBar高度。
問題解決
到了這里,一且都明白了。原來在本項(xiàng)目中
activity_splash.xml高度=RootView高度+ActionBarHeight/TitleBarHeight
WindowBackground高度=RootView高度+ActionBarHeight/TitleBarHeight+StatusBarHeight
多了一個(gè)StatusBarHeight,所以需要在初始化的時(shí)候RootView減去StatusBarHeight
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) rlSplashRoot.getLayoutParams();
params.topMargin = UIUtil.getStatusBarHeight();
rlSplashRoot.setLayoutParams(params);