Android View的layout_width属性是如何解析的

一 引言

在开发 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域中。

二 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设置的所有属性做了汇集和封装,后者提供了解析属性值的方法。

你可能感兴趣的:(Android)