最近在做项目的时候使用RecyclerView头部图片背景,上滑时标题颜色变深,下滑时变透明的功能,实现很简单
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mTotalDy += dy;
if (mTotalDy<=headerHeight) {
int alpha = (int) ((mTotalDy * 255) / headerHeight);
Drawable background = linearToolbar.getBackground();
if (background!=null) {
background.setAlpha(alpha);
}
} else {
Drawable background = linearToolbar.getBackground();
if (background!=null) {
background.setAlpha(255);
}
}
}
});
试了下效果还不错,然后就没管了,后来发现有时会出现标题栏背景变成透明,找了很久找不到原因,好像以前也有遇到过,不过在那公司没有解决,也不知道具体是什么bug导致,这次又出现了就研究了一下。
直接分析源码找出了原因,我们在布局界面设置的背景颜色是属于ColorDrawable
类型,再看他设置透明度的方法
/**
* Sets the color's alpha value.
*
* @param alpha The alpha value to set, between 0 and 255.
*/
@Override
public void setAlpha(int alpha) {
alpha += alpha >> 7; // make it 0..256
final int baseAlpha = mColorState.mBaseColor >>> 24;
final int useAlpha = baseAlpha * alpha >> 8;
final int useColor = (mColorState.mBaseColor << 8 >>> 8) | (useAlpha << 24);
if (mColorState.mUseColor != useColor) {
mColorState.mUseColor = useColor;
invalidateSelf();
}
}
很简单的看出使用mColorState
保存他的色值,那么是什么原因导致他会出现全局的颜色都变了呢?首先猜想ColorDrawable
是被共用了,通过对比布同一界面中的两个布局的背景的toString()
可以看出hashCode
不一样
然后再看ColorDrawable
是如何实例化的
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
...
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;
}
}
if (background != null) {
setBackground(background); // 设置为背景
}
}
可以看出我们获取的背景就是在这得到的drawable
,继续跟踪background = a.getDrawable(attr);
,最终调用的ResourcesImpl.loadDrawable(this, value, id, density, theme);
获取drawable
@Nullable
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
int density, @Nullable Resources.Theme theme)
throws NotFoundException {
final boolean useCache = density == 0 || value.density == mMetrics.densityDpi;
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 {
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;
}
if (!mPreloading && useCache) {
final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme); // 读取缓存里的drawable
if (cachedDrawable != null) {
cachedDrawable.setChangingConfigurations(value.changingConfigurations);
return cachedDrawable;
}
}
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) {
if (TRACE_FOR_DETAILED_PRELOAD) {
if (((id >>> 24) == 0x1) && (android.os.Process.myUid() != 0)) {
final String name = getResourceName(id);
if (name != null) {
Log.d(TAG_PRELOAD, "Hit preloaded FW drawable #"
+ Integer.toHexString(id) + " " + name);
}
}
}
dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(wrapper, value, id, density);
}
if (dr instanceof DrawableContainer) {
needsNewDrawableAfterCache = true;
}
final boolean canApplyTheme = dr != null && dr.canApplyTheme();
if (canApplyTheme && theme != null) {
dr = dr.mutate();
dr.applyTheme(theme);
dr.clearMutated();
}
if (dr != null) {
dr.setChangingConfigurations(value.changingConfigurations);
if (useCache) {
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr); // 将加载出来的drawable缓存起来
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;
}
}
缓存
private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
Resources.Theme theme, boolean usesTheme, long key, Drawable dr) {
final Drawable.ConstantState cs = dr.getConstantState(); // 拿出Drawable的ConstantState来缓存
if (cs == null) {
return;
}
// 通过点击查看mPreloading一直没被赋值
if (mPreloading) {
final int changingConfigs = cs.getChangingConfigurations();
if (isColorDrawable) {
if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) {
sPreloadedColorDrawables.put(key, cs);
}
} else {
if (verifyPreloadConfig(
changingConfigs, ActivityInfo.CONFIG_LAYOUT_DIRECTION, value.resourceId, "drawable")) {
if ((changingConfigs & ActivityInfo.CONFIG_LAYOUT_DIRECTION) == 0) {
// If this resource does not vary based on layout direction,
// we can put it in all of the preload maps.
sPreloadedDrawables[0].put(key, cs);
sPreloadedDrawables[1].put(key, cs);
} else {
// Otherwise, only in the layout dir we loaded it for.
sPreloadedDrawables[mConfiguration.getLayoutDirection()].put(key, cs);
}
}
}
} else {
synchronized (mAccessLock) {
// 将ConstantState缓存起来
caches.put(key, theme, cs, usesTheme);
}
}
}
通过分析到这里大概就明白了,主要是缓存了ConstantState
,然而我们设置透明度的时候又是改变的ConstantState的值,所以会导致全局使用相同背景颜色的控件都跟着改变,最后我们再来打印一验证一下
通过打印出来的数据可以看出确实是ConstantState
被缓存下来了,但是我们要如何处理呢,要想单独一个的ConstantState
改变而其他的不变,我们就要单独给他实例化,查看api有没有暴露方法给我们使用呢
可以看到这个方法有给mColorState
重新赋值,并且copy了同样的数据,这样我们就可以调用该方法改变单独这一个控件的背景透明度而不会影响其他控件。
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mTotalDy += dy;
if (mTotalDy<=headerHeight) {
int alpha = (int) ((mTotalDy * 255) / headerHeight);
Drawable background = linearToolbar.getBackground();
if (background!=null) {
background.mutate().setAlpha(alpha);
}
} else {
Drawable background = linearToolbar.getBackground();
if (background!=null) {
background.mutate().setAlpha(255);
}
}
}
});