View 的 background 属性相信每一个 Android 开发都是非常了解的,但是好像对于 View 如何加载 background 资源的解析还是比较少的。

另外,最近项目里面加了很多圆角按钮,写 xml 写的实在不愉快,就打算以代码的形式来实现一个自定义的圆角按钮,这其中也涉及到不少 background 的知识点。正好在这里做一个梳理。

解析 XML 文件中的 background 属性

给 View 设置 background 通常有两种方式,一个是在 xml 中设置android:background,另一种是直接通过代码调用setBackground()。这里我们先了解 xml 这种方式。

看过之前的从 XML 到 view的应该都知道,在系统通过 xml 文件来实例化出 View 对象的时候,需要调用到带有两个参数的构造函数,起最终调用的是带有四个参数的构造函数。

// 删除不关心的代码,仅保留 Background 相关
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    this(context);

    final TypedArray a = context.obtainStyledAttributes(
            attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);

    Drawable background = null;

    // ...

    final int N = a.getIndexCount();
    for (int i = 0; i < N; i++) {
        int attr = a.getIndex(i);
        switch (attr) {
            case com.android.internal.R.styleable.View_background:
                background = a.getDrawable(attr);
                break;
            // ...
        }
    }

    // ...

    if (background != null) {
        setBackground(background);
    }

    // ...
}

Background 实际上是一个 Drawable 对象,所以只需要关注 Drawable 相关的代码就可以了。构造函数通过 TypedArray 获取到写在 xml 文件里头的android:background字段,然后通过a.getDrawable(attr)这个方法返回了 Drawable 对象。

分析 TypedArray

跳入到 TypydArray 的源码中:

public Drawable getDrawable(@StyleableRes int index) {
    if (mRecycled) {
        throw new RuntimeException("Cannot make calls to a recycled instance!");
    }

    final TypedValue value = mValue;
    if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
        if (value.type == TypedValue.TYPE_ATTRIBUTE) {
            throw new UnsupportedOperationException(
                    "Failed to resolve attribute at index " + index + ": " + value);
        }
        return mResources.loadDrawable(value, value.resourceId, mTheme);
    }
    return null;
}

调用的是mResources.loadDrawable(value, value.resourceId, mTheme)。此处的mResources 是一个Resources对象。通过查看 Resources 的源码得知,实际调用的是 ResourcesImpl 这个类的loadDrawable()方法:

Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
            boolean useCache) throws NotFoundException {
    try {
        // ...

        final boolean isColorDrawable;
        final DrawableCache caches;
        final long key;
        if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
            isColorDrawable = true;
            caches = mColorDrawableCache;
            key = value.data;
        } else {
            isColorDrawable = false;
            caches = mDrawableCache;
            key = (((long) value.assetCookie) << 32) | value.data;
        }

        // ...

        Drawable dr;
        if (cs != null) {
            dr = cs.newDrawable(wrapper);
        } else if (isColorDrawable) {
            dr = new ColorDrawable(value.data);
        } else {
            dr = loadDrawableForCookie(wrapper, value, id, null);
        }

        // ...

        return dr;
    } catch (Exception e) {
        // ...
    }
}

这里涉及到不少缓存 Drawable 的代码,系统会优先加载缓存中的 Drawable 对象,有兴趣的可以自行查看。这里我只保留了一些关键的代码。

当 XML 文件中直接设置为十六进制的颜色信息(比如#ff0000)时,会创建一个ColorDrawable对象。否则则是调用loadDrawableForCookie(wrapper, value, id, null)方法:

private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id,
            Resources.Theme theme) {

    // ...

    final Drawable dr;

    // ...
    try {
        if (file.endsWith(".xml")) {
            final XmlResourceParser rp = loadXmlResourceParser(
                    file, id, value.assetCookie, "drawable");
            dr = Drawable.createFromXml(wrapper, rp, theme);
            rp.close();
        } else {
            final InputStream is = mAssets.openNonAsset(
                    value.assetCookie, file, AssetManager.ACCESS_STREAMING);
            dr = Drawable.createFromResourceStream(wrapper, value, is, file, null);
            is.close();
        }
    } catch (Exception e) {
        // ...
    }
    // ...

    return dr;
}

OK,与加载 XML 生成 View 对象类似,依然需要XmlResourceParser来分析 XML 文件。

XML 解析

此处的代码在Drawable里:

public static Drawable createFromXml(Resources r, XmlPullParser parser, Theme theme)
            throws XmlPullParserException, IOException {
    AttributeSet attrs = Xml.asAttributeSet(parser);

    int type;
    //noinspection StatementWithEmptyBody
    while ((type=parser.next()) != XmlPullParser.START_TAG
            && type != XmlPullParser.END_DOCUMENT) {
        // Empty loop.
    }

    // ...

    Drawable drawable = createFromXmlInner(r, parser, attrs, theme);

    // ...

    return drawable;
}

调用了createFromXmlInner(r, parser, attrs, theme)方法:

public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs,
            Theme theme) throws XmlPullParserException, IOException {
    return r.getDrawableInflater().inflateFromXml(parser.getName(), parser, attrs, theme);
}

回到Resources查看一下getDrawableInflater()方法,返回的是DrawableInflater对象。这是一个被标识为_@hide_的类,进入到这个类中:

public Drawable inflateFromXml(@NonNull String name, @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs, @Nullable Theme theme)
            throws XmlPullParserException, IOException {
    // Inner classes must be referenced as Outer$Inner, but XML tag names
    // can't contain $, so the <drawable> tag allows developers to specify
    // the class in an attribute. We'll still run it through inflateFromTag
    // to stay consistent with how LayoutInflater works.
    if (name.equals("drawable")) {
        name = attrs.getAttributeValue(null, "class");
        if (name == null) {
            throw new InflateException("<drawable> tag must specify class attribute");
        }
    }

    Drawable drawable = inflateFromTag(name);
    if (drawable == null) {
        drawable = inflateFromClass(name);
    }
    drawable.inflate(mRes, parser, attrs, theme);
    return drawable;
}

重点是inflateFromTag()。此处涉及到inflateFromClass()的判断我还不是特别明白,如果有哪位 dalao 愿意的话可以指点指点。

private Drawable inflateFromTag(@NonNull String name) {
    switch (name) {
        case "selector":
            return new StateListDrawable();
        case "animated-selector":
            return new AnimatedStateListDrawable();
        case "level-list":
            return new LevelListDrawable();
        case "layer-list":
            return new LayerDrawable();
        case "transition":
            return new TransitionDrawable();
        case "ripple":
            return new RippleDrawable();
        case "color":
            return new ColorDrawable();
        case "shape":
            return new GradientDrawable();
        case "vector":
            return new VectorDrawable();
        case "animated-vector":
            return new AnimatedVectorDrawable();
        case "scale":
            return new ScaleDrawable();
        case "clip":
            return new ClipDrawable();
        case "rotate":
            return new RotateDrawable();
        case "animated-rotate":
            return new AnimatedRotateDrawable();
        case "animation-list":
            return new AnimationDrawable();
        case "inset":
            return new InsetDrawable();
        case "bitmap":
            return new BitmapDrawable();
        case "nine-patch":
            return new NinePatchDrawable();
        default:
            return null;
    }
}

inflateFromTag()方法判断标签的名字,然后生成对应的 Drawable 的子类对象。

小结

至此,我们也大致了解了 Background-Drawable 的加载流程:

  1. 通过View的构造函数,获取android:background属性的值
  2. Resourses加载background中定义的 XML 文件
  3. XmlResourceParser解析 XML 文件,针对特定的 tag 生成指定的Drawable对象

但是仅仅是inflateFromTag(name) 还是只生成了指定的Drawable,那这些 Drawable 自己的属性是怎么生成的呢?我会在下一篇来解析