首先,setContentView 的主要目的有 2 个:
创建 DecorView: installDecor()
获取到 Content : mLayoutInflater.inflate(xx, xx)
继承 Activity 的情况
如果继承 Activity ,setContentView 的时候源码点进去发现是 getWindow.setContentView() ,我们知道这个getWindow 就是 PhoneWindow 。在 ActivityThread.performActivity 启动 Activity 过程中,会调用 activity.attach ,在 attach 中 会创建 PhoneWindow 对象。之后才会有Activity 的 onCreate 回调。
在 PhoneWindow 的 installDecor 中会调用 generateDecor,其实最终就是直接 new 一个 DecorView:
1 | protected DecorView generateDecor(int featureId) { |
DecorView 实际上就是一个 FrameLayout 。
在 PhoneWindow 的 installDecor 还会生成 contentParent:
1 | mContentParent = generateLayout(mDecor); |
在里面会根据各种标记位 Flag 来选择不同的 layout xml布局文件作为 contentParent 的布局文件
面试题: 我在 setContentView 之后,再去设置 requestWindowFeature () 方法,为什么会报错?
直接原因:在 setContentView 方法执行完成之后,会将一个标志位置为 true, 等 requestWindowFeature方法调用的时候会判断这个标志位为true则报错。间接原因:为什么这么设计呢?这是因为在 generateLayout 的代码中, requestWindowFeature 设置的值也会作为 Flag 来选择 不同的layoutResource ,比如是否有 title (Window.FEATURE_NO_TITLE)等,所以需要这样 。
状态栏不在 setContentView 中,它是属于系统的。
继承 AppcompatActivity 情况
继承 Activity 的时候,可以使用 requestWindowFeature 来更改一些 Window 属性;但是如果继承 AppcompatActivity ,直接使用 requestWindowFeature 是没作用的,需要使用 supportRequestWindowFeature 方法。
将原始的 windowContentView 的 id 置为 NO_ID ,将自己的 subDecorView 的id 设置成原来的 windowContentView 的 id ,实现了个替换。后来就通过 PhoneWindow 的 setContentView 将 subDecorView 设置进去了。
所以,这个步骤相当于将 DecorView 中的内容完全替换了。
View 是如何根据xml创建的
上述会执行到:
1 | LayoutInflater.from(mContext).inflate(resId, contentParent) |
那么如何根据 resId 布局文件创建出布局中的哪些 View 呢?
再 createView 中,通过 clazz = Class.forName() 获得这些 View 的 class 对象,之后,通过 newInstance 的形式将 View 创建出来。
需要注意一点,如果 LayoutInflater.from(mContext).inflate 的时候,layout 是一个 merge 的,一定要 有parent 并且 attach 为 true:
1 |
ViewStub 继承的是 View ,首先它是 Gone 的,在显示的时候将自己的 id 设置为 NO_ID
## 面试题
- 为什么requestWindowFeature()要在setContentView()之前调用
requestWindowFeature 实际调用的是 PhoneWindow.requestFeature,
在这个方法里面会判断如果变量 mContentParentExplicitlySet 为true则报错,
而这个变量会在PhoneWindow.setContentView
调用的时候设置为true。- 为什么这么设计呢?
DecorView的xml布局是通过设置的窗口特征进行选择的。 - 为什么 requestWindowFeature(Window.FEATURE_NO_TITLE);设置无效?
需要用 supportRequestWindowFeature(Window.FEATURE_NO_TITLE);,因为继承的是AppCompatActivity,这个类里面会覆盖设置。
- 为什么这么设计呢?
2.LayoutInflate几个参数的作用?
LayoutInflater inflater = LayoutInflater.from(this);
// 方式一:布局添加成功,里面执行了 ll.addView(view)View view = inflater.inflate(R.layout.inflate_layout, ll, true);
// 方式二:报错,一个View只能有一个父亲(The specified child already has a parent.)
View view = inflater.inflate(R.layout.inflate_layout, ll, true);
ll.addView(view);
// 方式三:布局成功,第三个参数为false
// 目的:想要 inflate_layout 的根节点的属性(宽高)有效,又不想让其处于某一个容器中
View view = inflater.inflate(R.layout.inflate_layout, ll, false);
ll.addView(view);
// 方式四:root = null,这个时候不管第三个参数是什么,显示效果一样
// inflate_layout 的根节点的属性(宽高)设置无效,只是包裹子View,
// 但是子View(Button)有效,因为Button是处于容器下的
View view = inflater.inflate(R.layout.inflate_layout, null, false);
ll.addView(view);
3.描述下merge、include、ViewStub标签的特点
include:
- 不能作为根元素,需要放在 ViewGroup中
- findViewById查找不到目标控件,这个问题出现的前提是在使用include时设置了id,而在findViewById时却用了被include进来的布局的根元素id。
- 为什么会报空指针呢?
如果使用include标签时设置了id,这个id就会覆盖 layout根view中设置的id,从而找不到这个id
代码:LayoutInflate.parseInclude
–》final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
–》if (id != View.NO_ID) {
view.setId(id);
}
merge:
- merge标签必须使用在根布局
- 因为merge标签并不是View,所以在通过LayoutInflate.inflate()方法渲染的时候,第二个参数必须指定一个父容器,
且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点. - 由于merge不是View所以对merge标签设置的所有属性都是无效的.
ViewStub:就是一个宽高都为0的一个View,它默认是不可见的
- 类似include,但是一个不可见的View类,用于在运行时按需懒加载资源,只有在代码中调用了viewStub.inflate()
或者viewStub.setVisible(View.visible)方法时才内容才变得可见。 - 这里需要注意的一点是,当ViewStub被inflate到parent时,ViewStub就被remove掉了,即当前view hierarchy中不再存在ViewStub,
而是使用对应的layout视图代替。