DecorView高度問題

問題描述

在最近的項(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)致的,此次不必追究)。具體效果看圖:


matter.gif

這里不妨大膽猜測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)。

layout.png

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

app.png

綠色區(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);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容