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

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

自定義控件(二) 從源碼分析事件分發機制android

自定義控件(三) 源碼分析measure流程bash

自定義控件(四) 源碼分析 layout 和 draw 流程app


先看一張 Activity 的構成簡化圖

這裏寫圖片描述

  • 每個Activity都包含一個Window對象,Window由它的惟一的子類PhoneWindow實現ide

  • PhoneWindow:將Decoriew設置爲整個應用窗口的根View。它是Android中的最基本的窗口系 統,每一個Activity 均會建立一個PhoneWindow對象,是Activity和整個View系統交互的接口。源碼分析

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

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

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

  • C. 做爲PhoneWindow與ViewRoot之間的橋樑,ViewRoot經過DecorView設置窗口屬性。能夠同 View view = getWindow().getDecorView() 獲取它;ui

  • 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的構成

若是以爲對你有幫助, 請點個贊再走吧~

相關文章
相關標籤/搜索