Android view 部分 setContentView 的前因後果

ostracod發表於2017-03-24

在我們開發過程中,我們有的時候會碰到一些眼熟的單詞,如Window、PhoneWindow、DecorView、ViewGroup等等名詞,雖然不知道它們都包含什麼意思,但是經常會碰到,作為一個準備進階的Android程式設計師,我們有必要了解一下前因後果,接下來我們便一一瞭解這些名詞。首先我們從最常用的部分-setContentView學起。

學習工具

//開發工具
1、Android Studio

Android Studio 2.2.3
Build #AI-145.3537739, built on December 2, 2016
JRE: 1.8.0_112-release-b05 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o

//原始碼環境
2、Android API -25
compileSdkVersion 25
buildToolsVersion "25.0.2"複製程式碼

開始學習

首先我們從最常見的setContentView開始分析原始碼。

@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //最常見的開始分析,進入詳細程式碼
        setContentView(R.layout.activity_set_content_view_learn);
    }複製程式碼

進入到了詳細程式碼中我們會發現其實我們進入到了activity類程式碼中了。

//程式碼清單Activity.java

 /**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     *
     * @param layoutResID Resource ID to be inflated.
     *
     * @see #setContentView(android.view.View)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
    public void setContentView(@LayoutRes int layoutResID) {

        //呼叫Window類中的方法setContentView。
        getWindow().setContentView(layoutResID);

        //無關程式碼,初始化actionBar相關東東...
        initWindowDecorActionBar();
    }複製程式碼

首先我們會發現,我們先呼叫getWindow獲取window物件,然後再呼叫Window類中的setContentView方法,我們可以發現Window類其實是一個抽象類,所以肯定會有它的實現類,在這裡,它的實現類就是PhoneWindow類。在這裡,我們終於碰到了一個經常會提到的類PhoneWindow。

//程式碼清單 抽象類Window
public abstract class Window {
    //省略相關程式碼...
}

//程式碼清單 Window實現類,PhoneWindow
public class PhoneWindow extends Window implements MenuBuilder.Callback {
    //省略相關程式碼...
}複製程式碼

從上面的類申明當中我們就可以發現,PhoneWindow類其實是抽象類Window的實現類,所以對於setContentView方法,其實我們就得到它的實現類PhoneWindow中去檢視。

//程式碼清單 PhoneWindow.java

// This is the view in which the window contents are placed. It is either mDecor itself, or a child of mDecor where the contents go.

//變數申明
ViewGroup mContentParent;

@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        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 {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }複製程式碼

在PhoneWindow這個具體的實現類中,我們可以看到的流程是首先判斷mContentParent是不是等於null,是的話就加在installDecor()方法,不是的話再判斷是否使用了這個FEATURE_CONTENT_TRANSITIONS的flag,如果沒有的話則mContentParent刪除其中所有的view。接下來再判斷是否需要通過LayoutInflater.inflate將我們傳入的layout放置到mContentParent中。

其實我們可以稍微預測下installDecor()的功能,大概就是初始化mContentParent這個變數。而mContentParent這個變數就是包裹我們設定的整個xml佈局內容的ViewGroup。

最後就是呼叫了一個介面Callback裡面的方法。我們可以發現其實Activity實現了這個介面,但是onContentChanged這個介面方法在Activity中是一個空實現,這不是重點。

接下來,我們繼續研究installDecor()中的原始碼~

//程式碼清單 PhoneWindow中的installDecor方法

//申明變數
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            //省略無關程式碼...
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

            //省略無關程式碼...
            } else {
                mTitleView = (TextView) findViewById(R.id.title);
                //設定是否需要標題
                if (mTitleView != null) {
                    if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                        final View titleContainer = findViewById(R.id.title_container);
                        if (titleContainer != null) {
                            titleContainer.setVisibility(View.GONE);
                        } else {
                            mTitleView.setVisibility(View.GONE);
                        }
                        mContentParent.setForeground(null);
                    } else {
                        mTitleView.setText(mTitle);
                    }
                }
            }

            //省略無關程式碼...

        }
    }複製程式碼

從上面程式碼我們可以看出大概的流程。首先通過generateDecor(-1)來初始化一下mDecor,這個mDecor是DecorView的物件,看吧,這裡面已經出現了這個常見名詞,等等我們來分析一下DecorView這個類的作用。

接下來,我們用mDecor這個物件通過generateLayout(mDecor)來初始化mContentParent這個物件。然後我們便可以通過findViewById這個方法來獲取相關的控制元件了。

//程式碼清單 window.java類獲取相關控制元件

@Nullable
    public View findViewById(@IdRes int id) {
        return getDecorView().findViewById(id);
    }複製程式碼

在這裡的getDecorView()方法其實就是獲取mDecor這個物件。接下來我們開始分析DecorView這個類。入口點就是我們剛剛在installDecor方法中初始化mDecor這個物件的地方mDecor = generateDecor(-1)。

//程式碼清單 DecorView.java類

protected DecorView generateDecor(int featureId) {

        Context context;
        //沒什麼鳥用的無關程式碼,省略...
        return new DecorView(context, featureId, this, getAttributes());
    }複製程式碼

從generateDecor中我們沒發現什麼有用的東西,我們繼續分析相關程式碼。接下來我們分析mContentParent = generateLayout(mDecor);

從這個方法名便能看出個大概了,它是生成佈局,然後賦值給mContentParent,我們進入到方法中詳細瞭解一下整個佈局生成過程。

//程式碼清單 generateLayout方法流程

    protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.

        //設定當前activity的theme
        TypedArray a = getWindowStyle();

        //省略無關程式碼...
        //首先通過WindowStyle中設定的各種屬性,對Window進行requestFeat
        mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
        int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
                & (~getForcedWindowFlags());
        if (mIsFloating) {
            setLayout(WRAP_CONTENT, WRAP_CONTENT);
            setFlags(0, flagsToUpdate);
        } else {
            setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
        }

        //省略無關程式碼...
        //根據feature來載入對應的xml佈局檔案
        int layoutResource;
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        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;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
            // System.out.println("Title Icons!");
        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
            // Special case for a window with only a progress bar (and title).
            // XXX Need to have a no-title version of embedded windows.
            layoutResource = R.layout.screen_progress;
            // System.out.println("Progress!");
        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
            // Special case for a window with a custom title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogCustomTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = R.layout.screen_custom_title;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
            // If no other features and not embedded, only need a title.
            // If the window is floating, we need a dialog layout
            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,
                        R.layout.screen_action_bar);
            } else {
                layoutResource = R.layout.screen_title;
            }
            // System.out.println("Title!");
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        }


        //將上面獲取到的xml佈局載入到mDecor物件中
        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
            ProgressBar progress = getCircularProgressBar(false);
            if (progress != null) {
                progress.setIndeterminate(true);
            }
        }

        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            registerSwipeCallbacks();
        }

        // Remaining setup -- of background and title -- that only applies
        // to top-level windows.
        if (getContainer() == null) {
            final Drawable background;
            if (mBackgroundResource != 0) {
                background = getContext().getDrawable(mBackgroundResource);
            } else {
                background = mBackgroundDrawable;
            }
            mDecor.setWindowBackground(background);

            final Drawable frame;
            if (mFrameResource != 0) {
                frame = getContext().getDrawable(mFrameResource);
            } else {
                frame = null;
            }
            mDecor.setWindowFrame(frame);

            mDecor.setElevation(mElevation);
            mDecor.setClipToOutline(mClipToOutline);

            if (mTitle != null) {
                setTitle(mTitle);
            }

            if (mTitleColor == 0) {
                mTitleColor = mTextColor;
            }
            setTitleColor(mTitleColor);
        }

        mDecor.finishChanging();

        return contentParent;
    }複製程式碼

上面這個方法流程我們能看出個大概,首先getWindowStyle在當前的Window的theme中獲取我們的Window中定義的屬性。然後就根據這些屬性的值,對我們的Window進行各種requestFeature,setFlags等等。

還記得我們平時寫應用Activity時設定的theme或者feature嗎(全屏啥的,NoTitle等)?我們一般是不是通過XML的android:theme屬性或者java的requestFeature()方法來設定的呢?譬如:

通過java檔案設定:
requestWindowFeature(Window.FEATURE_NO_TITLE);

通過xml檔案設定:
android:theme="@android:style/Theme.NoTitleBar"複製程式碼

對的,其實我們平時requestWindowFeature()設定的值就是在這裡通過getLocalFeature()獲取的;而android:theme屬性也是通過這裡的getWindowStyle()獲取的。

所以這裡就是解析我們為Activity設定theme的地方,至於theme一般可以在AndroidManifest裡面進行設定。接下來,通過對features和mIsFloating的判斷,為layoutResource進行賦值,至於值可以為R.layout.screen_custom_title;R.layout.screen_action_bar;等等。至於features,除了theme中設定的,我們也可以在Activity的onCreate的setContentView之前進行requestFeature,也解釋了,為什麼需要在setContentView前呼叫requestFeature設定全屏什麼的。

最後通過我們得到了layoutResource,然後將它載入給mDecor物件,這個mDecor物件其實是一個FrameLayout,它的中心思想就是根據theme或者我們在setContentView之前設定的Feature來獲取對應的xml佈局,然後通過mLayoutInflater轉化為view,賦值給mDecor物件,這些佈局檔案中都包含一個id為content的FrameLayout,最後將其引用返回給mContentParent。

//程式碼清單 xml佈局檔案,包含id為content的FrameLayout佈局
<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>複製程式碼

我們用word來畫一個簡單的示意圖

Android view 部分 setContentView 的前因後果

到這裡我們分析了生成佈局後的大概流程,這樣生成了佈局後我們接著幹嗎呢?請回看當初的程式碼:

//程式碼清單 PhoneWindow.java
 @Override
    public void setContentView(int layoutResID) {

       //我們上面分析的生成系統xml佈局的流程
        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 {

            //將我們自己寫的xml載入到我們上面獲取到的裡面包含id為content的xml佈局中去,並賦值給mContentParent
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
       //省略無關程式碼...
    }複製程式碼

我們剛剛寫了一個系統生成的xml佈局就是包含id為content的佈局:

<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" />複製程式碼

其實這就是我們將自己設定的xml佈局裝載進這個佈局中。這就是整個setContentView所做的工作。

最後我們來總結一下全部流程,用一個圖來表示,更方面直接:

Android view 部分 setContentView 的前因後果

Android中有一個成員叫Window,Window是一個抽象類,提供了繪製視窗的一組通用API,PhoneWindow繼承自Window,是Window的具體實現類,PhoneWindow中有一個私有成員DecorView,這個DecorView物件是所有應用Activity頁面的根View,DecorView繼承自FrameLayout,在內部根據使用者設定的Activity的主題(theme)對FrameLayout進行修飾,為使用者提供給定Theme下的佈局樣式。一般情況下,DecorView中包含一個用於顯示Activity標題的TitleView和一個用於顯示內容的ContentView。

Android view 部分 setContentView 的前因後果

可以看出,DecorView中包含一個Vertical的LinearLayout佈局檔案,檔案中有兩個FrameLayout,上面一個FrameLayout用於顯示Activity的標題,下面一個FrameLayout用於顯示Activity的具體內容,也就是說,我們通過setContentView方法載入的佈局檔案View將顯示在下面這個FrameLayout中

首先初始化mDecor,即DecorView為FrameLayout的子類。就是我們整個視窗的根檢視了。然後,根據theme中的屬性值,選擇合適的佈局,通過infalter.inflater放入到我們的mDecor中。在這些佈局中,一般會包含ActionBar,Title,和一個id為content的FrameLayout。最後,我們在Activity中設定的佈局,會通過infalter.inflater壓入到我們的id為content的FrameLayout中去。


參考

1、http://blog.csdn.net/lmj623565791/article/details/41894125

2、http://blog.csdn.net/yanbober/article/details/45970721/

3、http://www.2cto.com/kf/201409/331824.html


關於作者

github: github.com/crazyandcod…
部落格: crazyandcoder.github.io

相關文章