Android 利用换肤技术,实现自定义Drawable从Xml中加载

一、前言

在 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之前的版本。

本篇就这些内容,觉得可以话请点赞收藏。

相关推荐
Henrii_历小海1 小时前
WhatsApp Web 不是手机镜像:多设备协议、本地数据流与出海获客流水线
前端·智能手机
ayqy贾杰1 小时前
Claude Fable 5 提示词泄漏,抓紧学习下
前端·后端·面试
鱼儿也有烦恼2 小时前
07.RecyclerView、SwipeRefreshLayout、SmartRefreshLayout
android
三少爷的鞋2 小时前
Kotlin Flow 深入解析:`stateIn()` 的真正核心,其实是 SharingStarted
android
To_OC9 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic10 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy10 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志10 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble12 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯12 小时前
管理是否可以被完全量化
前端·产品经理·团队管理