01 - [開胃菜] - Activity.setContentView 涉及到的類及相關概念
02 - [正菜] - Activity.setContentView流程
03 - [甜湯] - AppCompatActivity.setContentView 流程
04 - [完結(jié)] - setContentView 流程總結(jié)
前面兩個章節(jié)學習了 Activity 的 setContentView 流程, 復習一下大概流程.
-
Activity
調(diào)用setContentView .
- 執(zhí)行
PhonwWindow.setContentView
-
PhonwWindow.setContentView
中調(diào)用installDecor
初始化DecorView
(installDecor
中調(diào)用generateDecor()
) - 初始化
mContentParent
(installDecor
調(diào)用generateLayout()
)- 在初始化
mContentParent
的同時, 又根據(jù)Window
不同屬性加載不同的布局, 然后添加到DecorView
中. - 最后獲取
DecorView
中的一個ViewGroup
作為mContentParent
并返回.
- 在初始化
- 在
PhoneWindow.setContentView
中, 調(diào)用mLayoutInflater.inflate(layoutResID, mContentParent);
把我們要設置的布局文件ID, 加載到mContentParent
中. - 流程結(jié)束
可是目前我們所使用的 Activity , 都是直接繼承自 AppCompatActivity, 而 AppCompatActivity 最終也是繼承自 Activity, 那么今天來看一下 AppCompatActivity.setContentView 的流程
( 以下分析基于 Android 10.0 )
首先在 MainActivity 中點擊 setContentView 進入內(nèi)部
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
跳轉(zhuǎn)到了 AppCompatActivity 的 setContentView
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
看到這里獲取了一個代理, 然后調(diào)用代理的 setContentView 方法, 繼續(xù)跟進, 看一下獲取的是什么代理, 點擊 getDelegate 進入.
private AppCompatDelegate mDelegate;
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
看看究竟 create 創(chuàng)建了什么.
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}
進入 create內(nèi). 發(fā)現(xiàn)創(chuàng)建的是 AppCompatDelegateImpl ,
原來 AppCompatDelegate 是一個抽象類, 而 AppCompatDelegateImpl 繼承自 AppCompatDelegate .
( 注: AppCompatDelegate 是一個可以放在任意Activity中,并且回調(diào)相應生命周期的類淤袜,在不使用 AppCompatActivity 的情況下, 也可以得到一致的主題與顏色 )
那么現(xiàn)在就清楚了. AppCompatActivity.setContentView 真正調(diào)用了 AppCompatDelegateImpl 中的 setContentView 方法.
進入 AppCompatDelegateImpl.setContentView
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
先來看第一個方法 ensureSubDecor
, 進入
// true if we have installed a window sub-decor layout.
private boolean mSubDecorInstalled;
private ViewGroup mSubDecor;
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
...
}
}
上面省去了一些暫時不需要關注的代碼, 重點就是這個 mSubDecor 的初始化.
DecorView 知道, 因為上一章說到了, 可是這個 mSubDecor 是什么鬼. 就是一個簡單的 ViewGroup ?
進入 AppCompatDelegateImpl.createSubDecor()
private ViewGroup createSubDecor() {
//---------------------------------第一部分----------------------------------
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
...
...
a.recycle();
//---------------------------------第二部分----------------------------------
// Now let's make sure that the Window has installed its decor by retrieving it
mWindow.getDecorView();
//---------------------------------第三部分----------------------------------
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
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);
}
if (Build.VERSION.SDK_INT >= 21) {
...
} else {
...
}
}
if (subDecor == null) {
throw new IllegalArgumentException(
"AppCompat does not support the current theme features: { "
+ "windowActionBar: " + mHasActionBar
+ ", windowActionBarOverlay: "+ mOverlayActionBar
+ ", android:windowIsFloating: " + mIsFloating
+ ", windowActionModeOverlay: " + mOverlayActionMode
+ ", windowNoTitle: " + mWindowNoTitle
+ " }");
}
...
...
...
//---------------------------------第四部分----------------------------------
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
...
...
}
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor);
...
...
return subDecor;
}
代碼量有點多, 我已省去了, 目前我們不需要關注的代碼. 只留下和流程相關的關鍵性代碼.
如上代碼所示, 我將 createSubDecor 方法整體分為了 4個部分.
?
?
createSubDecor() 第一部分
又見到了 TypedArray, 上一章也有這個, 還記得嗎? 不記得的回去翻一下加深下印象.
上章的 TypedArray 是取的是 Window的, 然后賦值給 PhoneWindow 的.
這里的 TypedArray 取的是 AppCompatTheme 的, 然后賦值給 AppCompatDelegateImpl 的.
兩者之間不要弄混淆了.
所以說, 第一部分的主要是從 AppCompatTheme 獲取 style, 然后逐個進行判斷, 然后賦值給 AppCompatDelegateImpl .
?
?
createSubDecor() 第二部分
mWindow.getDecorView();
看到這句, 熟悉嗎? 我猜測這里是初始化了DecorView 和 mContentParnt,
為了驗證. 我們直接進入 PhoneWindow 的 getDecorView() 方法.
PhoneWindow.getDecorView()
@Override
public final @NonNull View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
果不其然, 確實是去初始化了 DecorView 和 mContentParent. ( installDecor() 調(diào)用. installDecor方法具體流程, 這里不再說明, 上一章 [02-正菜] 中已經(jīng)詳細解釋 )
第二部分就是初始化 PhoneWindow 中的 DecorView 和 mContentParent
?
?
createSubDecor() 第三部分
看到第三部分是不是還是有點熟悉?
是不是和在 PhoneWindow 初始化 mContentParent 并且加載一個 LinearLayout 的邏輯一樣? 只是這里是給 subDecor 賦值.
那既然這樣, 我們還是先找到一個最簡單的布局文件看一下.
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
在我電腦上的路徑如下:
D:\SDK\extras\android\support\v7\appcompat\res\layout\R.layout.abc_screen_simple (家里的是 windows 系統(tǒng))
<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>
繼續(xù)尋找 include 的, abc_screen_content_include, 還是在上面的那個路徑下.
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<android.support.v7.widget.ContentFrameLayout
android:id="@id/action_bar_activity_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</merge>
思考一下, PhoneWindow 中給 DecorView 加載了一個 LinearLayout, 里面有兩部分, 其中一部分是一個 FrameLayout.
而這里呢, 給 subDecor 賦值了一個 FitWindowsLinearLayout 布局 , 里面也有兩部分, 其中一部分是一個 ContentFrameLayout .
這兩者看起來是不是很相似? 有沒有聯(lián)想到什么? 別著急繼續(xù)往下看.
第三部分根據(jù)條件給 subDecor 賦值. (加載不同的布局文件)
?
?
createSubDecor() 第四部分. 重點
為了方便, 我把第四部分的代碼拿下來放到這里.
//---------------------------------第四部分----------------------------------
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
...
...
}
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor);
...
...
return subDecor;
先是從 subDecor 中取出一個 ID 為 action_bar_activity_content 的控件賦值給 contentView 變量.
這個 ID 就是上面布局文件中的 ContentFrameLayout接著從 PhoneWindow 中的 DecorView 中獲取一個 ID 為 content 的 ViewGroup. 賦值給 windowContentView .
回想一下, 這個ID 不就是 DecorView 中 LinearLayout 里面的 FrameLayout 嘛.循環(huán)遍歷 windowContentView, 取出 windowContentView 中每一個子 View, 然后 windowContentView 先刪除這個View, 再添加到 ContentFrameLayout 中. 直至windowContentView 為空.
把 DecorView 中 LinearLayout 里面的 FrameLayout ID 改為 NO_ID.
把 subDecor 中 ContentFrameLayout ID 改為 R.id.content.
調(diào)用 PhoneWindow 的 setContentView 把 subDecor 加載到 PhoneWindow 中的 mContentParent 中.
返回 subDecor.
第四部分總的來說就是, 將 PhoneWindow --> DecorView 布局中 id 為 content 的 FrameLayout 所有子 View 遍歷添加到 subDecor 中 id 為 action_bar_activity_content 的 ContentFrameLayout 中去. 添加一個刪除一個. 遍歷完成后, 改變兩個 ViewGroup 的 id, 最后將 subDecor 添加到 PhoneWindow 的 mContentParent 中.
?
?
現(xiàn)在代碼繼續(xù)回到 AppCompatDelegateImpl.setContentView
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
ensureSubDecor();
已經(jīng)執(zhí)行完成, 初始化了 mSubDecor .
接著從 mSubDecor 中獲取 id 為 content 的控件, 并轉(zhuǎn)為 ViewGroup.
然后把我們傳入的資源文件 添加到 這個 ViewGroup 中去.
至此, 這整個流程分析完成.
在下一章, 會有一個總結(jié), 從網(wǎng)上找了幾張圖來總結(jié)和對比.