本文已参与「新人创作礼」活动,一起开启掘金创作之路。在xml中使用自定义Drawable

656 阅读3分钟

1、概述

在xml中使用自定义Drawable,不用new一个自定义的Drawable对象,在布局中通过@drawable/文件名.xml 直接引用

2、缺陷:

api24才开始支持。如果项目minSdkVersion<24,则这种创建的xml文件只能放在 drawable-v24 目录下。

3、使用方式

创建一个自定义的Drawable

新建一个类,继承Drawable 或它的子类

创建自定义drawable后,在drawable-v24目录下创建xml , 我们取名为custom_drawable.xml。 名字是随意的

<drawable xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    class="com.example.test.drawable.TextDrawable"
    app:td_adjustViewBounds="true"
    app:td_text="创新层"
    app:td_text_color="#ff0"
    app:td_text_size_ratio="0.8">

    <corners android:radius="3dp" />
    <solid android:color="#f0f" />
    <size
        android:width="60dp"
        android:height="30dp" />
</drawable>

或是

<com.example.test.drawable.TextDrawable xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:td_adjustViewBounds="true"
    app:td_text="创新层"
    app:td_text_color="#ff0"
    app:td_text_size_ratio="0.8">

    <corners android:radius="3dp" />
    <solid android:color="#f0f" />
    <size
        android:width="60dp"
        android:height="30dp" />
</com.example.test.drawable.TextDrawable>

​编辑

标签源码解析标签如下

在 DrawableInflater 类中,如果标签名是 drawable,则会获取 class属性,对应的是自定义drawable 完整类名; 如果所有的Drawable标题都没有匹配上,则会直接拿标签名执行 inflateFromClass(name) 方法,通过反射执行无参构靠 方法。所以自定义Drawable中最好有无参构造方法。

public final class DrawableInflater {  
@NonNull
    Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
            @NonNull AttributeSet attrs, int density, @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.setSrcDensityOverride(density);
        drawable.inflate(mRes, parser, attrs, theme);
        return drawable;
    }

    @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 "adaptive-icon":
                return new AdaptiveIconDrawable();
            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();
            case "animated-image":
                return new AnimatedImageDrawable();
            default:
                return null;
        }
    }
}

引用自定义的drawable资源

使用起来非常方便,就像引用图片资源是一样的。

    <View
        android:id="@+id/z1"
        android:background="@drawable/sg1"
        android:layout_width="60dp"
        android:layout_height="25dp" />

问题

引用自定义的drawable xml文件,会发现只有第一次引用 custom_drawable.xml时,它会生效,以后就再也不会生效了。debug也发现再次引用时,通过 getResource().getDrawable(R.drawable.custom_drawable) 获取的Drawable对象,居然是自定义Drawable的子类。

​编辑

原因:这是因为创建Drawable后有缓存机制,以资源的id 为key,如果再次解析这个资源文件时,就会走到缓存逻辑。 例如  custom_drawable.xml 文件被第二次使用时,它就会走缓存逻辑。存储是一个map, key为资源id,value为资源中对应drawable的 ConstantState对象 . 由它来创建一个新的Drawable对象,但新的drawable对象还是持有原来的 ConstantState 。
ConstantState 对象是用于创建Drawable 和 保存状态的一个抽象类。 以GradientDrawable为例,设置圆角、背景色、边框等参数在解析xml时保存到GradientState类中,GradientState类是GradientDrawable的静态内部类,继承ConstantState抽象类。

getResource().getDrawable()

ResourcesImpl 处理资源,会先到DrawableCache类中找有没有缓存的Drawable.ConstantState对象,如果有,则执行下段代码中 entry.newDrawable(resources,theme) 方法,返回drawable对象。
ResourcesImplfinal Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                if (cachedDrawable != null) {
                    cachedDrawable.setChangingConfigurations(value.changingConfigurations);
                    return cachedDrawable;
                }


class DrawableCache extends ThemedResourceCache<Drawable.ConstantState> {

    @UnsupportedAppUsage
    DrawableCache() {
    }
 
    public Drawable getInstance(long key, Resources resources, Resources.Theme theme) {
        final Drawable.ConstantState entry = get(key, theme);
        if (entry != null) {
           //如果有缓存,则通过ConstantState对象执行newDrawable()方法获取一个新的Drawable对象。
            return entry.newDrawable(resources, theme);
        }

        return null;
    }

    @Override
    public boolean shouldInvalidateEntry(Drawable.ConstantState entry, int configChanges) {
        return Configuration.needNewResources(configChanges, entry.getChangingConfigurations());
    }
}

我们看一下GradientDrawable.GradientState 类,这也就是上面我们提到为什么引用 自定义drawable xml文件时,只有第一次会生效,后面都成了子类对象。原因就是自定Drawable时并没有创建自己的ConstantState,在第一次顺利加载后,第二次从缓存中找ConstantSate对象,最终执行了子类的newDrawable()方法。所以如果要解决这个问题,在自定义Drawable对象时,且需要在xml中使用,则必须创建自己的ConstantState对象。

        
 final static class GradientState extends ConstantState {
        @Override
        public Drawable newDrawable() {
            //通过state对象创建一个GradientDrawable对象
            return new GradientDrawable(this, null);
        }

        @Override
        public Drawable newDrawable(@Nullable Resources res) {
            // If this drawable is being created for a different density,
            // just create a new constant state and call it a day.
            final GradientState state;
            final int density = Drawable.resolveDensity(res, mDensity);
            if (density != mDensity) {
                state = new GradientState(this, res);
            } else {
                state = this;
            }
            
            return new GradientDrawable(state, res);
        }

...
}

自定义Drawable创建ConstanteState对象

public class KxGradientDrawable extends GradientDrawable {

    public boolean mMutated;
    private KxGradientState mKxGradientState;
    private int mWidth;
    private int mHeight;

    public KxGradientDrawable() {
        this(new KxGradientState(), null);
    }

    public KxGradientDrawable(KxGradientState state, Resource res) {
        super();
        mKxGradientState = state;//不能保存,否则不能重新创建
        if (mKxGradientState != null) {
            mKxGradientState.setDrawable(this);
        }
    }

    @Override
    public Drawable mutate() {
        if (!mMutated && super.mutate() == this) {
            //重新创建一个state对象
            mKxGradientState = new KxGradientState(mKxGradientState, null);
            onUpdateLocalState(null);
            mMutated = true;
        }
        return this;
    }

    @Override
    public final void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) throws IOException, XmlPullParserException {
        onInflatte(r, parser, attrs, theme);
        super.inflate(r, parser, attrs, theme);
    }

    public void onInflatte(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme) {
        //记录自定义属性
        if (mKxGradientState != null) {
//            mKxGradientState
        }
    }
    public void clearMutated() {
        mMutated = false;
    }

    protected void onUpdateLocalState(Resources res) {
    }


    @Override
    public ConstantState getConstantState() {
        super.getConstantState();
//        mGradientState.mChangingConfigurations = getChangingConfigurations();
        return mKxGradientState;
    }

    public ConstantState getParentState() {
        return super.getConstantState();
    }


    @Override
    protected void onBoundsChange(Rect r) {
        super.onBoundsChange(r);
        int left = r.left;
        int right = r.right;
        int top = r.top;
        int bottom = r.bottom;
        mWidth = right - left;
        mHeight = bottom - top;
    }

    public int getWidth() {
        return mWidth;
    }

    public int getHeight() {
        return mHeight;
    }

    public static TypedArray obtainAttributes(Resources res,
                                              Resources.Theme theme, AttributeSet set, int[] attrs) {
        if (theme == null) {
            return res.obtainAttributes(set, attrs);
        }
        return theme.obtainStyledAttributes(set, attrs, 0, 0);
    }

    public static class KxGradientState extends ConstantState {

        protected KxGradientDrawable mDrawable;

        public KxGradientState() {
        }

        public KxGradientState(KxGradientState orig, Resource res) {
            if (orig != null) {
                //记录原成员变量数据  自定义属性等
            }
        }

        @NonNull
        @Override
        public Drawable newDrawable() {
            return new KxGradientDrawable(this,null);
        }

        @NonNull
        @Override
        public Drawable newDrawable(@Nullable Resources res) {
            return new KxGradientDrawable(this,null);
        }

        @Override
        public int getChangingConfigurations() {
            return 0;
        }

        public void setDrawable(KxGradientDrawable drawable) {
            mDrawable = drawable;
        }
    }
}

备注:

由于系统原生的Drawable的几个子类的ConstantState对象全部被final处理了,所以根本没有办法继承和复写。