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

前言

前面几篇文章介绍了从生成一个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,可以有两个选择

  • 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

  • 调用旧的内容中的RefreshableDrawableremoveCallback()移除当前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-drawableRefreshableDrawable实现

  • 继承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<Callback, Integer> 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<Callback> 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<Callback> 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) {

    }
  }
}

基于GlideRefreshableDrawable实现

  • 继承上篇文章中的GlidePreDrawable
  • 刷新间隔写死了是60
  • Drawable.CallBack是一个内部类,持有了其实例的强引用
public class GlideReusePreDrawable extends GlidePreDrawable implements RefreshableDrawable,
    Measurable {

  private WeakHashMap<Callback, Integer> 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<Callback> 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

GlideGifDrawablefindCallback()方法会在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<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动图显示

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容