天天看點

setContentView源碼分析

轉載請注明出處:http://blog.csdn.net/fishle123/article/details/50812266

在Activity中,常常第一件事就是在onCreate裡面調用setContentView來設定布局。大家都知道setContentView用來設定Activity的布局,但是有沒有研究過setContentView究竟做了哪些事呢?本文就一起看看setContentView是如何加載我們的布局的。看完之後大家就會知道為什麼使用requestWindowFeature設定視窗風格的時候一定要在setContentView之前調用。

1 Window相關的基礎知識

在Android裡面,Activity的内容就是一個Window,Dialog和Toast也都是通過Window來展示的。在實際使用當中并不能直接通路Window,需要通過WindowManager才能通路到Window,WindowManager提供了addView、updateViewLayout和removeView三個方法來管理Window中的View。Window類是一個抽象類,它唯一的實作是PhoneWindow。在PhoneWindow裡面有個屬性mDecor,它的類型是DecorView,DecorView是PhoneWindow的内部類。DecorView是FrameLayout的子類,包含整個PhoneWindow所有要顯示的View,包括系統的狀态欄、标題欄、以及Activity的布局等,下面通過一張圖來說明它們之間的關系,這張圖展示了Activity的預設視圖結構。

setContentView源碼分析

從這張圖可以很清楚看到,Android中看到的視圖都是通過Window來呈現,具體地是通過PhoneWindow來管理的。需要說明的是不同樣式風格的Window内部視圖結構可能會有所不同,比如有的Window沒有TitleBar,但是mContentParent是一定要有的。DecorView對應着Widnow要呈現的内容。我們通過setContentView設定的其實是mParentConent的子View,mParentContent對應的id是com.android.internal.R.id.content,是以使用setContentView就非常貼切了。

2 setContentView源碼分析

在onCreate裡面調用的是Activity的setContentView,setContentView有三個重載版本,它們的邏輯都是差不多的,這裡就看一下setContentView(int layoutResID)的源碼:

1)Activity的setContentView最終是調用Window的setContentView

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
           

這裡可以看到,Activity其實是調用Window的setContentView來加載布局的。

2)初始化DecorView--根據Window的樣式風格標明布局并加載,然後把Activity的布局添加到DecorView中

上面有提到Window類是一個抽象類,唯一實作了Window的是PhoneWindow。下面就是看一下PhoneWindow的setContentView:

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
           

首次調用setContentView的時候,mContentParent為null,需要調用installDecor來初始化mDecor。然後在第20行調用mLayoutInflater.inflate(layoutResID, mContentParent)将我們指定的布局添加到mDecor即添加到DecorView中。從這裡我們可以看到mContentParent就是setContentView(layoutResID)中layoutResID的父視圖,後面會看到mContentParent是DecorView的一部分。這裡看一下installDecor的實作:

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
    }
    //............................省略部分代碼
}
           

installDecor的代碼很長,這裡隻挑重要的看:首次調用installDecor的時候mDecor和mContentParent都為null,需要調用generateDecor和genreateLayout。我們先看generateDecor方法,從名字來看generateDecor裡面似乎會構造一個DecorView。

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}
           

實際上,在generateDecor裡面非常簡單,直接new了一個DecorView。再看一下DecorView的構造函數。

public DecorView(Context context, int featureId) {
    super(context);
    mFeatureId = featureId;

    mShowInterpolator = AnimationUtils.loadInterpolator(context,
            android.R.interpolator.linear_out_slow_in);
    mHideInterpolator = AnimationUtils.loadInterpolator(context,
            android.R.interpolator.fast_out_linear_in);

    mBarEnterExitDuration = context.getResources().getInteger(
            R.integer.dock_enter_exit_duration);
}
           

DecorView的構造函數隻作了一些簡單的初始化,這個時候DecorView還是空的。是以這個時候mParentContent還是null,是以在installDecor方法中調用完generateDecor建立mDecor後,會繼續調用genreateLayout來往DecorView中添加内容。genrateLayout會根據我們設定的視窗樣式來初始化DecorView。

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

    //先擷取Window的風格樣式
    TypedArray a = getWindowStyle();
    ...................................
    //下面的這段代碼都是擷取Window詳細的樣式設定,并指派給相應的feature
    //是否為懸浮框
    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);
    }
    //是否添加标題欄
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
        requestFeature(FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
        // Don't allow an action bar if there is no title.
        requestFeature(FEATURE_ACTION_BAR);
    }
    //沉浸式狀态欄
    if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
        requestFeature(FEATURE_ACTION_BAR_OVERLAY);
    }

    if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
        requestFeature(FEATURE_ACTION_MODE_OVERLAY);
    }

    //...........................省略其他樣式的處理,邏輯是類似的

    // Inflate the window decor.

    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!");
    }

    mDecor.startChanging();
    //根據樣式選擇好布局後,将相應的布局inflate為View
    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in;//mContentRoot儲存了整個視窗的内容
    //找到DecorView中ID為com.android.internal.R.id.content的view并指派給contentParent ,我們為Activity等設定的View都是contentParent 的子view,setContentView這個名字就顯得很貼切了。
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }
    //...............................................省略部分代碼
    //在installDecor中可以看到,generateLayout的傳回值有mContentParent來接收,
    //mContentParent就是setContentView(layoutResID)中layoutResID的父容器
    return contentParent;
}
           

上面這段代碼先擷取Window的樣式,然後根據樣式選擇相應的布局并inflate轉化成ViewGroup,并傳回mContentParent對應的ViewGroup。Window的樣式可以通過requestWindowFeature或者給Activity設定android:theme來指定。現在應該知道requestWindowFeature一定要在setContentView之前調用才能生效了吧。

3) DecorView完成初始化之後,通知Activity布局已經成功添加到mContentParent

完成上面的處理之後DecorView就建立好了,最後在PhoneWindow的setContentView中回調Activity的onContentChanged,通知Activity它指定的布局已經添加到mContentParent(DecorView)。回調過程如下:

final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
    cb.onContentChanged();
}
           

到此為止,DecorView已經建立并完成初始化,Activity的布局也添加到DecorView中,但是這個時候我們還看不到DecorView,因為它還沒有通過WindowManager添加到Window中。DecorView真正顯示是在ActivityThread的handleResumeActivity方法中,handleResumeActivity先調用Activity的onResume,然後調用Activity的makeVisible把DecorView添加到Window中。

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}
           

總結

最後總結一下setContentView将布局檔案添加并最終顯示的過程:

1)在PhoneWindow的setContentView中先調用installDecor初始化DecorView。具體的,在installDecor裡面先調用generateDecor建立一個DecorView,然後調用generateLayout根據Window的樣式風格選擇布局檔案并初始化DecorView;

2)通過mLayoutInflater将Activity的布局添加到mContentParent中;

3)回調Activity的onContentChanged,通知Activity布局檔案已經成功添加到DecorView中;

4)最後在ActivityThread的handleResumeActivity中先調用Activity的onResume,然後調用Activity的makeVisible把DecorView添加到Window中,并顯示到手機上。

前面三步都是在Activity的setContentView傳回之前完成。