一行代码让TextView中ImageSpan支持Gif(四)----drawable复用,减少内存消耗,支持RecylerView/ListView等场景

2018-02-11 14:13:41来源:https://www.jianshu.com/p/c10042269f6f作者:sunhapper人点击

分享


前言

前面几篇文章介绍了从生成一个GifDrawable到和TextView绑定实现刷新的过程,但是有个比较致命的问题,就是一个GifDrawable只能刷新一个TextView


这样会造成在RecyclerView/ListView这样TextView会复用的场景,要想正常的显示gif图片,就得在onBindView/getView方法中每次都重新创建新的GifDrawable,这样的内存消耗是无法接受的


下面会介绍让GifDrawable支持刷新多个TextView,并且引入缓存机制,减少内存占用


ScreenShot


gifRecyclerSp.gif
一个GifDrawable刷新多个TextView
之前的局限

之前的方法是Drawable.Callback去持有TextView,并在drawable刷新时去刷新TextView


Drawable.Callback在drawable中是弱引用,为了不让它被回收,又作为Tag被TextView所持有了,这是一个一对一的关系


按照这样的方式如果希望一个drawable可以刷新多个TextView,那么它的Callback需要持有多个TextView,但是如何保证Drawable.Callback不被回收呢?将Drawable.Callback作为Tag放到多个TextView中?


需要注意,一个TextView中可能会有多个drawable,这样就变成了一个多对多的关系了,这样维护起来就很困难了,也没有必要,因为只有Drawable.Callback需要刷新TextView,TextView本身并不会对Callback做什么操作


经过上面的分析,要解决的问题就渐渐清晰


我们需要一个Drawable.Callback刷新多个TextView这样一对多的关系
保证Drawable.Callback不会被回收掉
改进思路

将drawable作为Drawable.CallbackTextView交互的中间人


drawable不仅持有Drawable.Callback的弱引用还用成员变量持有一个它的强引用,甚至可以实现Drawable.Callback将drawable自身作为一个Callback


此外drawable用一个数据结构保存所有包含这个drawable的TextView,在Drawable.Callback回调被调用时刷新所有TextView


如此一对多刷新且Callback不会被回收


实现代码

首先先要实现我定义的接口


public interface RefreshableDrawable {
//是否可以刷新
boolean canRefresh();
//获取刷新间隔
//对于包含多个drawable的TextView只取刷新间隔最小的drawable作为刷新的调用者
//可以简单的在实现中写死一个合适的值
int getInterval();
//向drawable中添加需要被刷新的textview
void addHost(TextView tv);
//移除textview,这个在某个textview被设置了新的内容时对旧的drawable调用
void removeHost(TextView tv);
}

同样提供了一个静态方法GifTextUtil.setTextWithReuseDrawable


调用旧的内容中的RefreshableDrawableremoveHost()移除当前的TextView
取刷新间隔最小的一个RefreshableDrawable调用addHost()

public static void setTextWithReuseDrawable(final TextView textView, final CharSequence nText,
final BufferType type) {
CharSequence oldText = "";
try {
//EditText第一次获取到的是个空字符串,会强转成Editable,出现ClassCastException
oldText = textView.getText();
} catch (ClassCastException e) {
e.fillInStackTrace();
}
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).removeHost(textView);
}
}
}
textView.setText(nText, type);
CharSequence text = textView.getText();
if (text instanceof Spannable) {
RefreshableDrawable temp = null;
int tempInterval = 0;
ImageSpan[] gifSpans = ((Spannable) text).getSpans(0, text.length(), ImageSpan.class);
for (ImageSpan gifSpan : gifSpans) {
Drawable drawable = gifSpan.getDrawable();
if (drawable != null && drawable instanceof RefreshableDrawable) {
if (((RefreshableDrawable) drawable).canRefresh()) {
if (tempInterval < ((RefreshableDrawable) drawable).getInterval()) {
temp = (RefreshableDrawable) drawable;
}
}
}
}
if (temp != null) {
temp.addHost(textView);
}
}
textView.invalidate();
}

注意
因为添加TextView和移除TextView都是在这一个方法中调用的,所以要用setTextWithReuseDrawable()完全代替TextView.setText(),才能最大程度的提升性能
对同一个TextView不能混用GifTextUtil.setTextWithReuseDrawable()GifTextUtil.setText()
所有需要显示gif的drawable都需要实现RefreshableDrawable接口,而且刷新TextView操作是需要自己处理的
基于android-gif-drawableRefreshableDrawable实现
继承GifDrawable
getDuration() /getNumberOfFrames()计算刷新间隔
用一个List保存TextView实例
自己作为Drawable.Callback

public class RefreshGifDrawable extends GifDrawable implements RefreshableDrawable,Drawable.Callback {
private List< TextView> hosts;
private static final String TAG = "RefreshGifDrawable";
public RefreshGifDrawable(@NonNull Resources res,
int id) throws NotFoundException, IOException {
super(res, id);
}
@Override
public boolean canRefresh() {
return true;
}
@Override
public int getInterval() {
return getDuration() /getNumberOfFrames();
}
@Override
public void addHost(TextView tv) {
if (hosts==null){
hosts=new ArrayList<>();
setCallback(this);
}
if (!hosts.contains(tv)){
hosts.add(tv);
}
}
@Override
public void removeHost(TextView tv) {
if (hosts!=null&&hosts.contains(tv)){
hosts.remove(tv);
}
}
@Override
public void invalidateDrawable(@NonNull Drawable who) {
if (hosts!=null){
for (TextView tv : hosts) {
tv.invalidate();
}
}
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
}
}

基于GlideRefreshableDrawable实现
继承上篇文章中的GlidePreDrawable
刷新间隔写死了是60
Drawable.CallBack是一个内部类,持有了其实例的强引用
public class GlideReusePreDrawable extends GlidePreDrawable implements RefreshableDrawable,
Measurable {
private List<TextView> hosts;
private CallBack callBack = new CallBack();

@Override
public boolean canRefresh() {
return true;
}
@Override
public int getInterval() {
return 60;
}
@Override
public void addHost(TextView tv) {
if (hosts == null) {
hosts = new ArrayList<>();
//Glide的GifDrawable的findCallback会一直去找不为Drawable的Callback
// 所以不能直接implements Drawable.Callback
setCallback(callBack);
}
if (!hosts.contains(tv)) {
hosts.add(tv);
}
}
@Override
public void removeHost(TextView tv) {
if (hosts != null && hosts.contains(tv)) {
hosts.remove(tv);
}
}
class CallBack implements Callback {
@Override
public void invalidateDrawable(@NonNull Drawable who) {
if (hosts != null) {
for (TextView tv : hosts) {
tv.invalidate();
}
}
}
@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


GlideGifDrawablefindCallback()方法会在onFrameReady()时调用,发findCallback()会一直循环去找第一个不是Drawable的Callback实例,如果将RefreshGifDrawable自身作为Callback,那么会造成这段代码死循环


  private Callback findCallback() {
Callback callback = getCallback();
while (callback instanceof Drawable) {
callback = ((Drawable) callback).getCallback();
}
return callback;
}

Drawable的缓存

这里我写了一个单例,在其中用一个HashMap去缓存已经创建的Drawable,比较简单,不再赘述


HashMap<Object, Drawable> drawableCacheMap = new 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动图显示








最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台