前言
上一篇文章原来Span可以这样加载网络图(上)讲了Span加载网络图的方法。为了简化使用和拓展,接下来尝试封装一下。
先来看看之前的做法
val ss = SpannableStringBuilder("<img>To be or not to be, that is the question(生存还是毁灭,这是一个值得考虑的问题)")
val placeholderSpan = ImageSpan(context, R.mipmap.placeholder)
ss.setSpan(placeholderSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.setText(ss, TextView.BufferType.SPANNABLE) // 必需设置
val spannable = textView.text as? Spannable ?: return
Glide.with(textView)
.load("https://sf6-ttcdn-tos.pstatp.com/img/user-avatar/d8111dfb52a63f3f12739194cf367754~100x100.png")
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
val start = spannable.getSpanStart(placeholderSpan)
val end = spannable.getSpanEnd(placeholderSpan)
if (start != -1 && end != -1) {// 替换Span
resource.setBounds(0, 0, resource.intrinsicWidth, resource.intrinsicHeight)
spannable.removeSpan(placeholderSpan)
spannable.setSpan(ImageSpan(resource), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
override fun onLoadCleared(placeholder: Drawable?) {}
})
复制代码
先是设置占位ImageSpan,然后调用了加载图片的方法,图片加载后用新的ImageSpan替换占位ImageSpan。
对于使用者来说,这太过于繁琐了。最好能做到只创建一个Span,设置几个参数就能自己加载图片。使用起来大概是这样
val ss = SpannableStringBuilder("<img>To be or not to be, that is the question(生存还是毁灭,这是一个值得考虑的问题)")
val urlImageSpan = new ImageSpanBuilder()
...
.build()
ss.setSpan(urlImageSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.setText(ss, TextView.BufferType.SPANNABLE) // 必需设置
复制代码
设置占位ImageSpan,这一步是必不可少的。那么如何将加载图片还有替换占位ImageSpan的步骤”藏”起来?这就需要看下ImageSpan的源码了。
ImageSpan继承自DynamicDrawableSpan,实现了getDrawable()方法。
// ImageSpan,API-30
public Drawable getDrawable() {
Drawable drawable = null;
if (mDrawable != null) {
drawable = mDrawable;
} else if (mContentUri != null) {
Bitmap bitmap = null;
try {
InputStream is = mContext.getContentResolver().openInputStream(
mContentUri);
bitmap = BitmapFactory.decodeStream(is);
drawable = new BitmapDrawable(mContext.getResources(), bitmap);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight());
is.close();
} catch (Exception e) {
Log.e("ImageSpan", "Failed to loaded content " + mContentUri, e);
}
} else {
try {
drawable = mContext.getDrawable(mResourceId);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight());
} catch (Exception e) {
Log.e("ImageSpan", "Unable to find resource: " + mResourceId);
}
}
return drawable;
}
复制代码
看了源码后我们知道,该方法返回了需要展示的Drawable。那么我们可以仿照ImageSpan,自定义一个DynamicDrawableSpan,在getDrawable()方法返回指定的占位图,并且在这个方法内调用加载图片的逻辑。这样就可以简化使用了。
封装
理论上没什么问题了,开始实践。先来明确下需求
- 可替换图片加载框架
- 可设置占位图、错误图、图像大小等
- 使用简单
首先我们定义一个请求类,用来记录占位图、错误图、图像大小,占位Span等信息。
class URLImageSpanRequest(
textView: TextView,
val url: String?,
val placeholderDrawable: Drawable?,
val errorPlaceholder: Drawable?,
val desiredWidth: Int,
val desiredHeight: Int,
val verticalAlignment: Int
) {
private val viewRef = WeakReference(textView)
val view get():TextView? = viewRef.get()
var span: Any? = null
}
复制代码
这里对textView使用弱引用,避免内存泄漏。然后定义一个图片加载接口,用来处理这个请求。
interface DrawableProvider {
fun get(request: URLImageSpanRequest): Drawable
}
复制代码
这里我用了Glide来加载图片。
class GlideDrawableProvider : DrawableProvider {
override fun get(request: URLImageSpanRequest): Drawable {
val drawable = if (request.url.isNullOrEmpty()) {
request.placeholderDrawable ?: request.errorPlaceholder
} else {
execute(request)
request.placeholderDrawable
}
return drawable ?: ColorDrawable()/*Can't be null*/
}
fun execute(request: URLImageSpanRequest) {
val view = request.view ?: return
val span = request.span
Glide.with(view)
.load(request.url)
.error(request.errorPlaceholder)
.override(request.desiredWidth, request.desiredHeight)
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
resource.setBounds(0, 0, resource.intrinsicWidth, resource.intrinsicHeight)
onResponse(request, resource)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
if (errorDrawable != null) {
onResponse(request, errorDrawable)
}
}
override fun onLoadCleared(placeholder: Drawable?) {
}
private fun onResponse(request: URLImageSpanRequest, drawable: Drawable) {
val spannable = request.view?.text as? Spannable ?: return
spannable.replaceSpan(
span,
ImageSpan(drawable, request.verticalAlignment),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
})
}
fun Spannable.replaceSpan(oldSpan: Any?, newSpan: Any?, flags: Int): Boolean {
if (oldSpan == null || newSpan == null) {
return false
}
val start = getSpanStart(oldSpan)
val end = getSpanEnd(oldSpan)
if (start == -1 || end == -1) {
return false
}
removeSpan(oldSpan)
setSpan(newSpan, start, end, flags)
return true
}
}
复制代码
如果url是空的话就没必要开启图片加载了。get()方法的返回值是占位图。图片加载后替换占位Span。
最后用Builder模式将上面的内容组合一下
class URLImageSpan {
open class Builder(private val provider: DrawableProvider = GlideDrawableProvider()) {
private var url: String? = null
private var placeholderDrawable: Drawable? = null
private var placeholderId = 0
private var useInstinctPlaceholderSize = true
private var errorPlaceholder: Drawable? = null
private var errorId = 0
private var useInstinctErrorPlaceholderSize = true
private var verticalAlignment = DynamicDrawableSpan.ALIGN_BOTTOM
private var desiredWidth = -1
private var desiredHeight = -1
fun override(width: Int, height: Int): Builder {
this.desiredWidth = width
this.desiredHeight = height
return this
}
fun url(url: String?): Builder {
this.url = url
return this
}
fun placeholder(drawable: Drawable?): Builder {
this.placeholderDrawable = drawable
this.placeholderId = 0
this.useInstinctPlaceholderSize = true
return this
}
fun error(drawable: Drawable?): Builder {
this.errorPlaceholder = drawable
this.errorId = 0
this.useInstinctErrorPlaceholderSize = true
return this
}
...
fun buildRequest(textView: TextView): URLImageSpanRequest {
val context = textView.context
return URLImageSpanRequest(
textView = textView,
url = url,
placeholderDrawable = getPlaceholderDrawable(context),
errorPlaceholder = getErrorDrawable(context),
verticalAlignment = verticalAlignment,
desiredWidth = desiredWidth,
desiredHeight = desiredHeight
)
}
fun build(textView: TextView): DynamicDrawableSpan {
val request = buildRequest(textView)
return object : DynamicDrawableSpan() {
override fun getDrawable(): Drawable {
request.span = this
return provider.get(request)
}
}
}
}
}
复制代码
注意记得将占位span赋值。见上面的build()方法
封装后的使用方式
val ss =
SpannableString("<img>To be or not to be, that is the question(生存还是毁灭,这是一个值得考虑的问题)")
val urlImageSpan = URLImageSpan.Builder()
.url("https://sf6-ttcdn-tos.pstatp.com/img/user-avatar/d8111dfb52a63f3f12739194cf367754~500x500.png")
.override(100.dp, 100.dp)
.build(textView)
ss.setSpan(urlImageSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.setText(ss, TextView.BufferType.SPANNABLE) // 必需设置
复制代码
与之前的用法对比,明显简单了不少
val ss = SpannableStringBuilder("<img>To be or not to be, that is the question(生存还是毁灭,这是一个值得考虑的问题)")
val placeholderSpan = ImageSpan(context, R.mipmap.placeholder)
ss.setSpan(placeholderSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.setText(ss, TextView.BufferType.SPANNABLE) // 必需设置
val spannable = textView.text as? Spannable ?: return
Glide.with(textView)
.load("https://sf6-ttcdn-tos.pstatp.com/img/user-avatar/d8111dfb52a63f3f12739194cf367754~100x100.png")
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
val start = spannable.getSpanStart(placeholderSpan)
val end = spannable.getSpanEnd(placeholderSpan)
if (start != -1 && end != -1) {// 替换Span
resource.setBounds(0, 0, resource.intrinsicWidth, resource.intrinsicHeight)
spannable.removeSpan(placeholderSpan)
spannable.setSpan(ImageSpan(resource), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
override fun onLoadCleared(placeholder: Drawable?) {}
})
复制代码
总结
Span加载网络图的原理很简单,但是使用起来并不简单。在原来做法的基础上抽象封装了一下,简化使用,方便以后的拓展和修改。
最后附上Github链接