Android view 部分 setContentView 的來龍去脈

在咱們開發過程當中,咱們有的時候會碰到一些眼熟的單詞,如Window、PhoneWindow、DecorView、ViewGroup等等名詞,雖然不知道它們都包含什麼意思,可是常常會碰到,做爲一個準備進階的Android程序員,咱們有必要了解一下來龍去脈,接下來咱們便一一瞭解這些名詞。首先咱們從最經常使用的部分-setContentView學起。javascript

學習工具

//開發工具
一、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

//源碼環境
二、Android API -25
compileSdkVersion 25
buildToolsVersion "25.0.2"複製代碼

開始學習

首先咱們從最多見的setContentView開始分析源碼。html

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

進入到了詳細代碼中咱們會發現其實咱們進入到了activity類代碼中了。java

//代碼清單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。android

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

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

從上面的類申明當中咱們就能夠發現,PhoneWindow類實際上是抽象類Window的實現類,因此對於setContentView方法,其實咱們就獲得它的實現類PhoneWindow中去查看。git

//代碼清單 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。github

最後就是調用了一個接口Callback裏面的方法。咱們能夠發現其實Activity實現了這個接口,可是onContentChanged這個接口方法在Activity中是一個空實現,這不是重點。windows

接下來,咱們繼續研究installDecor()中的源碼~app

//代碼清單 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這個類的做用。ide

接下來,咱們用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來畫一個簡單的示意圖

到這裏咱們分析了生成佈局後的大概流程,這樣生成了佈局後咱們接着幹嘛呢?請回看當初的代碼:

//代碼清單 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中有一個成員叫Window,Window是一個抽象類,提供了繪製窗口的一組通用API,PhoneWindow繼承自Window,是Window的具體實現類,PhoneWindow中有一個私有成員DecorView,這個DecorView對象是全部應用Activity頁面的根View,DecorView繼承自FrameLayout,在內部根據用戶設置的Activity的主題(theme)對FrameLayout進行修飾,爲用戶提供給定Theme下的佈局樣式。通常狀況下,DecorView中包含一個用於顯示Activity標題的TitleView和一個用於顯示內容的ContentView。

能夠看出,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中去。


參考

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

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

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


關於做者

github: github.com/crazyandcod…
博客: crazyandcoder.github.io

相關文章
相關標籤/搜索