前言
前面几篇文章介绍了从生成一个GifDrawable到和TextView绑定实现刷新的过程,但是有个比较致命的问题,就是一个GifDrawable只能刷新一个TextView
这样会造成在RecyclerView/ListView这样TextView会复用的场景,要想正常的显示gif图片,就得在onBindView/getView方法中每次都重新创建新的GifDrawable,这样的内存消耗是无法接受的
下面会介绍让GifDrawable支持刷新多个TextView,并且引入缓存机制,减少内存占用
ScreenShot
一个GifDrawable刷新多个TextView
之前的方法是Drawable.Callback
去持有TextView,并在drawable刷新时去刷新TextView
Drawable.Callback
在drawable中是弱引用,为了不让它被回收,又作为Tag被TextView所持有了,这是一个一对一的关系
如果要实现一个drawable刷新多个TextView,可以有两个选择
- drawable持有多个Drawable.Callback,每次刷新自己时调用所有的Drawable.Callback的刷新
- drawable持有多个TextView,每次刷新自己时调用所有的TextView的刷新
最终我选择了在drawable持有Drawable.Callback
而不是TextView,因为callback中可以不仅仅去刷新TextView也可以刷新ImageView或者进行其他操作,持有Drawable.Callback
可以保证灵活性
实现代码
首先先要实现我定义的接口
public interface RefreshableDrawable {
//是否可以刷新
boolean canRefresh();
//获取刷新间隔
//对于包含多个drawable的TextView只取刷新间隔最小的drawable作为刷新的调用者
//可以简单的在实现中写死一个合适的值
int getInterval();
//向drawable中添加需要被调用的Drawable.Callback
void addCallback(Drawable.Callback callback);
//移除Drawable.Callback
void removeCallback(Drawable.Callback callback);
}
同样提供了一个静态方法GifTextUtil.setTextWithReuseDrawable
- 调用旧的内容中的
RefreshableDrawable
的removeCallback()
移除当前drawable中的Callback - needAllCallback表示是否需要对所有的drawable都addCallback,对于EditText建议needAllCallback=true,因为如果needAllCallback=false,删除了刷新间隔最小的drawable,那这个EditText的刷新都不会再被调用了
- needAllCallback=false取刷新间隔最小的一个
RefreshableDrawable
调用addCallback()
public static void setTextWithReuseDrawable(final TextView textView, final CharSequence nText,
boolean needAllCallback, final BufferType type) {
CharSequence oldText = "";
try {
//EditText第一次获取到的是个空字符串,会强转成Editable,出现ClassCastException
oldText = textView.getText();
} catch (ClassCastException e) {
e.fillInStackTrace();
}
Object cachedCallback = textView
.getTag(R.id.drawable_callback_tag);
CallbackForTextView callback;
if (cachedCallback != null && cachedCallback instanceof CallbackForTextView) {
callback = (CallbackForTextView) cachedCallback;
if (oldText instanceof Spannable) {
ImageSpan[] gifSpans = ((Spannable) oldText).getSpans(0, oldText.length(), ImageSpan.class);
for (ImageSpan gifSpan : gifSpans) {
Drawable drawable = gifSpan.getDrawable();
if (drawable != null && drawable instanceof RefreshableDrawable) {
((RefreshableDrawable) drawable).removeCallback(callback);
}
}
}
} else {
callback = new CallbackForTextView(textView);
textView.setTag(R.id.drawable_callback_tag, callback);
}
//type 默认SPANNABLE,保证textView中取出来的是Spannable类型
textView.setText(nText, type);
CharSequence text = textView.getText();
if (text instanceof Spannable) {
RefreshableDrawable temp = null;
int tempInterval = 0;
ImageSpan[] imageSpans = ((Spannable) text).getSpans(0, text.length(), ImageSpan.class);
if (needAllCallback) {
int refreshDrawableCount=0;
for (ImageSpan gifSpan : imageSpans) {
Drawable drawable = gifSpan.getDrawable();
if (drawable != null) {
if (drawable instanceof RefreshableDrawable) {
((RefreshableDrawable) drawable).addCallback(callback);
refreshDrawableCount++;
} else {
drawable.setCallback(callback);
}
}
}
callback.setNeedInterval(refreshDrawableCount > 5);
} else {
for (ImageSpan gifSpan : imageSpans) {
Drawable drawable = gifSpan.getDrawable();
if (drawable != null && drawable instanceof RefreshableDrawable) {
if (((RefreshableDrawable) drawable).canRefresh()) {
if (tempInterval == 0 || tempInterval > ((RefreshableDrawable) drawable)
.getInterval()) {
temp = (RefreshableDrawable) drawable;
tempInterval = temp.getInterval();
}
}
}
}
if (temp != null) {
temp.addCallback(callback);
}
}
//gifSpanWatcher是SpanWatcher,继承自NoCopySpan
//只有setText之后设置SpanWatcher才能成功
GifSpanWatcher cacheSpanWatcher;
Object object = textView.getTag(R.id.span_watcher_tag);
if (object != null && object instanceof GifSpanWatcher) {
cacheSpanWatcher = (GifSpanWatcher) object;
} else {
cacheSpanWatcher = new GifSpanWatcher(callback);
textView.setTag(R.id.span_watcher_tag, cacheSpanWatcher);
}
((Spannable) text).setSpan(cacheSpanWatcher, 0, text.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE | Spanned.SPAN_PRIORITY);
}
textView.invalidate();
}
注意
- 因为添加Callback和移除Callback都是在这一个方法中调用的,所以要用
setTextWithReuseDrawable()
完全代替TextView.setText()
,才能最大程度的提升性能 - 对同一个TextView不能混用
GifTextUtil.setTextWithReuseDrawable()
和GifTextUtil.setText()
- 所有需要显示gif的drawable都需要实现
RefreshableDrawable
接口,而且刷新TextView操作是需要自己处理的 - 被drawable持有的Drawable.Callback和设置给drawable自身的Drawable.Callback不是同一个东西,注意区分
基于android-gif-drawable
的RefreshableDrawable
实现
- 继承
GifDrawable
- 用
getDuration() /getNumberOfFrames()
计算刷新间隔 - 用一个WeakHashMap保存Callback实例作为key,被添加的次数作为value
- 自身作为
Drawable.Callback
public class RefreshGifDrawable extends GifDrawable implements RefreshableDrawable,
Drawable.Callback {
private CallBack callBack = new CallBack();
private WeakHashMap callbackWeakHashMap;
...
@Override
public boolean canRefresh() {
return true;
}
@Override
public int getInterval() {
return getDuration() / getNumberOfFrames();
}
@Override
public void addCallback(Drawable.Callback currentCallback) {
if (callbackWeakHashMap == null) {
callbackWeakHashMap = new WeakHashMap<>();
setCallback(callBack);
}
if (!containsCallback(currentCallback)) {
//一个新的Callback被添加
callbackWeakHashMap.put(currentCallback, 1);
} else {
//一个已有的Callback被添加,将count加1
int count = callbackWeakHashMap.get(currentCallback);
callbackWeakHashMap.put(currentCallback, ++count);
}
}
@Override
public void removeCallback(Callback currentCallback) {
if (callbackWeakHashMap == null) {
return;
}
if (containsCallback(currentCallback)) {
int count = callbackWeakHashMap.get(currentCallback);
if (count <= 1) {
//count小于等于1表示只有一个drawable出现在TextView中
//可以直接移除当前Callback
callbackWeakHashMap.remove(currentCallback);
} else {
//count大于1表示有多个drawable出现在TextView中
//删除一个还有其他的,所以不能移除当前Callback而是将count减1
callbackWeakHashMap.put(currentCallback, --count);
}
}
}
private boolean containsCallback(Callback currentCallback) {
return callbackWeakHashMap != null && callbackWeakHashMap.containsKey(currentCallback);
}
@Override
public void invalidateDrawable(@NonNull Drawable who) {
if (callbackWeakHashMap != null) {
Set set = callbackWeakHashMap.keySet();
for (Callback callback : set) {
callback.invalidateDrawable(who);
}
}
}
@Override
public void invalidateSelf() {
super.invalidateSelf();
Callback callback = getCallback();
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
}
class CallBack implements Callback {
@Override
public void invalidateDrawable(@NonNull Drawable who) {
if (callbackWeakHashMap != null) {
Set set = callbackWeakHashMap.keySet();
for (Callback callback : set) {
callback.invalidateDrawable(who);
}
}
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
}
}
}
基于Glide
的RefreshableDrawable
实现
- 继承上篇文章中的
GlidePreDrawable
- 刷新间隔写死了是60
-
Drawable.CallBack
是一个内部类,持有了其实例的强引用
public class GlideReusePreDrawable extends GlidePreDrawable implements RefreshableDrawable,
Measurable {
private WeakHashMap callbackWeakHashMap;
private CallBack callBack = new CallBack();
@Override
public boolean canRefresh() {
return true;
}
@Override
public int getInterval() {
return 60;
}
@Override
public void addCallback(Drawable.Callback currentCallback) {
if (callbackWeakHashMap == null) {
callbackWeakHashMap = new WeakHashMap<>();
//Glide的GifDrawable的findCallback会一直去找不为Drawable的Callback
// 所以不能直接implements Drawable.Callback
setCallback(callBack);
}
if (!containsCallback(currentCallback)) {
callbackWeakHashMap.put(currentCallback, 1);
} else {
int count = callbackWeakHashMap.get(currentCallback);
callbackWeakHashMap.put(currentCallback, ++count);
}
}
@Override
public void removeCallback(Callback currentCallback) {
if (callbackWeakHashMap == null) {
return;
}
if (containsCallback(currentCallback)) {
int count = callbackWeakHashMap.get(currentCallback);
if (count <= 1) {
callbackWeakHashMap.remove(currentCallback);
} else {
callbackWeakHashMap.put(currentCallback, --count);
}
}
}
private boolean containsCallback(Callback currentCallback) {
return callbackWeakHashMap != null && callbackWeakHashMap.containsKey(currentCallback);
}
class CallBack implements Callback {
@Override
public void invalidateDrawable(@NonNull Drawable who) {
if (callbackWeakHashMap != null) {
Set set = callbackWeakHashMap.keySet();
for (Callback callback : set) {
callback.invalidateDrawable(who);
}
}
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
}
}
}
特殊说明下为什么Drawable.Callback
要用一个内部类而不像上面直接把自己作为Callback
Glide
的GifDrawable
中findCallback()
方法会在onFrameReady()
时调用,findCallback()
会一直循环去找第一个不是Drawable的Callback实例,如果将GlideReusePreDrawable
自身作为Callback,那么会造成这段代码死循环
private Callback findCallback() {
Callback callback = getCallback();
while (callback instanceof Drawable) {
callback = ((Drawable) callback).getCallback();
}
return callback;
}
Drawable的缓存
这里我写了一个单例,在其中用一个HashMap去缓存已经创建的Drawable,比较简单,不再赘述
HashMap
总结
至此本系列的所有细节都介绍完毕了,希望对大家实现自己的功能有所帮助,祝各位新年快乐
完整代码
项目地址https://github.com/sunhapper/SpEditTool
欢迎star,提PR、issue
索引
一行代码让TextView中ImageSpan支持Gif(一)
第一篇给出解决方案并分析整体思路
一行代码让TextView中ImageSpan支持Gif(二)
第二篇对实现中的细节和踩过的坑进行说明
一行代码让TextView中ImageSpan支持Gif(三)
第三篇介绍如何使用android-gif-drawable和Glide实现远程gif图片在TextView中的图文混排
一行代码让TextView中ImageSpan支持Gif(四)
第四篇介绍在RecyclerView等需要drawable复用的场景下的gif动图显示