Android LayerDrawable 和 Drawable.Callback

1955次阅读  |  发布于5年以前

Android LayerDrawable 和 Drawable.Callback

LayerDrawable是一个特殊的Drawable,它内部保持着一个Drawable数组,其中每一个Drawable都是视图中的一层。如果你不了解LayerDrawable的机制,当程序出了问题后是很难去找到bug在哪里的。我发这些文章就是为了分享在使用LayerDrawableDrawable.Callback时可能出现的一个bug。

Callback调用链

LayerDrawable中,每层视图(Drawable)都会将LayerDrawable注册为它的Drawable.Callback。这允许Drawable能够在需要重绘自己的时候告知LayerDrawable重绘它。我们可以在下面这个Callback.invalidateSelf()函数中看到是由注册callback端(在此处为LayerDrawable)来执行invalidateDrawable(Drawable drawable)的。

public void invalidateSelf() {
    /* 获取注册的Callback实例,如果无则返回null。 */
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

我们知道View是实现了Drawable.Callback接口的,所以当图片需要重绘的时候就能够告知View。如果我们把View的背景图片设置成了LayerDrawable,在Drawable需要更新的时候callback的调用将有一个传递的过程,首先会调用注册的LayerDrawableinvalidateDrawable(Drawable drawable)方法,LayerDrawable又会调用ViewinvalidateDrawable(Drawable drawable)方法。如下图所示:

Alt text

View改变背景时移除原背景Callback

ViewsetBackgroundDrawable(Drawable background)中有这么一段代码:

    if (mBackground != null) {
        mBackground.setCallback(null);
        unscheduleDrawable(mBackground);
    }

    …
    if (background != null) {
        background.setCallback(this);
    }

我们可以看出:当View改变背景时将会无条件将原背景(如果原背景是Drawable的话)的Drawable.Callback设置为null

什么情况下会出现Bug?

有了上面这些知识,我们可以通过下面这个步骤产生一个Bug:

  1. DrawableA 设置成ViewV的背景。现在A的callback指向V
  2. 将A设置成LayerDrawable L中的一层。现在A的callback指向L
  3. 现在为V设置另一个背景,V会把原背景(A)的callback强制设置成null,破坏了A与L之间的联系。
  4. BUG出现了:更新DrawableA不会让L更新了。

解决方法就是在更新V的背景之后再创造LayerDrawableL。Bug发生与解决的例子可以在这里下载。

为了方便看官,我也贴了一部分关键代码到这边来,你可以通过注释理解这段代码。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button btn1 = (Button) findViewById(R.id.button1);
    btn1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // 1. 将 launcherIconDrawable.callback 赋值给 actionBar
            actionBar.setBackgroundDrawable(launcherIconDrawable);
            animateActionBarWorking();
        }
    });
    Button btn2 = (Button) findViewById(R.id.button2);
    btn2.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // 1. 将 launcherIconDrawable.callback 赋值给 actionBar
            actionBar.setBackgroundDrawable(launcherIconDrawable);
            animateActionBarNotWorking();
        }
    });
    actionBar = getSupportActionBar();
    launcherIconDrawable = getResources().getDrawable(R.drawable.launcher_repeat);
    colorLayer = new ColorDrawable(Color.rgb(0, 255, 0));
    actionBar.setBackgroundDrawable(colorLayer);
}

/* 这个函数运行后ActionBar不会得到更新。 */
private void animateActionBarNotWorking() {
    Drawable[] layers = new Drawable[] { colorLayer, launcherIconDrawable };
    LayerDrawable layerDrawable = new LayerDrawable(layers);
    actionBar.setBackgroundDrawable(layerDrawable);
    ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 255);
    valueAnimator.setDuration(1000);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            // 4. Updates launcherIconDrawable will not trigger action bar background to update
            // as launcherIconDrawable.callback is null
            launcherIconDrawable.setAlpha((Integer) animation.getAnimatedValue());
        }
    });
    valueAnimator.start();
}

/* 由于先移除了launcherIconDrawable与ActionBar的联系,这个函数运行后会让ActionBar得到更新。
private void animateActionBarWorking() {
    actionBar.setBackgroundDrawable(null);
    animateActionBarNotWorking();
}

参考文章:

LayerDrawable - Android Developers

Copyright© 2013-2019

京ICP备2023019179号-2