Android窗口機制(二)Window,PhoneWindow,DecorView,setContentView源碼理解

Android窗口機制系列

Android窗口機制(一)初識Android的窗口結構
Android窗口機制(二)Window,PhoneWindow,DecorView,setContentView源碼理解
Android窗口機制(三)Window和WindowManager的創建與Activity
Android窗口機制(四)ViewRootImpl與View和WindowManager
Android窗口機制(五)最終章:WindowManager.LayoutParams和Token以及其他窗口Dialog,Toast

前篇文章中出現了PhoneWindow,DecorView這些類,如果是第一次見過的話,肯定會覺得陌生。這篇文章主要跟大家講解Window,PhoneWindow,DecorView他們的理解以及他們之間的聯系

Window

我們來看下源碼里面的說明

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
    ...
    @Nullable
    public View findViewById(@IdRes int id) {
        return getDecorView().findViewById(id);
    }

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

一個頂級窗口查看和行為的一個抽象基類。這個類的實例作為一個頂級View添加到Window Manager。它提供了一套標準的UI方法,比如添加背景,標題等等。當你需要用到Window的時候,你應該使用它的唯一實現類PhoneWindow。可以看到,Window是一個抽象基類,它提供了一系列窗口的方法,比如設置背景,標題等等,而它的唯一實現類則是PhoneWindow

PhoneWindow

Window的唯一實現類

public class PhoneWindow extends Window implements MenuBuilder.Callback {

    private final static String TAG = "PhoneWindow";

    ...

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

    // 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.
    private ViewGroup mContentParent;

    private ViewGroup mContentRoot;
    ...
}

可以看到,在PhoneWindow里面,出現了成員變量DecorView的而這里,DecorView則是PhoneWindow里面的一個內部類,它是繼承與FrameLayout

 private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {

        /* package */int mDefaultOpacity = PixelFormat.OPAQUE;

        /** The feature ID of the panel, or -1 if this is the application's DecorView */
        private final int mFeatureId;

        private final Rect mDrawingBounds = new Rect();

        private final Rect mBackgroundPadding = new Rect();

        private final Rect mFramePadding = new Rect();

        private final Rect mFrameOffsets = new Rect();
        ....
 }

既然是FrameLayout,也就可以加載布局文件,也就是說,我們那些標題欄,內容欄,頂級上看是加載在DecorView上的。而DecorView則是由PhoneWindow負責添加

兩者關系以及setContentView源碼流程

接下我們就從一個常見的方法中去認知他們之間的關系,那就是activity里面的setContentView,就是我們平常把布局內容顯示到界面上的一個方法。點擊activity.setContentView時

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

里面方法調用了getWindow().setContentView,而這個getWindow方法獲取的就是Activity上的Window

/**
     * Retrieve the current {@link android.view.Window} for the activity.
     * This can be used to directly access parts of the Window API that
     * are not available through Activity/Screen.
     *
     * @return Window The current window, or null if the activity is not
     *         visual.
     */
    public Window getWindow() {
        return mWindow;
    }

可以看到如果當前mWindow為null的話,則表示當前Activity不在窗口上,這里的mWindow.setContentView,實際上調用到的是它的實現類方法phoneWindow.setContentView

 @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) {
            //創建DecorView,并添加到mContentParent上
            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 {
            //將要加載的資源添加到mContentParent上
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            //回調通知表示完成界面加載
            cb.onContentChanged();
        }
    }

如果當前內容還未放置到窗口,此時mContentParent==null,也就是第一次調用的時候,調用那個installDecor方法。FEATURE_CONTENT_TRANSITIONS,則是標記當前內容加載有沒有使用過度動畫,也就是轉場動畫。如果內容已經加載過,并且不需要動畫,則會調用removeAllViews。添加完Content后如有設置了FEATURE_CONTENT_TRANSITIONS則添加Scene來過度啟動。否則mLayoutInflater.inflate(layoutResID, mContentParent);將我們的資源文件通過LayoutInflater對象轉換為View樹,并且添加至mContentParent視圖中。既然是第一次啟動則會調用到installDecor,從字面上看可以知道該方法用來添加DecorView,看下里面說明

private void installDecor() {
        if (mDecor == null) {
        //調用該方法創建new一個DecorView
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        //一開始DecorView未加載到mContentParent,所以此時mContentParent=null
        if (mContentParent == null) {
        //該方法將mDecorView添加到Window上綁定布局
            mContentParent = generateLayout(mDecor);

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);
                
                ...//添加其他資源
                ...//設置轉場動畫
        }
    }

可以看到該方法,先通過吊桶generateDecor創建DecorView

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

創建完后再通過調用generateLayout將setContentView的內容賦值到mContentParent,這個方法有點長,我們看下

 protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        //根據當前設置的主題來加載默認布局
        TypedArray a = getWindowStyle();
        //如果你在theme中設置了window_windowNoTitle,則這里會調用到,其他方法同理,
        //這里是根據你在theme中的設置去設置的
        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_windowFullscreen, false)) {
            setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
        }
        
        ...//省略其他加載資源
        
        // 添加布局到DecorView,前面說到,DecorView是繼承與FrameLayout,它本身也是一個ViewGroup,而我們前面創建它的時候,只是調用了new DecorView,此時里面并無什么東西。而下面的步奏則是根據用戶設置的Feature來創建相應的默認布局主題。舉個例子,如果我在setContentView之前調用了requestWindowFeature(Window.FEATURE_NO_TITLE),這里則會通過getLocalFeatures來獲取你設置的feature,進而選擇加載對應的布局,此時則是加載沒有標題欄的主題,對應的就是R.layout.screen_simple

        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 {
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        }

        mDecor.startChanging();
        //選擇對應布局創建添加到DecorView中
        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        ...
        return contentParent;
    }

首先generateLayout會根據當前用戶設置的主題去設置對應的Feature,接著,根據對應的Feature來選擇加載對應的布局文件,(Window.FEATURE_NO_TITLE)接下來通過getLocalFeatures來獲取你設置的feature,進而選擇加載對應的布局,這也就是為什么我們要在setContentView之前調用requesetFeature的原因。此時則是加載沒有標題欄的主題,對應的就是R.layout.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
         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>

可以看到是LinearLayout里面包含了兩個,因為設置可NoTitle,所以上面只有一個ViewStub,否則還有一個FrameLayout。也證明前面第一篇中說的,“DecorView只有一個子元素為LinearLayout。代表整個Window界面,包含通知欄,標題欄,內容顯示欄三塊區域。”注意FrameLayout里面的id,@android:id/content ,我們setContentView的內容就是添加到這個FrameLayout中。

generateLayout的返回是contentParent,而它的獲取則是ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);`

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

正是id為content的FrameLayout。之后我們setContentView則是添加在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) {
            //創建DecorView,并添加到mContentParent上
            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 {
            //將要加載的資源添加到mContentParent上
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            //回調通知表示完成界面改變
            cb.onContentChanged();
        }
    }

此時已經創建完DecorView并且獲取到mContentParent,接著就是將你setContentView的內容添加到mContentParent中,也就是

 mLayoutInflater.inflate(layoutResID, mContentParent);
 或者
 mContentParent.addView(view, params);

最后調用Callback來通知界面發生改變。Callback是Window里面的一個接口,里面聲明了當界面更改觸摸時調用的各種方法。這里的話,我們看下onContentChanged,在PhoneWindow里面并沒有看到onContentChanged的實現類,而我們又知道Activity本身又是加載在Window上的,我們看下Activity

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback { ... }

可以看到Activity里面實現了Window.Callback接口而里面onContentChanged則是空的,也就是我們可以通過重寫該方法來監聽布局內容的改變了

 public void onContentChanged() {
    }

小結

  • Window是一個抽象類,提供了各種窗口操作的方法,比如設置背景標題ContentView等等
  • PhoneWindow則是Window的唯一實現類,它里面實現了各種添加背景主題ContentView的方法,內部通過DecorView來添加頂級視圖
  • 每一個Activity上面都有一個Window,可以通過getWindow獲取
  • DecorView,頂級視圖,繼承與FramentLayout,setContentView則是添加在它里面的@id/content里
  • setContentView里面創建了DecorView,根據Theme,Feature添加了對應的布局文件
  • 當setContentView設置顯示后會回調Activity的onContentChanged方法

回顧

再看一下前一篇文章的結構圖,是不是就更好理解了呢。

Paste_Image.png

下一篇文章
Android窗口機制(三)Window和WindowManager的創建與Activity
http://www.lxweimin.com/p/6afb0c17df43

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容