自定義控制元件(一) Activity的構成(PhoneWindow、DecorView)

飯小龍發表於2017-12-01

系列文章傳送門 (持續更新中..) :

自定義控制元件(二) 從原始碼分析事件分發機制

自定義控制元件(三) 原始碼分析measure流程

自定義控制元件(四) 原始碼分析 layout 和 draw 流程


先看一張 Activity 的構成簡化圖

這裡寫圖片描述

  • 每一個Activity都包含一個Window物件,Window由它的唯一的子類PhoneWindow實現

  • PhoneWindow:將Decoriew設定為整個應用視窗的根View。它是Android中的最基本的視窗系 統,每個Activity 均會建立一個PhoneWindow物件,是Activity和整個View系統互動的介面。

  • DecorView:頂層檢視,將要顯示的具體內容呈現在PhoneWindow上. 它並不會向使用者呈現任何東西,它主要有如下幾個功能,可能不全:

  • A. Dispatch ViewRoot分發來的key、touch、trackball等外部事件;

  • B. DecorView有一個直接的子View,我們稱之為System Layout,這個View是從系統的Layout.xml中解析出的,它包含當前UI的風格,如是否帶title、是否帶process bar等。可以稱這些屬性為Window decorations。

  • C. 作為PhoneWindow與ViewRoot之間的橋樑,ViewRoot通過DecorView設定視窗屬性。可以同 View view = getWindow().getDecorView() 獲取它;

  • D. DecorView只有一個子元素為LinearLayout。代表整個Window介面,包含通知欄,標題欄,內容顯示欄三塊區域。DecorView裡面TitleView:標題,可以設定requestWindowFeature(Window.FEATURE_NO_TITLE)取消掉. ContentView:是一個id為content的FrameLayout。我們平常在Activity使用的setContentView就是設定在這裡,也就是在FrameLayout上

1. 從setContentView()開始

大家都知道當我們寫Activity時會呼叫 setContentView() 方法來載入佈局, 讓我們來看一下內部實現:

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);  
    initWindowDecorActionBar();
}
複製程式碼

getWindow() :

public Window getWindow() {
    return mWindow;
}
複製程式碼

###Window 可以看到返回了一個 mWindow , 它的型別是 Window 類, 而 Window 是一個抽象類, setContentView() 也是一個抽象方法, 所以我們必須要找到它的實現子類

/**
 * 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.
 * 頂級視窗檢視和行為的抽象基類。它的例項作為一個頂級View被新增到Window Manager。
 * 它提供了一套標準的UI策略,例如背景,標題區域等。當你需要用到Window的時候,應該使
 * 用它的唯一實現子類PhoneWindow。
 */
public abstract class Window {
	...
	public abstract void setContentView(@LayoutRes int layoutResID);
	...
}
複製程式碼

而在 attach() 中 證實了 PhoneWindow 的初始化

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window);
}
複製程式碼

PhoneWindow

我們繼續看一下 PhoneWindow 這個類以及實現方法 setContentView()

public class PhoneWindow extends Window implements MenuBuilder.Callback {

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

	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) {
		       // 1. 初始化: 建立 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);   // Activity 轉場動畫相關
	      } else {
		      // 2. 填充佈局: 把 setContentView() 設定進來的佈局, 載入到 mContentParent,也就是 DecorView 中 id = content 的 FrameLayout
	          mLayoutInflater.inflate(layoutResID, mContentParent);   
	      }
	      mContentParent.requestApplyInsets();  // 讓DecorView的內容區域延伸到systemUi下方,防止在擴充套件時被覆蓋,達到全屏、沉浸等不同體驗效果。
	      
	      // 3. 通知 Activity 佈局改變
	      final Callback cb = getCallback();      
	      if (cb != null && !isDestroyed()) {
	          cb.onContentChanged();  // 觸發 Activity 的 onContentChanged() 方法
	      }
	      mContentParentExplicitlySet = true;
	  }
}

複製程式碼

可以看到當 mContentParent = null , 即當前內容佈局還沒有放置到視窗, 也就是第一次呼叫的時候, 會執行 installDecor(), 我們繼續去看下該方法

private void installDecor() {
      mForceDecorInstall = false;
      if (mDecor == null) {
	      // 生成 DecorView
          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) {
	       // 根據主題 theme 設定對應的 xml佈局檔案以及 Feature(包括style,layout,轉場動畫,
	       // 屬性等)到 DecorView中。並將 mContentParent 和 DecorView 佈局中的
	       // ID_ANDROID_CONTENT(com.android.internal.R.id.content)繫結
          mContentParent = generateLayout(mDecor); 
          
          // 省略                                        
          ...                                       

  }
複製程式碼

可以看到先呼叫 genaratDecor() 生成了 mDecorView

protected DecorView generateDecor(int featureId) {
	...
	
	return new DecorView(context, featureId, this, getAttributes());
}
複製程式碼

建立完了後執行了 generateLayout() , 在這個方法中會 根據主題 theme 設定對應的 xml佈局檔案以及 Feature(包括style,layout,轉場動畫,屬性等)到 DecorView中, 並在 DecorView 的xml 佈局中 findViewById() 獲取內容佈局的應用 contentView 並返回,即 mContentParent 就是 DecorView 中的內容佈局。由此我們可以知道為什麼要在setContentView 之前呼叫 requesetFeature 的原因。

這個方法有點長,我們大致看一下

protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.  -->  獲取當前的主題, 載入預設的資源和佈局
    
    /**
     * 下面的程式碼: 根據 theme 設定, 找到對應的 Feature(包括 style, layout, 轉場動畫, 屬性等)
     * / 
    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()));    
    }
	// 透明狀態列
    if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,    
            false)) {
        setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS
                & (~getForcedWindowFlags()));
    }
	
	// 其它資源的載入
	...
	...
	
	/** 
	 * 下面是新增布局到 DecorView. 
	 * 在前面我們看到已經呼叫 new DecorView 來建立一個例項, 但是 DecorView 本身是一個
	 * 繼承了 FrameLayout 的 ViewGroup, 建立完了後還沒有內容所以還需要對它建立相應的布
	 * 局. 而下面的程式碼則是根據使用者設定的 Feature 來建立相應的預設佈局主題.
	 * 
	 * 舉個例子:
	 * 如果我在setContentView之前呼叫了requestWindowFeature(Window.FEATURE_NO_TITLE),
	 * 這裡則會通過getLocalFeatures來獲取你設定的feature,進而選擇載入對應的佈局,此時則是載入
	 * 沒有標題欄的主題,對應的就是R.layout.screen_simple
	 * /
	 * 
    // Inflate the window decor.

    int layoutResource;
    int features = getLocalFeatures();
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        layoutResource = R.layout.screen_swipe_dismiss;
    } (// 省略各種 else if 判斷){
	    layoutResource = ...;
    }else {
        // Embedded, so no decoration is needed.
        layoutResource = R.layout.screen_simple;
    }

    mDecor.startChanging();
    // 把相應的佈局建立並新增到 DecorView 中
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);     
	// 從佈局中獲取 R.id.content
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);  
    
	...
	
	// 配置 DecorView 完成
    mDecor.finishChanging();   

    return contentParent;
}

複製程式碼

可以看到 在 else{} 中載入的是沒有標題欄的主題,對應的就是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>
複製程式碼

可以看到xml佈局中根佈局是 LinearLayout, 包含兩個子元素, 因為可以 no_title , 所以第一個是 ViewStub, 第二個子元素 id : content , 則是對應之前程式碼中的 mContentParent, 也就是 generateLayout() 返回的物件, 即 setContentView() 設定的內容就是新增到這個 FrameLayout 中。

我們繼續回到 setContentView() . 在方法的最後通過 cb.onContentChanged() 來通知介面改變的。Callback 是 Window 的內部介面,裡面宣告瞭當介面更改觸控時呼叫的各種方法, 並在Activity 中實現了這個介面, 並且實現的方法是空的,所以我們可以通過重寫這個方法, 來監聽佈局內容的改變了

public void onContentChanged() {
}
複製程式碼

參考文章: Android視窗機制 Android View體系(六)從原始碼解析Activity的構成

如果覺得對你有幫助, 請點個贊再走吧~

相關文章