自定义Drawable不生效问题

301 阅读2分钟

1 复现场景

1.1 先自定义Drawable

public class TestDrawable extends GradientDrawable {

}

1.2 xml中使用drawable

drawable_test.xml

<?xml version="1.0" encoding="utf-8"?>
<com.test.drawable.TestDrawable
    xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 设置圆角 -->
<!--    <corners android:radius="20dp"/>-->
</com.test.drawable.TestDrawable>

1.3 java/kotlin中使用

View v1 = findViewById(R.id.view1);
View v2 = findViewById(R.id.view2);
v1.setBackgroundResource(R.drawable.drawable_test);
Log.i("TAGTAG", "onCreate1: " + v1.getBackground());
v2.setBackgroundResource(R.drawable.drawable_test);
Log.i("TAGTAG", "onCreate2: " + v2.getBackground());

1.4 观察日志输出

image.png 同样的代码,v1使用TestDrawable,但v2却不是

2 原因分析

注意drawable的解析流程,ResourcesImpl#loadDrawable()

Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
                      int density, @Nullable Resources.Theme theme)
        throws Resources.NotFoundException {
    // If the drawable's XML lives in our current density qualifier,
    // it's okay to use a scaled version from the cache. Otherwise, we
    // need to actually load the drawable from XML.
    final boolean useCache = density == 0 || value.density == mMetrics.densityDpi;

    // ... 省略部分代码

    try {
        // ... 省略部分代码

        // First, check whether we have a cached version of this drawable
        // that was inflated against the specified theme. Skip the cache if
        // we're currently preloading or we're not using the cache.
        if (!mPreloading && useCache) {
            final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
            if (cachedDrawable != null) {
                cachedDrawable.setChangingConfigurations(value.changingConfigurations);
                return cachedDrawable;
            }
        }

        // Next, check preloaded drawables. Preloaded drawables may contain
        // unresolved theme attributes.
        final Drawable.ConstantState cs;
        if (isColorDrawable) {
            cs = sPreloadedColorDrawables.get(key);
        } else {
            cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
        }

        Drawable dr;
        boolean needsNewDrawableAfterCache = false;
        if (cs != null) {
            // ... 省略部分代码
            dr = cs.newDrawable(wrapper);
        } else if (isColorDrawable) {
            dr = new ColorDrawable(value.data);
        } else {
            dr = loadDrawableForCookie(wrapper, value, id, density);
        }

        // If we were able to obtain a drawable, store it in the appropriate
        // cache: preload, not themed, null theme, or theme-specific. Don't
        // pollute the cache with drawables loaded from a foreign density.
        if (dr != null) {
            dr.setChangingConfigurations(value.changingConfigurations);
            if (useCache) {
                cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
                // ... 省略部分代码
            }
        }

        return dr;
    } catch (Exception e) {
        // ... 省略部分代码
    }
}

上述大概流程是先调用DrawableCache#getInstance()从缓存中取,若取到直接返回,若未取到,正常加载并将drawable调用ResourcesImpl#cacheDrawable()进行缓存。

查看DrawableCache#getInstance()源码:

public Drawable getInstance(long key, Resources resources, Resources.Theme theme) {
    final Drawable.ConstantState entry = get(key, theme);
    if (entry != null) {
        return entry.newDrawable(resources, theme);
    }

    return null;
}

由此可知,从缓存中取的并不是Drawable,而是Drawable.ConstantState: This abstract class is used by {@link Drawable}s to store shared constant state and data between Drawables, 再根据其newDrawable.

再看看缓存的地方:

private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
                           Resources.Theme theme, boolean usesTheme, long key, Drawable dr) {
    final Drawable.ConstantState cs = dr.getConstantState();
    if (cs == null) {
        return;
    }

    if (mPreloading) {
       // ... 省略部分代码
    } else {
        synchronized (mAccessLock) {
            caches.put(key, theme, cs, usesTheme);
        }
    }
}

故,重点排查对象就在Drawable.ConstantState上,实际上TestDrawable的处理在GradientDrawable中:

public class GradientDrawable  extends Drawable {

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

    final static class GradientState extends Drawable.ConstantState {
        // ... 省略部分代码
        @Override
        public Drawable newDrawable() {
            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是GradientDrawable而不是TestDrawable

3 解决办法

结合分析,对自定义Drawable不进行缓存即可,故使Drawable#getConstantState返回为null即可:

public class TestDrawable extends GradientDrawable {
    @Override
    public ConstantState getConstantState() {
        return null;
    }
}

image.png