setContentView與Activity初始佈局
我們常常在Activity中呼叫setContentView方法來設定自己的佈局,然而其實仔細點會發現我們設定的佈局並不是Activity顯示的全部,有的地方可能設定一個空的佈局,甚至不呼叫setContentView方法,但介面上是有內容的,可能上面有個標題,而且使用工具檢視介面的佈局,也可以發現,其佈局不止呼叫setContentView設定的那部分,如
<activity
android:name=".DefaultInitActivity"
android:theme="@android:style/Theme.Light"
android:exported="false" />
public class DefaultInitActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_default_init);
}
}
上述程式碼生成的Activity介面:
image-20211213215551938
明顯的,這裡並未呼叫setContentView,但圖片中卻有個有MyDemos字樣的標題,其實還有狀態列和導航欄的顏色,也與這裡Activity初始佈局有關
再檢視下其佈局,顯然,即使沒有setContentView,其也是有個初始佈局的
image-20211214220610652
1、setContentView
setContentView是Activity中的方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
其中getWindow方法獲取的是在Activity建立時建立的一個PhoneWindow物件,這裡簡單走讀下兩行程式碼
1.1、PhoneWindow的setContentView方法
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) {
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 {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
這裡onContentChanged呼叫根據方法的註釋,其會在contentview變更時會呼叫,開發者可以在程式碼中新增監聽,如在Activity中呼叫如下程式碼:
getWindow().setCallback(callback);
即可在一些視窗相關變更時收到回撥,如contentview變更、視窗焦點變更等
這裡的關鍵邏輯在前面部分,而這裡根據hasFeature(FEATURE_CONTENT_TRANSITIONS)的值不同主要有兩個分支
hasFeature(FEATURE_CONTENT_TRANSITIONS)其實主要是一個場景切換動畫,當然這裡預設是false的,但如果在setContentView前呼叫了類似如下程式碼或者在style中有如下windowContentTransitions設定,那麼hasFeature(FEATURE_CONTENT_TRANSITIONS)則為true
requestWindowFeature(Window.FEATURE_CONTENT_TRANSITIONS);
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
<item name="windowContentTransitions">true</item>
1.1.1、installDecor()
該方法程式碼比較多,這裡只擷取與這裡相關的主要部分檢視下
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
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) {
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方法主要是new一個DecorView物件返回
這裡generateLayout方法程式碼也比較多,這裡先只關注下其返回值:
...
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
...
return contentParent;
其中ID_ANDROID_CONTENT在Window.java中有定義
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
其中layoutResource是根據feature等來選擇佈局的,而在mDecor的onResourcesLoaded方法中主要是會解析layoutResource然後新增到控制元件中(一般情況下是直接新增到DecorView下,在一些多視窗場景,會在DecorView下新增一個佈局,裡面包含一些按鈕,然後再將上面初始佈局新增到該佈局的根節點下)
顯然generateLayout最後返回的是上面初始佈局layoutResource中id為conent的view
1.1.2、hasFeature(FEATURE_CONTENT_TRANSITIONS)為false時
這裡主要有如下邏輯
mLayoutInflater.inflate(layoutResID, mContentParent);
這裡邏輯很簡單,即解析設定的佈局,然後新增到mContentParent控制元件下
1.1.3、hasFeature(FEATURE_CONTENT_TRANSITIONS)為true
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
在hasFeature(FEATURE_CONTENT_TRANSITIONS)為true時在setContentView中主要有上述程式碼,這裡第一行程式碼是根據mContentParent(即初始佈局中id為content的view),應用呼叫setContentView設定的佈局layoutResID來建立了一個對應的Scene物件,然後呼叫transitionTo方法
private void transitionTo(Scene scene) {
if (mContentScene == null) {
scene.enter();
} else {
mTransitionManager.transitionTo(scene);
}
mContentScene = scene;
}
這裡一般如果是第一次設定contentview時這裡mContentScene應該是為空,即走scene.enter()的邏輯
public void enter() {
// Apply layout change, if any
if (mLayoutId > 0 || mLayout != null) {
// empty out parent container before adding to it
getSceneRoot().removeAllViews();
if (mLayoutId > 0) {
LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
} else {
mSceneRoot.addView(mLayout);
}
}
// Notify next scene that it is entering. Subclasses may override to configure scene.
if (mEnterAction != null) {
mEnterAction.run();
}
setCurrentScene(mSceneRoot, this);
}
如果setContentView設定的佈局是有效的,這裡主要即會呼叫如下程式碼,最後設定的佈局會新增到前面mContentParent中
LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
而如果不是第一次設定contentview時在transitionTo方法中mContentScene應該不為空即會呼叫mTransitionManager.transitionTo(scene)方法,這裡會觸發一些動畫效果,最後仍會呼叫到scene.enter()的邏輯
1.1.4、小節
從上面分析可以看出,一個普通的Activity的佈局至少可以分為三層
1、DecorView:這個一般是佈局的根節點(對freeform的多視窗場景在DecorView下會新增幾個按鈕)
2、初始佈局:這個一般是根據Activity所在的一些feature和style等來選取不同的佈局,然後新增到根節點DecorView下面(對freeform的多視窗場景會新增到幾個按鈕所在的DecorCaptionView下面)
3、應用佈局:即一般應用呼叫setConentView新增到初始佈局下id為content的view下的佈局,另外還有個addContentView方法,可在id為content的view下新增布局
1.2、Activity的initWindowDecorActionBar方法
private void initWindowDecorActionBar() {
Window window = getWindow();
// Initializing the window decor can change window feature flags.
// Make sure that we have the correct set before performing the test below.
window.getDecorView();
if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
return;
}
mActionBar = new WindowDecorActionBar(this);
mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);
mWindow.setDefaultIcon(mActivityInfo.getIconResource());
mWindow.setDefaultLogo(mActivityInfo.getLogoResource());
}
這裡程式碼邏輯可以看出主要是針對ActionBar處理的,這裡會做一些ActionBar相關的設定和初始化等工作,這裡不細述
2、初始佈局
前面有看到在PhoneWindow的generateLayout方法中會根據feature等來選擇佈局,程式碼比較多,這裡就總結列舉下程式碼中相關style和feature及對應佈局的選擇和影響(這裡除了初始佈局的選擇還有一些其他設定如背景等這裡就不全部列舉了)
// Inflate the window decor.
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;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
如上程式碼所示,其判斷基本上都是根據feature來判斷,在對應activity沒有父activity時,feature一般有兩種設定方式,一是可在setContentView呼叫之前,呼叫requestFeature方法(或Activity的requestWindowFeature方法),一是可在style中activity對應樣式新增feature對應的樣式屬性
2.1、FEATURE_SWIPE_TO_DISMISS(windowSwipeToDismiss)
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
<com.android.internal.widget.SwipeDismissLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/content"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
image-20211218211545163
如上,在feature中包含FEATURE_SWIPE_TO_DISMISS或樣式中windowSwipeToDismiss為true時,其初始佈局就是一個SwipeDismissLayout(關於另外兩個的控制元件是另外新增的,這兩個控制元件主要是和沉浸式有關,後面再探討),該佈局可滑動退出當前activity
FEATURE_SWIPE_TO_DISMISS在Android11上已被棄用了,雖還有這個feature,但上述初始佈局程式碼已沒有了
2.2、FEATURE_LEFT_ICON或FEATURE_RIGHT_ICON
else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
這裡可以看到含FEATURE_LEFT_ICON或FEATURE_RIGHT_ICON的feature時,有分兩種情況,如果是floating視窗(樣式中windowIsFloating屬性為true)則獲取樣式中dialogTitleIconsDecorLayout屬性定義的佈局作為初始佈局,如果不是floating視窗,則使用R.layout.screen_title_icons作為初始佈局,如下顯示了佈局圖,當然沒有具體程式碼設定,有些控制元件預設是不可見的,同時,這裡還移除了FEATURE_ACTION_BAR的feature,說明這兩種feature是互斥的
2.3、FEATURE_PROGRESS或FEATURE_INDETERMINATE_PROGRESS
else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
這裡看到在feature含FEATURE_PROGRESS或FEATURE_INDETERMINATE_PROGRESS且不含FEATURE_ACTION_BAR時,初始佈局為R.layout.screen_progress,不過FEATURE_PROGRESS和FEATURE_INDETERMINATE_PROGRESS看原始碼裡註釋說是不再支援了
image-20211218221005303
2.4、FEATURE_CUSTOM_TITLE
else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
這裡可以看到含FEATURE_CUSTOM_TITLE的feature時,有分兩種情況,如果是floating視窗(樣式中windowIsFloating屬性為true)則獲取樣式中dialogCustomTitleDecorLayout屬性定義的佈局作為初始佈局,如果不是floating視窗,則使用R.layout.screen_custom_title作為初始佈局,如下顯示了佈局圖,當然沒有具體程式碼設定,有些控制元件預設是不可見的,同時,這裡還移除了FEATURE_ACTION_BAR的feature,說明這兩種feature是互斥的
image-20211218221943709
2.5、FEATURE_NO_TITLE(windowNoTitle)
else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
// System.out.println("Title!");
在feature不含FEATURE_NO_TITLE的時候這裡有幾個分支,
如果是floating視窗,則獲取樣式中dialogTitleDecorLayout屬性對應的佈局,
如果feature含FEATURE_ACTION_BAR,則優先獲取樣式中windowActionBarFullscreenDecorLayout屬性對應的佈局,如果未設定該屬性,則使用R.layout.screen_action_bar作為初始佈局
image-20211221001815343
如果不是floating視窗,feature也不含FEATURE_ACTION_BAR,則使用R.layout.screen_title作為初始佈局
image-20211221002032807
2.6、FEATURE_ACTION_MODE_OVERLAY(windowActionModeOverlay)
else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
在feature含FEATURE_ACTION_MODE_OVERLAY或樣式中windowActionModeOverlay屬性為true時使用R.layout.screen_simple_overlay_action_mode作為初始佈局
image-20211221002321188
2.7、其它
layoutResource = R.layout.screen_simple;
在非前面情況時,則使用R.layout.screen_simple作為初始佈局
image-20211221002500711
2.8、小節
上面列舉了一般的各種情況下的初始佈局,當然除了初始佈局外正常的佈局中可能還有一些其他的控制元件,比如上面的statusBarBackground、navigationBarBackground等,
另外上面的列舉圖片中是使用android:theme=“@android:style/Theme.Light”樣式的Activity,在樣式中有些引數,比如顏色等,不同樣式可能顯示並不一致,
還有,上面列舉的7中主要是從generateLayout方法中layoutResource賦值的if分支中分別擷取的,所以正常判斷的話是從上往下,比如第一個滿足則使用第一個,否則判斷下一個,這裡一般都是判斷包含某feature的條件,特殊的是FEATURE_NO_TITLE的if條件判斷中是不包含該feature,所以後面的if會在包含FEATURE_NO_TITLE時走到
3、其他(PhoneWindow中一些樣式屬性定義)
雖然之前大致的佈局已經瞭解了,但其實很多樣式屬性都會影響最終的顯示,如下(對應的樣式屬性很多,這裡不一一列舉)
R.styleable.Window_windowIsFloating:其有呼叫程式碼setLayout(WRAP_CONTENT, WRAP_CONTENT);,新增該屬性為true後其顯示:(工具中有一些橫線,實際手機顯示沒有橫線,是工具勾選了一些顯示項才顯示的),顯然,一個全屏的Activity變成了類似於Dialog彈框的顯示
image-20211228223511209
R.styleable.Window_windowNoTitle:其會新增FEATURE_NO_TITLE,表示上方沒有標題欄
R.styleable.Window_windowActionBar:其會新增FEATURE_ACTION_BAR,表示上方會顯示ActionBar(沒有新增FEATURE_NO_TITLE的feature或且沒有宣告windowNoTitle樣式屬性為true)