一行代码让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 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) {

    }
  }
}

基于GlideRefreshableDrawable实现

  • 继承上篇文章中的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

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 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动图显示

你可能感兴趣的:(一行代码让TextView中ImageSpan支持Gif(四)----drawable复用,减少内存消耗,支持RecylerView/ListView等场景)