Glide 不同请求间的过渡效果

2,753 阅读3分钟

Glide 不同请求间的过渡效果

Base On Gradle 4.11.0

Glide 中允许我们通过 Transitions 来设置占位图,缩略图过度到请求的图片的动画效果。

DrawableCrossFadeFactory factory =
        new DrawableCrossFadeFactory.Builder().build();

GlideApp.with(context)
        .load(url)
        .transition(withCrossFade(factory))
        .placeholder(R.color.placeholder)
        .into(imageView);

加载的图片透明度逐渐增加,覆盖 placeholder。

但是在不同的请求之间,每次都是变成 placehodler(或透明后)再渐变显示新的图片。

为什么不同请求间 Transition 不能平滑过渡

想要解决这个问题,我们需要先搞清楚,为什么不同请求间,Transition 不能平滑过渡。

首先,我们去看看 Transition 内部的动画怎么实现的

DrawableCrossFadeTransition 为例, BitmapCrossFadeTransition 内部也是调用 DrawableCrossFadeTransition 实现的,可以自行去查看一下

  @Override
  public boolean transition(Drawable current, ViewAdapter adapter) {
    Drawable previous = adapter.getCurrentDrawable();
    if (previous == null) {
      previous = new ColorDrawable(Color.TRANSPARENT);
    }
    TransitionDrawable transitionDrawable =
        new TransitionDrawable(new Drawable[] {previous, current});
    transitionDrawable.setCrossFadeEnabled(isCrossFadeEnabled);
    transitionDrawable.startTransition(duration);
    adapter.setDrawable(transitionDrawable);
    return true;
  }

可以发现,Glide 的切换动画其实就是利用了 TransitionDrawable

那么,这里的 previouscurrent 就是重点了,我们 debug 一下,看看他们的值。

debug 后,我们发现:

  1. 当初次加载图片的时候,previousplaceholder(如果设置了)
  2. 而当切换 url 后,adapter.getCurrentDrawable() 则为空了

所以,每次切换请求,图片都是从透明渐变到请求的图片。

为什么 adapter.getCurrentDrawable() 会为空呢

debug 的时候可以发现,这里的 ViewAdapter 实际上就是我们的 DrawableImageViewTarget ,继承自 ImageViewTraget

代码有点长,就不放出来了,自行查看

其中 getCurrentDrawable() 方法则是直接返回 ImageView.getDrawable()。那么,我们就看看哪里设置了 Drawable. 我们重点关注下面几个方法。

/*
    加载前调用,设置 placeholder
*/
  @Override
  public void onLoadStarted(@Nullable Drawable placeholder) {
    super.onLoadStarted(placeholder);
    setResourceInternal(null);
    setDrawable(placeholder);
  }

/*
    切换请求和推出时调用
    1. 停止动画
    2. 设置 placeholder
*/
  @Override
  public void onLoadCleared(@Nullable Drawable placeholder) {
    super.onLoadCleared(placeholder);
    if (animatable != null) {
      animatable.stop();
    }
    setResourceInternal(null);
    setDrawable(placeholder);
  }

  @Override
  public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {
    //加载成功后,切换显示图片,播放动画
    if (transition == null || !transition.transition(resource, this)) {
      setResourceInternal(resource);
    } else {
      maybeUpdateAnimatable(resource);
    }
  }

  private void setResourceInternal(@Nullable Z resource) {
    setResource(resource);
    maybeUpdateAnimatable(resource);
  }

Code from ImageViewTraget

我们可以发现,问题就出在 onLoadCleared 方法上,每次切换请求的时候都重新设置了 placeholder。 如果没有设置 placeholder,结合上述 Transition 的代码,可以知道就会从透明过渡到下一个图片。

自定义 Target

既然知道问题所在,就来重写一下 Target,去掉原本 ImaegViewTargetonLadCleared 中的方法吧。

重写,运行后应该会出现 Execption:java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@xxxxxx

这是因为 Glide 在 onLoadCleared 的时候,把 Bitmap 回收了,所以显示下一图片时,TransitionDrawable 就会抛出这个问题,想要解决这个问题,就需要在 onResourceReady 中,copy 一份 Bitmap 进行显示了,但是这部分的 Bitmap, Glide 就不能帮你回收了,要注意一下,这应该也是 Glide 没有默认就在不同请求间显示动画的原因吧。

最终代码:

class TransitionImageTarget(val imageView: ImageView) :
    CustomViewTarget<ImageView, Drawable>(imageView), ViewAdapter {

    private var animatable: Animatable? = null

    override fun onLoadFailed(errorDrawable: Drawable?) {
        setResourceInternal(null)
        setDrawable(errorDrawable)
    }

    override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
        var dst: Drawable = resource
        //避免Glide回收,造成
        //java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@xxxxxx
        if (resource is BitmapDrawable) {
            val bmp = resource.bitmap
            dst = BitmapDrawable(imageView.resources, bmp.copy(bmp.config, true))
        }
        if (transition == null || !transition.transition(dst, this)) {
            setResourceInternal(dst)
        } else {
            maybeUpdateAnimatable(dst)
        }
    }

    override fun onResourceCleared(placeholder: Drawable?) {
        animatable?.stop()
    }

    private fun setResourceInternal(resource: Drawable?) {
        setDrawable(resource)
        maybeUpdateAnimatable(resource)
    }

    private fun maybeUpdateAnimatable(resource: Drawable?) {
        if (resource is Animatable) {
            animatable = resource as Animatable?
            animatable?.start()
        } else {
            animatable = null
        }
    }

    override fun getCurrentDrawable(): Drawable? {
        return imageView.drawable
    }

    override fun setDrawable(drawable: Drawable?) {
        imageView.setImageDrawable(drawable)
    }

}

这时候,你应该就可以看到明显的过渡效果了,但是,当你重新加载缓存过的图片的时候动画消失了,这是因为 DrawableCrossFadeFactory 会判断当图片来自缓存时,使用 NoTransition

  @Override
  public Transition<Drawable> build(DataSource dataSource, boolean isFirstResource) {
    return dataSource == DataSource.MEMORY_CACHE
        ? NoTransition.<Drawable>get()
        : getResourceTransition();
  }

code from DrawableCrossFadeFactory

想要每次切换都显示,可以自定义 TransitionFactory

    private var factory = TransitionFactory<Drawable> { dataSource, isFirstResource ->
        DrawableCrossFadeTransition(1000, false)
    }


    Glide.with(this)
        .load(images[(index++ % images.size)])
        .placeholder(ColorDrawable(Color.GRAY))
        .error(ColorDrawable(Color.DKGRAY))
        .transition(DrawableTransitionOptions.with(factory))
        .into(TransitionImageTarget(binding.ivResult))

最后

  • 因为显示图片 copy 了一份 Bitmap,所以要注意一下内存占用
  • Demo
  • 有更好的方法希望可以分享一下