从代码创建 Shape Drawable

平时一直是用 xml 来写 Shape Drawable,但由于这次的背景是会根据状态变化不同的颜色,xml 就不能满足要求了。得从代码来构造相关的 Drawable 对象。

相关 xml 代码如下:

1
2
3
4
5
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="10dp"/>
<stroke android:width="2px" android:color="#ff000000"/>
</shape>

查看有个相关类叫 ShapeDrawable,根据里面的 Api 实现了类似的效果。

1
2
3
4
5
6
7
8
float radius = DensityUtil.dip2px(this, 10);
float[] outerR = new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
RoundRectShape rr = new RoundRectShape(outerR, null, null);
ShapeDrawable drawable = new ShapeDrawable(rr);
drawable.getPaint().setColor(0xff333333);
drawable.getPaint().setStyle(Paint.Style.STROKE);
drawable.getPaint().setStrokeWidth(2);
code.setBackgroundDrawable(drawable);

除了代码上相对繁琐之外,还有一点,ShapeDrawable 对 StrokeWidth 的处理不够完善,在边缘处的 Stroke 只显示了一半。

对比

源码

我们来研究下源码,看下对 xml 的解析到底是个怎样的流程。
从 view.setBackgroundResource 开始,发现是通过 Resources.getDrawable 来获取到 Drawable。重点是其中的 loadDrawable 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Nullable
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException {
TypedValue value;
synchronized (mAccessLock) {
value = mTmpValue;
if (value == null) {
value = new TypedValue();
} else {
mTmpValue = null;
}
getValue(id, value, true);
}
final Drawable res = loadDrawable(value, id, theme);
synchronized (mAccessLock) {
if (mTmpValue == null) {
mTmpValue = value;
}
}
return res;
}

loadDrawable 方法挺长的,抽取中间最重要的一部分进行说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Nullable
Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
// ...
// 上面都是对 Drawable 缓存的检查和处理
// 下文的 cs 为缓存的 Drawable 具体属性
Drawable dr;
if (cs != null) {
dr = cs.newDrawable(this);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(value, id, null);
}

// ...
}

可以分析出,当第一次加载 xml 时,最终会调用到 loadDrawableForCookie 来获取到 Drawable。
我们再来看下 loadDrawableForCookie 的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Loads a drawable from XML or resources stream.
*/
private Drawable loadDrawableForCookie(TypedValue value, int id, Theme theme) {
// ...

final Drawable dr;

Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
try {
if (file.endsWith(".xml")) {
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXml(this, rp, theme);
rp.close();
} else {
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
dr = Drawable.createFromResourceStream(this, value, is, file, null);
is.close();
}
} catch (Exception e) {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
final NotFoundException rnf = new NotFoundException(
"File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);

return dr;
}

若是 xml 文件,则会调用 Drawable.createFromXml,而 Drawable.createFromXml 会调用 Drawable.createFromInner 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs,
Theme theme) throws XmlPullParserException, IOException {
final Drawable drawable;

final String name = parser.getName();
switch (name) {
// ...
case "shape":
drawable = new GradientDrawable();
break;
// ...
default:
throw new XmlPullParserException(parser.getPositionDescription() +
": invalid drawable tag " + name);

}
drawable.inflate(r, parser, attrs, theme);
return drawable;

原来,shape 时创建的是 GradientDrawable 而不是 ShapeDrawable,被名字无情地欺骗了,还是源码最靠谱。

解决方案

将 ShapeDrawable 替换为 GradientDrawable 就可以完美解决问题了,代码也简洁了许多。

1
2
3
4
5
float radius = DensityUtil.dip2px(this, 10);
GradientDrawable drawable = new GradientDrawable();
drawable.setCornerRadius(radius);
drawable.setStroke(2, 0xff333333);
code.setBackgroundDrawable(drawable);