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)
标签:layout,features,setContentView,布局,FEATURE,feature,layoutResource,Activity,初始 From: https://www.cnblogs.com/luoliang13/p/18227139