现在的位置: 首页 > 综合 > 正文

BitmapDrawable设置alpha时的一个BUG

2019年07月29日 ⁄ 综合 ⁄ 共 5540字 ⁄ 字号 评论关闭

最近太忙,所以很久没有写新文章了。这次我们来讨论一下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;

抱歉!评论已关闭.