一、前言
在 Android 系统中,图形图像的绘制需要在画布上进行操作和处理,为了增强复用性,因此也对图形或者颜色提供了Drawable抽象。通过这个类可以减少我们的绘制工作和使用成本,同时系统也提供了众多的 Drawable 的派生类比如单色、图形、位图、裁剪、动画等等来完成一些常见的绘制需求。
Drawable 是一个抽象的可绘制类,他主要是提供了一个可绘制的区域 bound 属性以及一个 draw 成员函数,不同的派生类通过重载 draw 函数的实现而产生不同的绘制结果。
如下是 Drawable xml文件加载的末端代码。
java
if (file.endsWith(".xml")) {
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
rp.close();
} else {
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
AssetInputStream ais = (AssetInputStream) is;
dr = decodeImageDrawable(ais, wrapper, value);
}
从 Resource.getDrawable 会判断是否.xml 结尾,整个流程如下
getResources().getDrawable(...) -> ResourceImpl.loadDrawableForCookie(...) -> Drawable.createFromXml -> drawableInflater.inflateFromXmlForDensity(...) -> drawable.inflate(...)
Resources 的作用是将整个过程进行了封装、同时实现了资源的缓存。因此,为了更加直白的了解加载过程,以上步骤我们可以精简如下:
java
> Drawable.createFromXml(...) -> DrawableInflater.inflateFromXmlForDensity(...) -> drawable.inflate(...)
注意:Drawable 和 drawable,前者是类,后者是类的实例,同样 drawableInflater 也是类的实例。
本篇,我们通过一个自定义的Drawable,如下图,将其放入res目录中进行加载。
二、Drawable加载和方法解析
Drawable.createFromXml 是静态调用,实际上整个过程是 XmlPull 的解析。最终,会调用到 createFromXmlInnerForDensity
java
@NonNull
public static Drawable createFromXmlForDensity(@NonNull Resources r,
@NonNull XmlPullParser parser, int density, @Nullable 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.
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
Drawable drawable = createFromXmlInnerForDensity(r, parser, attrs, density, theme);
if (drawable == null) {
throw new RuntimeException("Unknown initial tag: " + parser.getName());
}
return drawable;
}
@NonNull
static Drawable createFromXmlInnerForDensity(@NonNull Resources r,
@NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density,
@Nullable Theme theme) throws XmlPullParserException, IOException {
//通过Resources里面的getDrawableInflater得到DrawableInflater的实例
return r.getDrawableInflater().inflateFromXmlForDensity(parser.getName(), parser, attrs,
density, theme);
}
DrawableInflater.inflateFromXmlForDensity 方法用来加载 Drawable 资源,如果不是我们自定义的 Drawable类,逻辑流程通常如下解析:
java
@NonNull
public Drawable inflateFromXml(@NonNull String name, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
if (name.equals("drawable")) { //无意义的drawable
name = attrs.getAttributeValue(null, "class");
if (name == null) {
throw new InflateException("<drawable> tag must specify class attribute");
}
}
Drawable drawable = inflateFromTag(name); //解析处Drawable的实例
if (drawable == null) {
drawable = inflateFromClass(name);
}
drawable.inflate(mRes, parser, attrs, theme);
//得到drawable实例,通过drawable.inflate去实现属性的解析
return drawable; //返回实例
}
通过上面代码我们知道,最终需要通过inflateFromTag生存相应的Drawable累,最终实现加载
inflateFromTag 源码如下:
java
@NonNull
@SuppressWarnings("deprecation")
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;
}
}
那么 drawable.inflate 方法是如何实现的?
Drawable 本身是抽象类,根据不同实现去解析属性,我们以 ShapeDrawable 为例,一般的通过 TypeArray 解析当前节点的属性,如果存在子元素继续遍历。
java
@Override
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
throws XmlPullParserException, IOException {
super.inflate(r, parser, attrs, theme);
final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ShapeDrawable);
updateStateFromTypedArray(a);
a.recycle();
int type;
final int outerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
// 解析子节点
if (!inflateTag(name, r, parser, attrs)) {
android.util.Log.w("drawable", "Unknown element: " + name +
" for ShapeDrawable " + this);
}
}
// Update local properties.
updateLocalState();
}
三、实现自定义 Drawable
通常我们说的自定义 drawable 是自定义 xml 文件,如果实现一种可以复用并且 Android 系统中没有内置的 Drawable,此外实现多个布局文件的引用,当然你可以说完全可以将代码自定义到静态方法中,实现多次引用也是可以,不过我们按照 Android 的建议,图形化的对象尽量以 xml 形式呈现。
下面,我们定义一个形状如下的 Drawable:
3.1、原理分析
那么,要实现 "自定义 Drawable 类的加载" 需求,比如要进行技术可行性分析,那我们的依据是什么呢?
在 DrawableInflater 中,除了通过 inflateFromTag 优先解析 Drawable 之外,我们发现同样提供了 inflateFromClass,通过这种方式我们同样可以得到 Drawable 子类的实例。
ini
Drawable drawable = inflateFromTag(name); //解析处Drawable的实例
if (drawable == null) {
drawable = inflateFromClass(name);
}
inflateFromClass 的实现如下:
java
@NonNull
private Drawable inflateFromClass(@NonNull String className) {
try {
Constructor<? extends Drawable> constructor;
synchronized (CONSTRUCTOR_MAP) {
constructor = CONSTRUCTOR_MAP.get(className);
if (constructor == null) {
//通过ClassLoader加载Drawable类,然后转为Drawable类
final Class<? extends Drawable> clazz =
mClassLoader.loadClass(className).asSubclass(Drawable.class);
constructor = clazz.getConstructor();
CONSTRUCTOR_MAP.put(className, constructor);
}
}
return constructor.newInstance(); //创建Drawable实例
} catch (Exception e) {
//省略
}
return null;
}
注意:我们通过 ClassLoader 去加载类,那么还要注意一个事情就是混淆,混淆时我们必须注意我们自定义的 Drawable 类不能被混淆,否则无法加载。
java
-keepclassmembers class * extends android.graphics.drawable.Drawable{
public void *(android.view.View);
}
3.2、代码示例
定义图形
首先,我们需要定义一个 Shape 图形,在 Android 系统中,实现圆角圆弧最好的方式是通过 Path 实现。
java
public class RadiusBorderShape extends Shape {
private Path mPath;
@ColorInt
private int color; //边框颜色
private float strokeWidth; //线宽
private float[] radius; //各个角的radius
@ColorInt
private int backgroundColor; //背景填充颜色
public void setColor(@ColorInt int color) {
this.color = color;
}
public void setRadius(float[] radius) {
if(radius==null || radius.length<4){
this.radius = new float[4];
}else{
this.radius = radius;
}
for (int i=0;i<this.radius.length;i++){
float v = this.radius[i];
if(v<0) {
this.radius[i] = 0f;
}
}
}
public void setStrokeWidth(float strokeWidth) {
if(strokeWidth<0) {
strokeWidth = 0;
}
this.strokeWidth = strokeWidth;
}
public RadiusBorderShape(){
mPath = new Path();
this.strokeWidth = 5f;
this.color = Color.RED;
this.backgroundColor = Color.GREEN;
this.radius = new float[]{5f,0f,20f,30f};
}
@Override
public void draw(Canvas canvas, Paint paint) {
Paint.Style old_style = paint.getStyle();
int old_color = paint.getColor();
float old_strokeWidth = paint.getStrokeWidth();
paint.setStrokeWidth(this.strokeWidth);
int backgroundId = canvas.save();
canvas.translate(strokeWidth,strokeWidth);
drawBackground(canvas, paint);
drawBorder(canvas, paint);
canvas.restoreToCount(backgroundId);
paint.setStyle(old_style);
paint.setColor(old_color);
paint.setStrokeWidth(old_strokeWidth);
}
private void drawBorder(Canvas canvas, Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setColor(this.color);
canvas.scale(1, 1);
canvas.drawPath(mPath, paint);
}
private void drawBackground(Canvas canvas, Paint paint) {
final Path.FillType fillType = mPath.getFillType();
int borderId = canvas.save();
paint.setStyle(Paint.Style.FILL);
paint.setColor(this.backgroundColor);
if(this.backgroundColor!=Color.TRANSPARENT){
mPath.setFillType(Path.FillType.WINDING); //填充,兼容低版本无法填充的问题
}
canvas.drawPath(mPath, paint);
canvas.restoreToCount(borderId);
mPath.setFillType(fillType);//还原
}
@Override
protected void onResize(float width, float height) {
super.onResize(width, height);
float w = width - strokeWidth*2; //减去左右侧的线宽
float h = height - strokeWidth*2; //减去上下侧的线宽
mPath.reset();
if(w<=0 && h<=0){
return;
}
float leftTopThresold = radius[0];
mPath.moveTo(0,leftTopThresold);
//从180度处顺时针旋转,增量90度
mPath.arcTo(new RectF(0,0,leftTopThresold,leftTopThresold), 180f, 90f);
float rightTopThresold = radius[1];
mPath.lineTo(w-rightTopThresold,0);
mPath.arcTo(new RectF(w-rightTopThresold,0,w,rightTopThresold), 270f, 90f);
float rightBottomThresold = radius[2];
mPath.lineTo(w,h-rightBottomThresold);
mPath.arcTo(new RectF(w-rightBottomThresold,h-rightBottomThresold,w,h), 0f, 90f);
float leftBottomThresold = radius[3];
mPath.lineTo(leftBottomThresold,h);
mPath.arcTo(new RectF(0,h-leftBottomThresold,leftBottomThresold,h), 90f, 90f);
mPath.lineTo(0,leftTopThresold);
mPath.close();
}
@Override
public Shape clone() throws CloneNotSupportedException {
final RadiusBorderShape shape = (RadiusBorderShape) super.clone();
shape.mPath = new Path(mPath);
shape.radius = radius;
shape.strokeWidth = strokeWidth;
shape.color = color;
return shape;
}
public void setBackgroundColor(int backgroundColor) {
this.backgroundColor = backgroundColor;
}
}
在这个类中,最终要的 2 个方法是 onResize 和 draw 方法,shape.onResize 在 Drawable 中会被 drawable.onBoundsChanged 调用,从而实现 Drawable 大小的监听。
定义 Drawable
java
public class RadiusRectDrawable extends ShapeDrawable {
private int backgroundColor;
private RadiusBorderShape shape;
@Override
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme) throws XmlPullParserException, IOException {
TypedArray array = RadiusRectDrawable.obtainAttributes(r, theme, attrs, R.styleable.RadiusRectDrawable);
if(array==null) return;
backgroundColor = array.getColor(R.styleable.RadiusRectDrawable_backgroundColor, Color.TRANSPARENT);
array.recycle();
super.inflate(r, parser, attrs, theme);
}
//低版本api兼容
@Override
public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs) throws XmlPullParserException, IOException {
TypedArray array = RadiusRectDrawable.obtainAttributes(r, null, attrs, R.styleable.RadiusRectDrawable);
if(array==null) return;
backgroundColor = array.getColor(R.styleable.RadiusRectDrawable_backgroundColor, Color.TRANSPARENT);
array.recycle();
super.inflate(r, parser, attrs);
}
@Override
protected boolean inflateTag(String name, Resources r, XmlPullParser parser, AttributeSet attrs) {
if("RadiusBorderShape".equals(name)){
TypedArray array = r.obtainAttributes(attrs, R.styleable.RadiusRectDrawable);
int lineColor = array.getColor(R.styleable.RadiusRectDrawable_lineColor, Color.TRANSPARENT);
float lineWidth = array.getFloat(R.styleable.RadiusRectDrawable_lineWidth, 0f);
float leftTopRadius = array.getDimensionPixelSize(R.styleable.RadiusRectDrawable_leftTop_radius, 0);
float leftBottomRadius = array.getDimensionPixelSize(R.styleable.RadiusRectDrawable_leftBottom_radius, 0);
float rightTopRadius = array.getDimensionPixelSize(R.styleable.RadiusRectDrawable_rightTop_radius, 0);
float rightBottomRadius = array.getDimensionPixelSize(R.styleable.RadiusRectDrawable_rightBottom_radius, 0);
if(shape==null){
shape = new RadiusBorderShape();
}
shape.setColor(lineColor);
shape.setStrokeWidth(lineWidth);
shape.setRadius(new float[]{leftTopRadius,rightTopRadius,rightBottomRadius,leftBottomRadius});
shape.setBackgroundColor(backgroundColor);
if(shape!=getShape()){
setShape(shape);
}
array.recycle();
return true;
}
else{
return super.inflateTag(name, r, parser, attrs);
}
}
protected static @NonNull TypedArray obtainAttributes(@NonNull Resources res,
@Nullable Resources.Theme theme, @NonNull AttributeSet set, @NonNull int[] attrs) {
if (theme == null) {
return res.obtainAttributes(set, attrs);
}
return theme.obtainStyledAttributes(set, attrs, 0, 0);
}
}
这个就是我们自己定义的 Drawable 类,当然,自定义往往需要自定义属性。
ini
<declare-styleable name="RadiusRectDrawable">
<attr name="lineColor" format="color|reference"/>
<attr name="backgroundColor" format="color|reference"/>
<attr name="lineWidth" format="float|reference"/>
<attr name="leftTop_radius" format="dimension|reference" />
<attr name="leftBottom_radius" format="dimension|reference" />
<attr name="rightBottom_radius" format="dimension|reference" />
<attr name="rightTop_radius" format="dimension|reference" />
</declare-styleable>
四、实现Drawable 加载
定义 drawable xml文件
自定义 drawble 的 xml 文件,安装惯例应该在 drawable 资源文件夹下,但是我们的编译器表现的有些不友好,要求 sdk 版本大于 24(android 7.0) 才行。
从 ResourcesImpl.loadDrawableForCookie 加载逻辑来看,文件加载主要通过 2 种方式,文件读取的核心代码如下:
java
if (file.endsWith(".xml")) {
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
rp.close();
} else {
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
AssetInputStream ais = (AssetInputStream) is;
dr = decodeImageDrawable(ais, wrapper, value);
}
一般代码实际上可以通过 loadXmlResourceParser 或者 mAssets.openNonAsset 加载,前者加载 xml 文件 内置资源,后者加载图片文件内置资源。通过 loadXmlResourceParser 加载文件,最后一个参数制定的是 drawable,但是从 loadXmlResourceParser 源码中并未使用第四个参数(篇幅有限,ResourcesImpl 源码自行查看),也就是说,加载资源时并没有对资源文件所在目录进行校验。
因此说,编译器会校验类型,但运行时不会校验。这样我们可以将 xml 文件放置到 非drawable 目录,可以是 Assets 文件夹中,同样也可以是 xml 资源文件夹下,当然,如果不觉得"报红"难受的话放mipmap或者drawable文件夹中也是可以的。
我们这里将定义文件放置到 xml 资源目录即可。
源码内容如下:
xml
<?xml version="1.0" encoding="utf-8"?>
<com.example.cc.myapplication.shape.RadiusRectDrawable
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:backgroundColor="@color/white"
>
<RadiusBorderShape
app:lineColor="@color/colorAccent"
app:lineWidth="5.5"
app:leftTop_radius="50dip"
app:leftBottom_radius="0dip"
app:rightTop_radius="0dip"
app:rightBottom_radius="0dip"
/>
</com.example.cc.myapplication.shape.RadiusRectDrawable>
加载并使用
事实上由于编译工具的要求 sdk api 大于 24 才可以使用,同样,Android 7.0之后Android才允许自定义的drawable可以通过DrawableInflater加载,因此,我们 android:background="@xml/radius_border" 显然存在问题,除非我们自行实现 LayoutInfater.Factory2,通过自定义的方式去拦截和解析,显然,android 7.0之后的版本加载是没有任何问题的,但是如何兼容Android 7.0 以前的Android 系统呢。
在解决这个问题之前,我们先实现一个工具,把加载逻辑实现了,要不然下一步万一不成功岂不是很尴尬?
java
public class ResourceUtils {
private static final HashMap<String, Constructor<? extends Drawable>> CONSTRUCTOR_MAP =
new HashMap<>();
private Context context;
private ResourceUtils(Context context){
this.context = context;
}
public Context getContext() {
return context;
}
//加载drawable
public static Drawable getDrawable(Context context, int xmlShapeId){
try {
ResourceUtils resourceUtils = new ResourceUtils(context);
return resourceUtils.parseDrawable(xmlShapeId);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
private Drawable parseDrawable(int xmlId) { //R.xml.radius_border)
Drawable drawable = null;
try{
if(Build.VERSION.SDK_INT<24) {
drawable = parseDrawableFromClass(xmlId);
}
if(drawable!=null){
return drawable;
}
Context context = getContext();
Resources resources = context.getResources();
XmlResourceParser xmlParse = resources.getXml(xmlId);
if(Build.VERSION.SDK_INT>=21) {
drawable = Drawable.createFromXml(resources, xmlParse, context.getTheme());
}else{
drawable = Drawable.createFromXml(resources, xmlParse);
}
xmlParse.close();
}catch (Exception e){
e.printStackTrace();
}
return drawable;
}
private Drawable parseDrawableFromClass(int xmlId){
Drawable drawable = null;
try {
Context context = getContext();
Resources resources = context.getResources();
XmlResourceParser xmlParse = resources.getXml(xmlId);
AttributeSet attrs = Xml.asAttributeSet(xmlParse);
int type;
while ((type = xmlParse.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
drawable = inflateFromClass(xmlParse.getName());
if(drawable==null) return null;
if (Build.VERSION.SDK_INT >= 21) {
drawable.inflate(resources, xmlParse, attrs, context.getTheme());
} else {
drawable.inflate(resources, xmlParse, attrs);
}
}catch (Exception e){
e.printStackTrace();
}
return drawable;
}
@NonNull
private Drawable inflateFromClass(@NonNull String className) {
try {
Constructor<? extends Drawable> constructor;
synchronized (CONSTRUCTOR_MAP) {
constructor = CONSTRUCTOR_MAP.get(className);
if (constructor == null) {
//通过ClassLoader加载Drawable类,然后转为Drawable类
final Class<? extends Drawable> clazz =
getClass().getClassLoader().loadClass(className).asSubclass(Drawable.class);
constructor = clazz.getConstructor();
CONSTRUCTOR_MAP.put(className, constructor);
}
}
return constructor.newInstance(); //创建Drawable实例
} catch (Exception e) {
//省略
}
return null;
}
}
当然,用法我们以 ImageView 为例
java
Drawable drawable = ResourceUtils.getDrawable(mContext,R.xml.radius_border);
myImageView.setBackgroundDrawable(drawable);
显然这个方法是成功的
那么,回到本小节前面,如何兼容Android 7.0之前的版本?
Hook Resources 方案
其实我们最理想的是扩展DrawableInflater,但是其本身是final修饰,所以,这招显然是不行的,不然换肤框架就不会那么难了
java
public final class DrawableInflater {
...
}
做过换肤的开发者应该知道,在Resources中有2个对象,每次加载资源时都会优先从这里查找一下
java
@UnsupportedAppUsage
private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables;
......
final Drawable.ConstantState cs;
if (isColorDrawable) {
cs = sPreloadedColorDrawables.get(key);
} else {
cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
}
但问题是,这辆都是final修饰,了解过反射的都知道,final字段是不可替换的,但是我们看到,这两者本身也是数组,替换不了这俩对象,但是还可以替换其中的数组元素的,而这里这两者本身就是LongSparseArray,也是允许继承的
java
public class LongSparseArray<E> implements Cloneable {
....
}
因此,我们给sPreloadedDrawables第0号元素(0号意味着从左到右的布局)替换为我们自定义的LongSparseArray
不过,为了提高兼容性,我们对ResourceUtils进行改造,移除对android 7.0之后版本的支持
java
public class ResourceUtils {
private static final HashMap<String, Constructor<? extends Drawable>> CONSTRUCTOR_MAP =
new HashMap<>();
private Context context;
private ResourceUtils(Context context) {
this.context = context;
}
public Context getContext() {
return context;
}
//加载drawable
public static Drawable getDrawable(Context context, long xmlShapeId) {
try {
ResourceUtils resourceUtils = new ResourceUtils(context);
return resourceUtils.parseDrawable(xmlShapeId);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private Drawable parseDrawable(long xmlId) { //R.xml.radius_border)
Drawable drawable = null;
try {
if (Build.VERSION.SDK_INT < 24) {
drawable = parseDrawableFromClass(xmlId);
}
} catch (Exception e) {
e.printStackTrace();
}
return drawable;
}
private Drawable parseDrawableFromClass(long xmlId) {
Drawable drawable = null;
try {
Context context = getContext();
Resources resources = context.getResources();
XmlResourceParser xmlParse = resources.getXml((int) xmlId);
AttributeSet attrs = Xml.asAttributeSet(xmlParse);
int type;
while ((type = xmlParse.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
drawable = inflateFromClass(xmlParse.getName());
if (drawable == null) return null;
if (Build.VERSION.SDK_INT >= 21) {
drawable.inflate(resources, xmlParse, attrs, context.getTheme());
} else {
drawable.inflate(resources, xmlParse, attrs);
}
} catch (Exception e) {
e.printStackTrace();
}
return drawable;
}
@NonNull
private Drawable inflateFromClass(@NonNull String className) {
try {
Constructor<? extends Drawable> constructor;
synchronized (CONSTRUCTOR_MAP) {
constructor = CONSTRUCTOR_MAP.get(className);
if (constructor == null) {
//通过ClassLoader加载Drawable类,然后转为Drawable类
final Class<? extends Drawable> clazz =
getClass().getClassLoader().loadClass(className).asSubclass(Drawable.class);
constructor = clazz.getConstructor();
CONSTRUCTOR_MAP.put(className, constructor);
}
}
return constructor.newInstance(); //创建Drawable实例
} catch (Exception e) {
//省略
}
return null;
}
}
下面,我们要自定义HookLongSparseArray
java
public class HookLongSparseArray<E> extends LongSparseArray<E> {
Context context;
public HookLongSparseArray(Context context) {
this.context = context;
}
@Override
public E get(long key) {
E e = super.get(key);
if(e != null){
return e;
}
if(Build.VERSION.SDK_INT > 24){
return null;
}
return ResourceUtils.getDrawable(context,RessourceMap.keyToResId(key));
}
....
}
这就完事了么?
并没有,我们看看RessourceMap#keyToResId需要特殊处理
但问题是,我们需要的是资源id而不是key,这是一个相当尴尬的问题
java
key = (((long) value.assetCookie) << 32) | value.data;
那怎么修复这个问题呢?
其实可以在app启动时,将这些特殊的资源文件进行扫描,然后形成
key->value的映射关系。
java
private final List<Integer> = new ArrayList(){
add(R.xml.radius_border);
};
Resources getResources(){
return AppContext.get().getResources();
}
private void init(){
final TypedValue tv = new TypedValue();
for(int resId : residList){
getResources().getValue(resId, tv, true);
long key = (((long) tv.assetCookie) << 32) | tv.data;
keyResMap.put(key,resId);
}
}
那么,后续我们从keyResMap中通过key获取resId即可。
然后根据拿到的有效id进行调用ResourceUtil加载Drawable
AndroidX ResourceManagerInternal 方案
ResourceManagerInternal是单例,在androidx中负责拦截Resources进行Drawable兼容,不过,最大的潜力就是换肤了。
可以注意到Androidx 中的ResourceManagerInternal 实现了hooks机制,这和库非常有潜力做皮肤框架。
然而,不幸的是,ResourceManagerInternal 其实对外并不公开,下面代码一直报红。
java
ResourceManagerInternal.get().setHooks(new ResourceManagerInternal.ResourceManagerHooks() {
@Override
public Drawable createDrawableFor(@NonNull ResourceManagerInternal appCompatDrawableManager, Context context, int resId) {
return null;
}
@Override
public boolean tintDrawable(@NonNull Context context, int resId, @NonNull Drawable drawable) {
return false;
}
@Override
public ColorStateList getTintListForDrawableRes(@NonNull Context context, int resId) {
return null;
}
@Override
public boolean tintDrawableUsingColorFilter(@NonNull Context context, int resId, @NonNull Drawable drawable) {
return false;
}
@Override
public PorterDuff.Mode getTintModeForDrawableRes(int resId) {
return null;
}
});
另外,还有delegate方式,仍然不希望外部使用
java
private static void installDefaultInflateDelegates(@NonNull ResourceManagerInternal manager) {
// This sdk version check will affect src:appCompat code path.
// Although VectorDrawable exists in Android framework from Lollipop, AppCompat will use
// (Animated)VectorDrawableCompat before Nougat to utilize bug fixes & feature backports.
if (Build.VERSION.SDK_INT < 24) {
manager.addDelegate("vector", new VdcInflateDelegate());
manager.addDelegate("animated-vector", new AvdcInflateDelegate());
manager.addDelegate("animated-selector", new AsldcInflateDelegate());
}
}
但是话又说回来,ResourceManagerInternal远比拦截sPreloadedDrawables可靠的多,因此,我个人的建议是想办法实现hooks,或者反射调用addDelegate注入再记得InflateDelegate,毕竟目前而言,大部份的项目都有androidx。
另外,这两者本身都是androidx的库,我们其实可以使用动态代理实现hooks,如果不喜欢hooks,那就老老实实反射addDelegate方法吧。
java
invokeAddDelegate("com.example.cc.myapplication.shape.RadiusRectDrawable", new RadiusldcInflateDelegate());
至于delegate的实现,参考其他delegate即可,这里就不再深入了。
两方案对比
对比两种方案的优劣,鉴于ResourceManagerInternal 兼容性更强,理论上更适合做皮肤框架,且不需要对资源进行搜索和映射,这里我们推荐ResourceManagerInternal 方案。
五、总结
好了,以上就是本篇方案实现的核心逻辑.
我们通过这种方式成功实现了自定义 Drawable xml的加载,同时通过hook resources 或者 反射调用ResourceManagerInternal 实现了兼容了android 7.0之前的版本。
本篇就这些内容,觉得可以话请点赞收藏。