一、前语

Android 体系中,图形图像的制作需求在画布上进行操作和处理,为了增强复用性,因而也对图形或许颜色供给了Drawable抽象。经过这个类能够减少咱们的制作工作和运用本钱,一起体系也供给了众多的 Drawable 的派生类比如单色、图形、位图、裁剪、动画等等来完成一些常见的制作需求。

Drawable 是一个抽象的可制作类,他主要是供给了一个可制作的区域 bound 特点以及一个 draw 成员函数,不同的派生类经过重载 draw 函数的完成而发生不同的制作成果。

如下是 Drawable xml文件加载的末端代码。

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 的作用是将整个进程进行了封装、一起完成了资源的缓存。因而,为了更加直白的了解加载进程,以上步骤咱们能够精简如下:

> Drawable.createFromXml(...) ->DrawableInflater.inflateFromXmlForDensity(...) ->drawable.inflate(...)

留意:Drawable 和 drawable,前者是类,后者是类的实例,相同drawableInflater也是类的实例。

本篇,咱们经过一个自界说的Drawable,如下图,将其放入res目录中进行加载。

Android  运用换肤技能,完成自界说Drawable从Xml中加载

二、Drawable加载和办法解析

Drawable.createFromXml 是静态调用,实际上整个进程是 XmlPull 的解析。终究,会调用到 createFromXmlInnerForDensity

 @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类,逻辑流程一般如下解析:

 @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 源码如下:

@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 解析当前节点的特点,假如存在子元素持续遍历。

  @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 机制能够增强Drawable的可复用性,图形化的目标尽量以xml形式呈现,运用上也有一定的优点。

下面,咱们界说一个形状如下的 Drawable:

Android  运用换肤技能,完成自界说Drawable从Xml中加载
Android  运用换肤技能,完成自界说Drawable从Xml中加载
Android  运用换肤技能,完成自界说Drawable从Xml中加载

3.1、原理分析

那么,要完成 “自界说 Drawable 类的加载” 需求,比如要进行技能可行性分析,那咱们的依据是什么呢?

在 DrawableInflater 中,除了经过 inflateFromTag 优先解析 Drawable 之外,咱们发现相同供给了 inflateFromClass,经过这种办法咱们相同能够得到 Drawable 子类的实例。

  Drawable drawable = inflateFromTag(name); //解析处Drawable的实例
 if (drawable == null) {
       drawable = inflateFromClass(name);
  }

inflateFromClass 的完成如下:

@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 类不能被混淆,不然无法加载。

-keepclassmembers class * extends android.graphics.drawable.Drawable{
    public void *(android.view.View);
}

3.2、代码示例

界说图形

首要,咱们需求界说一个 Shape 图形,在 Android 体系中,完成圆角圆弧最好的办法是经过 Path 完成。

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

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 类,当然,自界说往往需求自界说特点。

   <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) 才行。

Android  运用换肤技能,完成自界说Drawable从Xml中加载

从 ResourcesImpl.loadDrawableForCookie 加载逻辑来看,文件加载主要经过 2 种办法,文件读取的中心代码如下:

 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 资源目录即可。

Android  运用换肤技能,完成自界说Drawable从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 体系呢。

在解决这个问题之前,咱们先完成一个工具,把加载逻辑完成了,要不然下一步假如不成功岂不是很为难?

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 为例

Drawable drawable = ResourceUtils.getDrawable(mContext,R.xml.radius_border);
myImageView.setBackgroundDrawable(drawable);

明显这个办法是成功的

那么,回到本小节前面,如何兼容Android 7.0之前的版别?

Hook Resources 计划

其实咱们最理想的是扩展DrawableInflater,可是其自身是final润饰,所以,这招明显是不行的,不然换肤结构就不会那么难了

public final class DrawableInflater {
...
}

做过换肤的开发者应该知道,在Resources中有2个目标,每次加载资源时都会优先从这儿查找一下

@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,也是答应继承的

public class LongSparseArray<E> implements Cloneable {
....
}

因而,咱们给sPreloadedDrawables第0号元素(0号意味着从左到右的布局)替换为咱们自界说的LongSparseArray

不过,为了进步兼容性,咱们对ResourceUtils进行改造,移除对android 7.0之后版别的支撑

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

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,这是一个相当为难的问题

key = (((long) value.assetCookie) << 32) | value.data;

那怎样修正这个问题呢?

其实能够在app启动时,将这些特殊的资源文件进行扫描,然后形成

key->value的映射关系。


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 其实对外并不揭露,下面代码一向报红。

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办法,仍然不希望外部运用

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办法吧。

invokeAddDelegate("com.example.cc.myapplication.shape.RadiusRectDrawable", new RadiusldcInflateDelegate());

至于delegate的完成,参阅其他delegate即可,这儿就不再深入了。

两计划比照

比照两种计划的优劣,鉴于ResourceManagerInternal 兼容性更强,理论上更适合做皮肤结构,且不需求对资源进行搜索和映射,这儿咱们推荐ResourceManagerInternal 计划。

五、总结

好了,以上便是本篇计划完成的中心逻辑.

咱们经过这种办法成功完成了自界说 Drawable xml的加载,一起经过hook resources 或许 反射调用ResourceManagerInternal 完成了兼容了android 7.0之前的版别。

本篇就这些内容,觉得能够话请点赞保藏。