文章目录
- 概述
- 动态添加 View 的基本流程
- 代码模板
- 将 View 加载至内存并获得其引用
- 方式一:new 一个 View 对象
- 方式二:使用 LayoutInflater 将 XML 加载为 View 实例
- 获得 LayoutInflater 实例
- 使用 inflate 方法
- LayoutParams
- 为什么需要布局属性?
- 给 View 设置布局属性
- ViewGroup.addView
概述
本文提供了动态添加 View 的代码模板、阐述了 LayoutInflater
的获取方法、解答了获取 LayoutInflater
时传入不同类型 Context
的差别,并对 LayoutInflater.inflate
的不同重载方法进行分析,详细解释了 root
以及 attachToRoot
参数的作用,最后还讲解了什么是布局属性,以及布局属性存在的意义。
动态添加 View 的基本流程
将一个 View 添加在屏幕上,总共需要三步
- 将 View 加载到内存,并获得其引用
- 设置其布局属性、控件属性
- 调用 addView 方法
代码模板
这里先给出了动态添加 View 的场景与代码模板,具体细节后面再慢慢说,尽量知其然又知其所以然。
场景一:添加已经写好布局 XML 的 View 树到固定的父布局中
若 parent
为父布局的引用,则直接使用 View.inflate(context, resource, parent)
即可。
场景二:加载已经写好布局 XML 的 View 树到一个不定的父布局中
先使用 View.inflate(context, resource, null)
将XML加载到内存,再使用 parent.addView(view)
添加到父布局,最后通过 View.setLayoutParams
设置布局属性,或者直接使用 parent.addView(view,-1,layoutParams);
场景三:先将XML加载进内存,在某个实际再固定到特定父布局中
先使用 LayoutInflater.from(context).inflate(resource,parent,false);
将XML加载进内存,并保存其应用,到某个时刻再调用 parent.addView(view)
将其固定到布局
将 View 加载至内存并获得其引用
方式一:new 一个 View 对象
例如 在 MainActivity.java
中 new 一个 Button
对象
new Button(MainActivity.this)
通常,继承自 View 的类都有如下构造方法
// 简单创建一个View,新创建 View 会被设置上它自带的默认样式
public View(Context context) ;
// LayoutInflater 在 createView 时,通过反射调用了这个构造函数,
// 通常不需要使用这个构造方法来创建我们的 View 实例,
// 但是自定义 View 时一定要重载这个构造函数
public View(Context context, @Nullable AttributeSet attrs);
// 比上一个构造函数多了一个 defStyleAttr ,
// 在 defStyleAttr 传入一个样式 id,
// 未在 attrs 中定义的样式,将会从这里取得
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr);
// 比上一个构造函数多了一个 defStyleRes ,
// 当 defStyleAttr 传入 0 ,或者无法找到时,则会从这里尝试查找
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes);
参数解析
- context:上下文,即创建这个 View 时的环境信息,在创建 View 的时候,需要通过 context 获取当前的主题配置、资源文件等信息,阅读源码发现其专用调用
ContextWrapper
内的方法,ApplicationContext
、Activity
、Service
,都直接或者间接集成于ContextWrapper
,所以给其传入ApplicationContext
似乎没有什么问题。 - attrs : 属性集,即对应着使用 XML 文件定义 View 时,XML 标签上的那些属性
- defStyleAttr :用户在当前主题下定义的默认样式id,可以传入 0 表示不使用自定义的默认样式
- defStyleRes:用户定义的默认样式id(未必是当前主题下),可以传入 0 表示不使用自定义的默认样式
四个构造函数,主要就是关于样式的参数有差别,每个样式属性都可以在attrs、defStyleAttr 、defStyleRes 中设定,但是他们之中存在优先级:
- 若在 attrs 中设定,则优先使用 attrs 内设定的样式
- 若在 defStyleAttr 中设定,则若 attrs 内未设定,就使用 defStyleAttr 内设定的属性
- 若在 defStyleRes 中设定,则只有在 attrs 、defStyleAttr 都未涉及时,才会生效
- 若 attrs、defStyleAttr 、defStyleRes 都没涉及某个样式属性,则该属性会采用这个 View 的基础样式,比如 Button 就会使用
R.attr.buttonStyle
提供的属性
方式二:使用 LayoutInflater 将 XML 加载为 View 实例
获得 LayoutInflater 实例
在 Activity 中获得 LayoutInflater 有如下几种
LayoutInflater inflater1 = LayoutInflater.from(this);
LayoutInflater inflater2 = getLayoutInflater();
LayoutInflater inflater3 = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
其中 LayoutInflater.from(this);
的源码为:
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
Activity.getLayoutInflater()
的源码为
public LayoutInflater getLayoutInflater() {
return getWindow().getLayoutInflater();
}
getWindow()
返回的是一个 PhoneWindow
对象,在 PhoneWindow
构造时,会调用 LayoutInflater.from(context);
方法,为其组合一个与其 context 对应的 LayoutInflater
对象
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
mRenderShadowsInCompositor = Settings.Global.getInt(context.getContentResolver(),
DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR, 1) != 0;
}
所以归根到底,都是调用的
(LayoutInflater) context.getSystemService(LAYOUT_INFLATER_SERVICE);
得到的 LayoutInflater
对象getSystemService
是 Context
的方法,在 ContextThemeWrapper
中有:
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
这里注意几点问题:
-
getSystemService
是Context
的方法,ApplicationContext
、Activity
、Service
都可以使用 -
Activity
继承自ContextThemeWrapper
,所以Activity
于其他 context 略有不同 -
ApplicationContext
、Service
得到的LayoutInflater
是全局单例,不管在哪里获取,获取的都是一个对象 -
Activity
得到的是全剧单例的拷贝,即同一个Activity
得到的LayoutInflater
是同一个实例,不同的Activity
得到的LayoutInflater
是不同的实例 -
LayoutInflater
除了有inflate
方法外,还有onCreateView
这样的回调方法,所以有时候需要使用一个“特定”的实例
使用 inflate 方法
LayoutInflater.inflate
方法有多种重载,常用的是前两种重载,但是大多数实现逻辑都在最后一种重载中。
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root);
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot);
public View inflate(XmlPullParser parser, @Nullable ViewGroup root);
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
参数分析:
- resource:资源id,要加载的布局文件的id
- root :根布局,可选,
ViewGroup
类型的对象,继承ViewGroup
的对象就是那几种布局,以及部分可以包含子 View 的控件,所以称其为根布局。根布局可以为加载的 View 提供布局属性 - attachToRoot:是否把生成的 View 固定到根布局上,具体用法看下面分析。
结合源码进行分析,先看第一种重载:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
}
第一种重载的源码表明,使用该方法加载布局时:
- 若根布局存在,则将加载的 View 固定在根布局上
- 若根布局不存在,则不将加载的 View 固定在根布局上
但是什么叫 “固定在根布局上” 再需要继续看源码,第二种重载方法其实调用的是最后一种重载,所以直接看最后一种重载的源码,这里挑出部分进行分析:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
......
View result = root;
......
try {
......
if (TAG_MERGE.equals(name)) {
// 这里是如果 XML 标签是 merge 时的处理,这里略
......
} else {
......
// 如果根布局对象存在,attachToRoot为false,则只将根布局支持的布局属性抽取出来设置给temp
if (root != null) {
......
// 将XML中的根布局支持的布局属性抽取出来,布局属性就是那些以layout_*命名的属性
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 将布局属性设置给 temp
temp.setLayoutParams(params);
}
}
......
// 如果根布局存在,attachToRoot为true,则将新加载进来的View附加在根布局之下
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 如果根布局不存在,或者 attachToRoot为false,
// 该方法直接将新加载进来的View返回
// 否则就会把根布局对象返回
if (root == null || !attachToRoot) {
result = temp;
}
}
......
return result;
}
}
总结一下就是:
- root 不为 null
- attachToRoot 为 true :
- 新加载的 View 中,其根布局支持的布局属性将会生效
- 新加载的 View 将会被 add 到根布局上
- 返回的是根布局对象
- attachToRoot 为 false:
- 新加载的 View 中,其根布局支持的布局属性将会生效
- 返回是新加载的 View 对象
- root 为 null:
- 不管 attachToRoot 如何设置
- 新加载的 View 布局属性不会生效
- 返回是新加载的 View 对象
还有一种更常用的 inflate 方法,View.inflate(context,resource,root)
:
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
LayoutInflater factory = LayoutInflater.from(context);
return factory.inflate(resource, root);
}
这个静态方法就是对上述内容的封装。
LayoutParams
LayoutParams 布局属性,在 XML 中定义的控件有两种属性:
- 布局属性,指该控件在当前布局下的样式
- 自身属性,指该控件自身的特性
在 XML 文件中,布局属性是那些 layout_*
命名的属性,没有 layout
前缀的属性,就是自身属性
为什么需要布局属性?
一个控件可以放在不同布局下,但是同一控件在不同的布局下,可能需要一些“特色属性”,比如在 LinearLayout
中的 layout_weight
、在 RelativeLayout
中的 layout_toLeftOf
如果将这些属性归为控件的自身属性,那么一个在A布局的控件,很难不加改动的在B布局复用,这无疑增加了控件与布局的耦合。
又考虑到同一个布局下的所有“特色属性”都应该是一样的,所以不妨以布局为单位,对这些属性进行分类,就成了我们所谓的布局属性。
将布局属性抽取出来以后,很方便实现:
1. 父布局不变,更换控件
2.控件不变,更换父布局
由于布局属性是与控件所在父布局相关的属性,所以布局属性的类型要与父布局类型一致,例如 LinearLayout.LayoutParams
对应 LinearLayout
的布局属性,RelativeLayout.LayoutParams
对应 RelativeLayout
的布局属性。
给 View 设置布局属性
View 可以被代码动态设置布局属性,使用 View.setLayoutParams
方法
public void setLayoutParams(ViewGroup.LayoutParams params) {
if (params == null) {
throw new NullPointerException("Layout parameters cannot be null");
}
mLayoutParams = params;
resolveLayoutParams();
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).onSetLayoutParams(this, params);
}
requestLayout();
}
可以看到,当调用 setLayoutParams
方法时,会同时请求根布局与 View 的重新绘制,并执行其父布局的 onSetLayoutParams
回调函数。
ViewGroup.addView
addView
是 ViewGroup
内持有的方法,即那些可以包含子控件的布局与控件才可以执行addView
,在普通的 View 是没有的。
ViewGroup.addView
有四种重载
// 添加一个子控件,如果控件未设置布局属性,则使用其父布局的默认属性
// 该方法不应该在draw(Canvas), onDraw(Canvas), dispatchDraw(Canvas)以及任何相关方法中调用。
public void addView(View child) {
addView(child, -1);
}
// index 参数表示添加的子空间在父布局中的位置
// index传入 -1 表示添加在最后
//由源码可见,如果 child 不包含布局属性,则使用 generateDefaultLayoutParams() 获得默认布局属性
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException(
"generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
//添加一个子控件,并为子空间赋予一个布局属性
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}