Android檢視相關之setContentView()原始碼分析
日常使用Activity時都會用到setContentView(int layoutId)的方法,今天挖一下相關的原始碼,方便日後裝逼
(學會這個好像最直接的作用就是可以裝逼)
1、setContentView(int layoutId)
(基於Android8.1)
@Override public void setContentView(@LayoutRes int layoutResID) { getDelegate().setContentView(layoutResID); } --> @NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; } --> /** *AppCompatDelegateImplV9是下面幾個AppCompatDelegateImplX 類的父類 *setContentView在AppCompatDelegateImplV9中實現 */ private static AppCompatDelegate create(Context context, Window window, AppCompatCallback callback) { if (Build.VERSION.SDK_INT >= 24) { return new AppCompatDelegateImplN(context, window, callback); } else if (Build.VERSION.SDK_INT >= 23) { return new AppCompatDelegateImplV23(context, window, callback); } else if (Build.VERSION.SDK_INT >= 14) { return new AppCompatDelegateImplV14(context, window, callback); } else if (Build.VERSION.SDK_INT >= 11) { return new AppCompatDelegateImplV11(context, window, callback); } else { return new AppCompatDelegateImplV9(context, window, callback); } }
setContentView在AppCompatDelegateImplV9中實現,其他類最終都繼承了AppCompatDelegateImplV9,呼叫AppCompatDelegateImplV9中的setContentView() 。
class AppCompatDelegateImplV9{ private ViewGroup mSubDecor; @Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent);//常見的inflate layout mOriginalWindowCallback.onContentChanged();//呼叫onContentChanged()的生命週期方法 } --> private void ensureSubDecor() { if (!mSubDecorInstalled) { mSubDecor = createSubDecor();//初始化mSubDecor mSubDecorInstalled = true;//標記mSubDecor已經初始化 ... } } }
重點方法在createSubDecor(), 會關聯到了PhoneWindow和DecorView。
2、createSubDecor()
private ViewGroup createSubDecor() { //獲取主題屬性 TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme); //根據activity設定的window屬性,配置activity風格樣式 if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) { requestWindowFeature(Window.FEATURE_NO_TITLE);//無title } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) { // Don't allow an action bar if there is no title. requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);//設定actionbar } if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) { //ActionBar的Overlay模式,讓ActionBar浮動且透明 requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY); } if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) { requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY); } mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);//懸浮 a.recycle(); // Now let's make sure that the Window has installed its decor by retrieving it mWindow.getDecorView();//給mWindow建立一個DecorView final LayoutInflater inflater = LayoutInflater.from(mContext); ViewGroup subDecor = null;//subDecor會作為結果返回出去 //根據主題屬性的標誌來決定實現哪個layout,然後賦值給subDecor if (!mWindowNoTitle) { if (mIsFloating) { // If we're floating, inflate the dialog title decor subDecor = (ViewGroup) inflater.inflate( R.layout.abc_dialog_title_material, null); } else if (mHasActionBar) { // Now inflate the view using the themed context and set it as the content view subDecor = (ViewGroup) LayoutInflater.from(themedContext) .inflate(R.layout.abc_screen_toolbar, null); ... } } else { if (mOverlayActionMode) { subDecor = (ViewGroup) inflater.inflate( R.layout.abc_screen_simple_overlay_action_mode, null); } else { subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null); } } ... //獲取內容contentView,上面的四個layout中都有id為action_bar_activity_content的子view final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById( R.id.action_bar_activity_content); // 獲取PhoneWindow中的content佈局物件,這裡其實就是mWindow的mContentParent final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content); if (windowContentView != null) { // There might be Views already added to the Window's content view so we need to // migrate them to our content view //如果mWindow的contentview已經新增過一些view,把第一個子view放到我們contentView裡面 while (windowContentView.getChildCount() > 0) { final View child = windowContentView.getChildAt(0); windowContentView.removeViewAt(0); contentView.addView(child); } // Change our content FrameLayout to use the android.R.id.content id. // Useful for fragments. //把mWindow中的mContentParent的id修改成View.NO_ID,原來的id是android:id/content windowContentView.setId(View.NO_ID); contentView.setId(android.R.id.content); } // Now set the Window's content view with the decor mWindow.setContentView(subDecor);//把subDecor設定成mWindow的內容畫面 ... return subDecor; }
解析:
1、首先配置window的屬性樣式(看3.1的程式碼片段);
2、然後呼叫了 mWindow.getDecorView() ,即在window類中新建一個DecorView(FrameLayout),根據設定好的window屬性樣式inflate對應的layout佈局生成一個View,新增成為DecorView的子View。
3、根據屬性樣式的標誌載入對應的佈局,設定為subDecor(ViewGroup)的佈局;
4、執行mWindow.setContentView(subDecor)。
(2.1) requestWindowFeature(int featureId)
@Override public boolean requestWindowFeature(int featureId) { ... switch (featureId) { case FEATURE_SUPPORT_ACTION_BAR: throwFeatureRequestIfSubDecorInstalled(); mHasActionBar = true;// 改變屬性的標記 return true; ... } return mWindow.requestFeature(featureId);//設定window的屬性 } // 如果在setContentView之後再去設定requestWindowFeature,會丟擲Exception。 private void throwFeatureRequestIfSubDecorInstalled() { if (mSubDecorInstalled) { throw new AndroidRuntimeException( "Window feature must be requested before adding content"); } }
(2.2) 看看subDecor設定的layout佈局
R.layout.abc_dialog_title_material(在include佈局中包含了id為action_bar_activity_content的ContentFrameLayout)
<android.support.v7.widget.FitWindowsLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical" android:fitsSystemWindows="true"> <TextView android:id="@+id/title" style="?android:attr/windowTitleStyle" android:singleLine="true" android:ellipsize="end" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAlignment="viewStart" android:paddingLeft="?attr/dialogPreferredPadding" android:paddingRight="?attr/dialogPreferredPadding" android:paddingTop="@dimen/abc_dialog_padding_top_material"/> <include layout="@layout/abc_screen_content_include" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1"/> </android.support.v7.widget.FitWindowsLinearLayout>
R.layout.abc_screen_simple(在include佈局中包含了id為action_bar_activity_content的ContentFrameLayout)
<android.support.v7.widget.FitWindowsLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/action_bar_root" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:fitsSystemWindows="true"> <android.support.v7.widget.ViewStubCompat android:id="@+id/action_mode_bar_stub" android:inflatedId="@+id/action_mode_bar" android:layout="@layout/abc_action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" /> <include layout="@layout/abc_screen_content_include" /> </android.support.v7.widget.FitWindowsLinearLayout>
3、mWindow.setContentView(View view)
@Override public void setContentView(View view) { setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } @Override public void setContentView(View view, ViewGroup.LayoutParams params) { // 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();//如果mWindow中的mContentParent 為null,執行installDecor()會初始化mContentParent } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { view.setLayoutParams(params); final Scene newScene = new Scene(mContentParent, view); transitionTo(newScene); } else { //把subDecor新增到了window中 mContentParent.addView(view, params); } ... }
解析:mWindow.setContentView(subDecor)就是把mSubDecor新增到mWindow的mContentParent佈局中。
4、再回到setContentView
public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews();//清空contentParent佈局中的子控制元件 LayoutInflater.from(mContext).inflate(resId, contentParent);//把我們設定的layout新增到contentParent中 mOriginalWindowCallback.onContentChanged();//呼叫onContentChanged()的生命週期方法 }
從mSubDecor中獲取contentParent的佈局,把我們設定的layout新增到contentParent中。
總結:
1、Activity中有一個window(PhoneWindow), 配置主題屬性就是經過這個window;
2、Activity檢視層級中的根佈局是DecorView,這是個FrameLayout。
3、我們設定的佈局到DecorView之間還存在三個或者四個佈局。

Activity檢視層級.png
結論驗證
寫個demo,佈局頁面如下
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout>

preview.png
在來看一下檢視層級

檢視層級分析.JPG