背景
近期在开发一些界面,需要在不同场景弹出同一个界面,位移的区别就是改一下界面的背景颜色。但是在项目中发现改了颜色会导致全局的其他界面的相同颜色背景都改了。
布局如下
<View
android:id="@+id/vMask"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/overlay_page_mask"
android:alpha="0"
android:visibility="gone"/>
在不同场景修改背景颜色,刚好调用了项目中封装的扩展方法,直接修改drawble的颜色
binding.vMask.backgroundColor(R.color.overlay_layer_background.asColor(appContext))
fun View.backgroundColor(@ColorInt color: Int) {
this.background.let {
when (it) {
//原本已设置背景的,直接修改背景色,其他属性会得以保留,比如圆角
is GradientDrawable -> it.setColor(color)
is ColorDrawable -> it.color = color
null -> background = GradientDrawable().apply { setColor(color) }
}
}
}
问题来了,为什么我调用这个方法,会导致全局的同个颜色的背景都跟着修改,先来看看布局加载背景是怎么来的。开启源码阅读之旅
frameworks/base/core/java/android/view/View.java
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
mSourceLayoutId = Resources.getAttributeSetSourceResId(attrs);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
retrieveExplicitStyle(context.getTheme(), attrs);
saveAttributeDataForStyleable(context, com.android.internal.R.styleable.View, attrs, a,
defStyleAttr, defStyleRes);
if (sDebugViewAttributes) {
saveAttributeData(attrs, a);
}
Drawable background = null;
...
...
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.View_background:
background = a.getDrawable(attr);
break;
case com.android.internal.R.styleable.View_padding:
padding = a.getDimensionPixelSize(attr, -1);
mUserPaddingLeftInitial = padding;
mUserPaddingRightInitial = padding;
leftPaddingDefined = true;
rightPaddingDefined = true;
是从TypedArray.java 中的getDrawable
frameworks/base/core/java/android/content/res/TypedArray.java
public Drawable getDrawable(@StyleableRes int index) {
return getDrawableForDensity(index, 0);
}
public Drawable getDrawable(@StyleableRes int index) {
return getDrawableForDensity(index, 0);
}
@Nullable
public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
final TypedValue value = mValue;
if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
if (value.type == TypedValue.TYPE_ATTRIBUTE) {
throw new UnsupportedOperationException(
"Failed to resolve attribute at index " + index + ": " + value
+ ", theme=" + mTheme);
}
if (density > 0) {
// If the density is overridden, the value in the TypedArray will not reflect this.
// Do a separate lookup of the resourceId with the density override.
mResources.getValueForDensity(value.resourceId, density, value, true);
}
return mResources.loadDrawable(value, value.resourceId, density, mTheme);
}
return null;
}
frameworks/base/core/java/android/content/res/Resources.java
加载drawble
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
throws NotFoundException {
return mResourcesImpl.loadDrawable(this, value, id, density, theme);
}
frameworks/base/core/java/android/content/res/ResourcesImpl.java
@Nullable
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
int density, @Nullable Resources.Theme theme)
throws 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;
// Pretend the requested density is actually the display density. If
// the drawable returned is not the requested density, then force it
// to be scaled later by dividing its density by the ratio of
// requested density to actual device density. Drawables that have
// undefined density or no density don't need to be handled here.
if (density > 0 && value.density > 0 && value.density != TypedValue.DENSITY_NONE) {
if (value.density == density) {
value.density = mMetrics.densityDpi;
} else {
value.density = (value.density * mMetrics.densityDpi) / density;
}
}
try {
if (TRACE_FOR_PRELOAD) {
// Log only framework resources
if ((id >>> 24) == 0x1) {
final String name = getResourceName(id);
if (name != null) {
Log.d("PreloadDrawable", name);
}
}
}
final boolean isColorDrawable;
final DrawableCache caches;
final long key;
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
isColorDrawable = true;
caches = mColorDrawableCache;
key = value.data;
} else {
isColorDrawable = false;
caches = mDrawableCache;
key = (((long) value.assetCookie) << 32) | value.data;
}
// 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);
}
// DrawableContainer' constant state has drawables instances. In order to leave the
// constant state intact in the cache, we need to create a new DrawableContainer after
// added to cache.
if (dr instanceof DrawableContainer) {
needsNewDrawableAfterCache = true;
}
// Determine if the drawable has unresolved theme attributes. If it
// does, we'll need to apply a theme and store it in a theme-specific
// cache.
final boolean canApplyTheme = dr != null && dr.canApplyTheme();
if (canApplyTheme && theme != null) {
dr = dr.mutate();
dr.applyTheme(theme);
dr.clearMutated();
}
// 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);
if (needsNewDrawableAfterCache) {
Drawable.ConstantState state = dr.getConstantState();
if (state != null) {
dr = state.newDrawable(wrapper);
}
}
}
}
return dr;
} catch (Exception e) {
String name;
try {
name = getResourceName(id);
} catch (NotFoundException e2) {
name = "(missing name)";
}
// The target drawable might fail to load for any number of
// reasons, but we always want to include the resource name.
// Since the client already expects this method to throw a
// NotFoundException, just throw one of those.
final NotFoundException nfe = new NotFoundException("Drawable " + name
+ " with resource ID #0x" + Integer.toHexString(id), e);
nfe.setStackTrace(new StackTraceElement[0]);
throw nfe;
}
}
frameworks/base/core/java/android/content/res/DrawableCache.java
缓存中是key->drawble
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;
}
可以看出,从布局中定义的background,如果是纯色,首次会创建Drawable.ConstantState并且缓存,所以在布局中所有的纯色,其实对应的都是同一个drawble。
现在回到问题,调用下面方法,会修改drawble的信息,但是缓存中key没变,所以导致页面中的overlay_page_mask都被修改了。
View.backgroundColor(@ColorInt color: Int) {
// background Drawable
this.background.let {
when (it) {
//原本已设置背景的,直接修改背景色,其他属性会得以保留,比如圆角
is GradientDrawable -> it.setColor(color)
is ColorDrawable -> it.color = color
null -> background = GradientDrawable().apply { setColor(color) }
}
}
}
问题解决,如果需要直接修改drawble,可以通过mutate方法,官方注释也说明了该问题的
/**
* Make this drawable mutable. This operation cannot be reversed. A mutable
* drawable is guaranteed to not share its state with any other drawable.
* This is especially useful when you need to modify properties of drawables
* loaded from resources. By default, all drawables instances loaded from
* the same resource share a common state; if you modify the state of one
* instance, all the other instances will receive the same modification.
*
* Calling this method on a mutable Drawable will have no effect.
*
* @return This drawable.
* @see ConstantState
* @see #getConstantState()
*/
public @NonNull Drawable mutate() {
return this;
}
View.backgroundColor(@ColorInt color: Int) {
// background Drawable
this.background.mutate().let {
when (it) {
//原本已设置背景的,直接修改背景色,其他属性会得以保留,比如圆角
is GradientDrawable -> it.setColor(color)
is ColorDrawable -> it.color = color
null -> background = GradientDrawable().apply { setColor(color) }
}
}
}
网友评论