最近太忙,所以很久没有写新文章了。这次我们来讨论一下Drawable设置alpha的一个BUG。
一般来说,Drawable做alpha动画都是通过设置alpha来实现。比如使用
drawable.setAlpha(0); ... drawable.setAlpha(255);其中0是全透明,255是全不透明。
其实这里有一个很大的BUG,其中牵涉到Drawable的类型。我们以BitmapDrawable和NinePatchDrawable来举例。
在NinePatchDrawable中,设置Alpha值的代码如下:
@Override public void setAlpha(int alpha) { if (mPaint == null && alpha == 0xFF) { // Fast common case -- leave at normal alpha. return; } getPaint().setAlpha(alpha); invalidateSelf(); }其中getPaint()的代码如下:
public Paint getPaint() { if (mPaint == null) { mPaint = new Paint(); mPaint.setDither(DEFAULT_DITHER); } return mPaint; }也就是说,NinePatchDrawable设置alpha值的时候,会使用本身所有的一个Paint对象来实现Alpha值的变化。
再回头看看BitmapDrawable的实现。
@Override public void setAlpha(int alpha) { int oldAlpha = mBitmapState.mPaint.getAlpha(); if (alpha != oldAlpha) { mBitmapState.mPaint.setAlpha(alpha); invalidateSelf(); } }请注意,BitmapDrawable中,Paint对象实际是BitmapState的变量,而不是BitmapDrawable本身的。让我们找找BitmapState是从哪儿来的。
一般来说,我们的Drawable都是从resource里面读取而不是自己创建,所以,我们先聚焦从resource读取drawable的情况,所有从resource中读取Drawable的调用最终会调用到loadDrawable()。如下所示:
/*package*/ Drawable loadDrawable(TypedValue value, int id) throws NotFoundException { if (TRACE_FOR_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { final String name = getResourceName(id); if (name != null) android.util.Log.d("PreloadDrawable", name); } } final long key = (((long) value.assetCookie) << 32) | value.data; boolean isColorDrawable = false; if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { isColorDrawable = true; } Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key); if (dr != null) { return dr; } Drawable.ConstantState cs = isColorDrawable ? sPreloadedColorDrawables.get(key) : sPreloadedDrawables.get(key); if (cs != null) { dr = cs.newDrawable(this); } else { if (isColorDrawable) { dr = new ColorDrawable(value.data); } if (dr == null) { if (value.string == null) { throw new NotFoundException( "Resource is not a Drawable (color or path): " + value); } String file = value.string.toString(); if (TRACE_FOR_MISS_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { final String name = getResourceName(id); if (name != null) android.util.Log.d(TAG, "Loading framework drawable #" + Integer.toHexString(id) + ": " + name + " at " + file); } } if (DEBUG_LOAD) Log.v(TAG, "Loading drawable for cookie " + value.assetCookie + ": " + file); if (file.endsWith(".xml")) { try { XmlResourceParser rp = loadXmlResourceParser( file, id, value.assetCookie, "drawable"); dr = Drawable.createFromXml(this, rp); rp.close(); } catch (Exception e) { NotFoundException rnf = new NotFoundException( "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } } else { try { InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); // System.out.println("Opened file " + file + ": " + is); dr = Drawable.createFromResourceStream(this, value, is, file, null); is.close(); // System.out.println("Created stream: " + dr); } catch (Exception e) { NotFoundException rnf = new NotFoundException( "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } } } } if (dr != null) { dr.setChangingConfigurations(value.changingConfigurations); cs = dr.getConstantState(); if (cs != null) { if (mPreloading) { if (isColorDrawable) { sPreloadedColorDrawables.put(key, cs); } else { sPreloadedDrawables.put(key, cs); } } else { synchronized (mTmpValue) { //Log.i(TAG, "Saving cached drawable @ #" + // Integer.toHexString(key.intValue()) // + " in " + this + ": " + cs); if (isColorDrawable) { mColorDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); } else { mDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); } } } } } return dr; }请注意18行,这里我们会去尝试读取CachedDrawable,看看这里是否有我们需要读取的Drawable。显然,第一次读取的时候,是不会存在缓存。所以我们继续往下看。
关键是73行,这里会从资源中创建一个Drawable对象。这个函数最终会调用到drawableFromBitmap(),其中会new BitmapDrawable(res, bm):
public BitmapDrawable(Resources res, Bitmap bitmap) { this(new BitmapState(bitmap), res); mBitmapState.mTargetDensity = mTargetDensity; }请注意,我们创建了一个BitmapState对象。
继续看loadDrawable()函数,来到90行,这里,我们会把新的Drawable的ConstantState加入到缓存中。这个ConstantState对于BitmapDrawable来说,实际就是一个BitmapState对象。
final static class BitmapState extends ConstantState { }
然后,我们就完成第一次读取BitmapDrawable对象。
如果我们再次读取这个BitmapDrawable对象,回头看loadDrawable()的第18行,我们会从CachedDrawable中拿:
private Drawable getCachedDrawable( LongSparseArray<WeakReference<ConstantState>> drawableCache, long key) { synchronized (mTmpValue) { WeakReference<Drawable.ConstantState> wr = drawableCache.get(key); if (wr != null) { // we have the key Drawable.ConstantState entry = wr.get(); if (entry != null) { //Log.i(TAG, "Returning cached drawable @ #" + // Integer.toHexString(((Integer)key).intValue()) // + " in " + this + ": " + entry); return entry.newDrawable(this); } else { // our entry has been purged drawableCache.delete(key); } } } return null; }
请看12行,对于BitmapDrawable的BitmapState来说,newDrawable()就是:
@Override public Drawable newDrawable(Resources res) { return new BitmapDrawable(this, res); }
看到什么了?对了,两次读取到的Drawable共享了一个BitmapState。其实,所有的ConstantState都是共享的。NinePatchDrawable中的ConstantState,即NinePatchState,也是共享的。
那问题出在哪儿?请注意这个名字ConstantState。实际上,Drawable的设计者希望,能够共享给所有Drawable使用的state都必须是Constant的。但是,不幸的是BitmapDrawable的设计者却不打算遵循这个规范。请回头看看本文开始的地方写的BitmapDrawable的alpha是通过BitmapState的Paint来实现的。而BitmapState是共享的。也就是说同一个BitmapDrawable的alpha值是共享的!!而NinePatchDrawable就没有这个问题,其alpha设置并不共享。
会造成什么问题?
假设你有两个一样的Button,使用了BitmapDrawable作为背景,当你对某一个Button做alpha动画的时候,实际上另外一个Button的alpha值也在变化!!!但是因为另外一个Button没有重绘,所以你看不到,如果这个时候某一个事件导致界面重绘,比如锁屏再解锁,就会发现另外一个Button的alpha值也变化了。
所以,请慎用BitmapDrawable,最好不要用他来做alpha动画。当然,也不仅是alpha值才有这个问题,所有BitmapState中共享的值都会存在这个问题。请看:
Bitmap mBitmap; int mChangingConfigurations; int mGravity = Gravity.FILL; Paint mPaint = new Paint(DEFAULT_PAINT_FLAGS); Shader.TileMode mTileModeX = null; Shader.TileMode mTileModeY = null; int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT; boolean mRebuildShader;