在开发 Android UI 界面时,一般都会在 layout 目录下新建一个XML文件,用于编写布局文件。下面是一个简单的布局文件:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
LinearLayout>
从上面的文件的布局文件可以看到,控件由各个属性组成,如id、layout_width、layout_height等,这些属性最终都会经系统解析,从而在 View 在measure、layout 及 draw的过程中使用。
那么问题来了
layout目录下布局文件各View的属性是如何解析的?
View的属性很多,下面以 layout_width 属性的解析过程做简单的分析。
主要分两个步骤:
1. 将布局文件的属性解析到 AttributeSet 中
2. 将 AttributeSet 中 layout_width属性解析到 LayoutParams 的 width域中。
简单说下AttributeSet,它的作用从名字可以看出了,其实就是属性的集合,它包含了 View 设置的所有属性。详细见 官网 说明。
我们知道,在 Activity onCreate方法 中,将布局文件资源id作为参数传入 setContentView(int layoutResID) 方法中,从而展示布局。也有通过 adapter 的 getView() 方法中通过 LayoutInflater 将布局resID显式 inflate 进去。其实,setContentView最终也是通过 LayoutInflater 将布局文件信息填充到对应的 View 中。源码如下:
源码路径: frameworks/base/core/java/android/app/Activity.java
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
activity setContentView方法 通过 getWindow() 获得 Window 实例,window的实现类是 PhoneWindow, 来看下其对 setContentView*方法的实现:
@Override
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);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
从上面代码可知,当hasFeature(FEATURE_CONTENT_TRANSITIONS)
返回为false时,会调用 LayoutInflater 实例的inflate方法,其中参数 layoutResID 即是我们传入的布局资源id。
接下来,我们就看下 LayoutInflater 类的 inflate 方法是如何解析布局资源的。
public View inflate(int resource, ViewGroup root) {
return inflate(resource, root, root != null);
}
inflate 方法调用了同名方法,跟进:
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
首先,通过 getResources 方法获取 Resource 实例,然后将 resID 传入该实例的 getLayout 方法,从而返回一个 XmlResourceParser 实例,它是对已经编译过的xml文件的封装,接下来将该 parser 作为参数传入到 inflate,看下 这个inflate 方法实现逻辑。
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
final AttributeSet attrs = Xml.asAttributeSet(parser);
...
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, attrs, false);
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
return result;
}
}
这里的 inflate 方法实现逻辑较长,我们截取了部分代码。重点关注 下final AttributeSet attrs = Xml.asAttributeSet(parser);
到这里,,AttributeSet 出现了!
Xml将已经封装的 XmlResourceParser 实例转化为我们想要的 AttributeSet 对象。
接下来父View通过 AttributeSet 解析其子View的layout_width属性,从而将 生成该父View 的LayoutParams width的值。
root.generateLayoutParams(attrs)
其中 attrs 参数是由 Xml 的 asAttributeSet 方法将之前已经封装的 XmlResourceParser 实例解析而来。root是该我们先前在activity传入的资源布局id所对应view的父容器。由其负责解析其子view的 LayoutParams。代码如下:
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
继续跟进 LayoutParams 类的构造函数实现
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
首先看构造函数体里面的第一行代码
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
obtainStyledAttributes 方法的目的是为了获取属性,关于该方法有许多重载方法,如下:
public final TypedArray obtainStyledAttributes(int[] attrs)
用于从系统主题中获取 attrs 中的属性
public final TypedArray obtainStyledAttributes(int resid, int[] attrs)
用于从资源文件定义的 style 中读取属性
public final TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs)
从 layout 设置的属性集中获取 attrs 中的属性
这里用到的是第三个方法,它有两个参数。第一个参数为 AttributeSet 引用,它是数据源,表示属性从哪里来的,第二个参数 attrs 表示需要获取哪些属性。第二个参数传入的是 R.styleable.ViewGroup_Layout
,它定义在
frameworks/base/core/res/res/values/attrs.xml 文件中,如下:
"ViewGroup_Layout">
"layout_width" format="dimension">
<enum name="fill_parent" value="-1" />
<enum name="match_parent" value="-1" />
<enum name="wrap_content" value="-2" />
"layout_height" format="dimension">
<enum name="fill_parent" value="-1" />
<enum name="match_parent" value="-1" />
<enum name="wrap_content" value="-2" />
在这里说明下,通过在 attr 文件中定义styleable,编译后在 R 文件中自动生成一个int[],数组里面的值就是定义在 styleable 里面的 attr 的id。下面分析
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
的解析过程:
首先 attrs(属性源) 参数是对我们一开始就传入到 Activity setContentView 方法里面的 layout 文件id进行解析得到的属性集合,由 LayoutInflater 加载得到的,R.styleable.ViewGroup_Layout(要获取的属性) 就是一个int[],里面包含了声明的 attr id。我们知道了属性源,也知道了需要获取的属性集合,通过 obtainStyledAttributes 方法最终得到一个 TypedArray。
TypedArray是什么鬼?
TypedArray的存在简化了我们解析属性的步骤。比如解析 android:text="@string/my_label
,text的属性值是引用类型。如果直接使用 AttributeSet 解析该属性,需要两步:1. 获取text属性引用值的id;2. 根据该id去获取text对应的String值。而有了TypedArray,这些工作交给它就行了。
好了,我们继续跟着刚才 LayoutParams 构造函数里的实现,我们进入
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
到这里,layout_width 属性值最终赋给了width。其中widthAttr 的参数值是我们刚刚传入的R.styleable.ViewGroup_Layout_layout_width。TypedArray根据名称索引到对应的值。
属性解析的过程说的有些泛,只是了解了解析的流程,有些内容还需要后面继续深入了解。View属性的解析设计到两个类:AttributeSet 和 TypedArray,前者将View设置的所有属性做了汇集和封装,后者提供了解析属性值的方法。