Evil Mouth's Blog

自定义 Glide Target 实现动态 TextView ImageSpan

April 15, 2021

😄 记录下实现下图效果的过程

1

思考

原先这里只是一个TextView,并不需要显示图片,所以只需要将数据加入/间隔即可,然而现在需要每种属性展示对应的图片,并且该图片还需要支持GIF,本来想换成ImageView+TextView列表来显示,但需求还需要保持原先的一行限制,也就是文字过长时显示...,此时如果弃用单TextView的话还需要计算多个控件的宽度,所以还是继续使用单TextView方案方便,幸好Span可以很轻松的将文字替换成其它形式展示,所以实现起来应该很简单。

设计

整体思路就是下载图片得到Drawable,包装进ImageSpan,并将其添加到TextView的文本中

val text = SpannableString("  红色")
text.set(0..1, ImageSpan(drawable))
textView.text = text

问题

因为要支持GIF,所以下载下来的Drawable如果是Animatable,还需要进行start()调用进行播放,并刷新TextView

start自然要有stop,否则动图将会一直播放。

原先我只是通过Glide来下载图片拿到Drawable,如下代码所示,那我还需要处理好生命周期,在页面关闭或者其它时机关闭动画,虽然现在挺多方式可以处理,但这种方式总是要外部介入,我想提供一个工具来实现这个功能并内部接管好生命周期的处理要如何实现呢?

Glide.with(textView)
    .load(url)
    .asDrawable()
    .submit(SIZE, SIZE)
    .get()

Glide Target

Glide内部帮我们处理了生命周期的回调,自动并及时关闭图片加载请求,查看源码不难发现Glide.into(imageView)最后都是包装成一个TargetTarget实现了LifecycleListener,所以完全可以自定义一个Target,借助Glide内部的生命周期管理来实现该功能。

借鉴GlideImageViewTarget,看到其继承自ViewTarget,但弃用了,推荐使用CustomViewTarget,那就实现一个

class MyTarget : CustomViewTarget<TextView, Drawable>() {
    override fun onResourceReady(resource: Drawable) {
        // setSpan
    }
    override fun onResourceCleared() {
        // removeSpan
    }
    override fun onStart() {
        // animatable.start()
    }
    override fun onStop() {
        // animatable.stop()
    }
}

现在就可以直接像平时Glide.into(imageView)一样直接调用了

Glide.with(textView)
    .load(url)
    .into(MyTarget(textView))

问题又来了

回看效果图,当有多张图片时,自然需求加载多个图片,那就是

urls.forEach(url -> {
    Glide.with(textView)
        .load(url)
        .into(MyTarget(textView))
})

当此时会发现只有最后一张图显示得出来,为什么呢?

2

原来Glide.into时,会将之前的请求给清除掉,而这个请求是绑定在View上的(下面源码),在这里就是绑定在了TextView上。

# CustomViewTarget

@Override
public final void setRequest(@Nullable Request request) {
  setTag(request);
}
private void setTag(@Nullable Object tag) {
  view.setTag(VIEW_TAG_ID, tag);
}

Glide这里的设计是一个View对应一个Request,思考下也合理,一个ImageView也就是为了展示一张图片,但我这里其实是一个TextView对应多个Request,那自然想到重写setRequestgetRequest,但发现CustomViewTarget的这两个方法修饰成了final,那只能跟CustomViewTarget说再见,跟他的爸爸Target说哈喽。

// 将tag变成map,将request和url绑定
class MyTarget(textView: TextView, url: String) : Target<Drawable> {
    override fun setRequest(request: Request?) {
        textView.getTag(VIEW_TAG_ID)?.let {
            it as MutableMap<String, Request?>
            it[attr] = request
        } ?: run {
            textView.setTag(VIEW_TAG_ID, mutableMapOf(url to request))
        }
    }

    override fun getRequest(): Request? {
        textView.getTag(VIEW_TAG_ID)?.let {
            it as MutableMap<String, Request?>
            return it[url]
        }
        return null
    }
}

剩下的就是一些细节处理了,包括拿到Drawable后如何定位到具体位置、Animatable播放如何刷新TextViewImageSpan居中问题、setTextBufferType细节、占位图实现等。

总结

至此效果成功实现,外部只是要一行代码调用即可,不需要担心内存泄漏。

— Evil Mouth