一 引言

在开发 Android UI 界面时,一般都会在 layout 目录下新建一个XML文件,用于编写布局文件。下面是一个简单的布局文件:

<?xml version="1.0" encoding="utf-8"?><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域中。

二 layout/layout_name.xml -> AttributeSet

简单说下AttributeSet,它的作用从名字可以看出了,其实就是属性的集合,它包含了 View 设置的所有属性。详细见 官网 说明。
我们知道,在 Activity onCreate方法 中,将布局文件资源id作为参数传入 setContentView(int layoutResID) 方法中,从而展示布局。也有通过 adaptergetView() 方法中通过 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 对象。

三 将 AttributeSet 中 layout_width属性解析为LayoutParams 的 width

接下来父View通过 AttributeSet 解析其子View的layout_width属性,从而将 生成该父View 的LayoutParams width的值。

root.generateLayoutParams(attrs)

其中 attrs 参数是由 XmlasAttributeSet 方法将之前已经封装的 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 方法的目的是为了获取属性,关于该方法有许多重载方法,如下:

  1. public final TypedArray obtainStyledAttributes(int[] attrs)

    用于从系统主题中获取 attrs 中的属性

  2. public final TypedArray obtainStyledAttributes(int resid, int[] attrs)

    用于从资源文件定义的 style 中读取属性

  3. 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属性的解析设计到两个类:AttributeSetTypedArray,前者将View设置的所有属性做了汇集和封装,后者提供了解析属性值的方法。

更多相关文章

  1. 一款常用的 Squid 日志分析工具
  2. GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
  3. RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
  4. Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
  5. 在Android(安卓)Studio 2.3 中开发运行 Cordova 8.0.0项目
  6. apk的优化方案-1
  7. Android(安卓)- Handler 、AsyncTask(二)
  8. Android开发全程记录(十四)——Android显示gif动画的方法
  9. Android调用WebService系列之对象构建传递

随机推荐

  1. Android(安卓)SDK环境配置
  2. Android数据存储方式之:ContentProvider
  3. Day2.3--Android常用布局之RelativeLayou
  4. 自定义Logcat,完全控制打印信息
  5. JNI 引用问题梳理
  6. Android(安卓)vector 标签 pathData
  7. Android(安卓)ContentProvider的实现及简
  8. Article 2017- Q1
  9. Android(安卓)快速开发框架:推荐10个框架
  10. OpenCore代码阅读--PVPlayer的实现