基于 SDK-24 的 Android Background-Drawable 解析
文章目录
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 的加载流程:
- 通过
View
的构造函数,获取android:background
属性的值 Resourses
加载background
中定义的 XML 文件- 以
XmlResourceParser
解析 XML 文件,针对特定的 tag 生成指定的Drawable
对象
但是仅仅是inflateFromTag(name)
还是只生成了指定的Drawable,那这些 Drawable 自己的属性是怎么生成的呢?我会在下一篇来解析
文章作者 Dio.Ye
上次更新 2016-06-12