一行代碼讓TextView中ImageSpan支持Gif(三)----利用android-gif-drawable和Glide實(shí)現(xiàn)TextView中g(shù)if的圖文混排

前言

之前兩篇文章介紹了如何讓ImageSpan中的drawable如何去刷新TextView,那么如何創(chuàng)建一個(gè)gif的drawable呢?這篇文章主要介紹利用 android-gif-drawableglide產(chǎn)生GifDrawable的方法

android-gif-drawable

android-gif-drawable使用jni方法去渲染gif圖片,效率不錯(cuò),使用也很簡單,不過不支持遠(yuǎn)程圖片的加載

使用

GifDrawable類的構(gòu)造方法可以接受File,Uri,InputStream,byte[],ResourcesId...等多種類型的參數(shù),直接new就可以創(chuàng)建一個(gè)可以動(dòng)的Drawable

利用GifDrawable實(shí)現(xiàn)TextView支持gif

      GifDrawable gifDrawable = new GifDrawable(getResources(), R.drawable.a);
      Spannable spannable = new SpannableString("[笑臉]");
      ImageSpan imageSpan = new EqualHeightSpan(gifDrawable);
      spannable.setSpan(imageSpan, 0, spannable.length(),Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
      //利用之前介紹的靜態(tài)方法使spEditText中的gif動(dòng)起來
      GifTextUtil.setText(spEditText,spannable);
  • 這里傳入了一個(gè)資源的id,當(dāng)然它是一張gif圖片
  • EqualHeightSpan是自己寫的一個(gè)讓圖片保持和文字等高的ImageSpan
  • 最后使用GifTextUtil.setTextspEditText的gif動(dòng)起來

遇到的坑

按道理上面幾行代碼就可以做到讓gif動(dòng)起來了,結(jié)果卻讓我大失所望,spEditTextinvalidate()確實(shí)被調(diào)用了,但是圖片卻不能正常刷新
折騰兩個(gè)晚上,逐漸將問題鎖定在android-gif-drawable身上,果然最終在https://github.com/koral--/android-gif-drawable/issues/368找到了解決方案

  • 對于EditText,需要setLayerType(View.LAYER_TYPE_SOFTWARE, null)才能正常刷新
  • TextView不需要特殊處理

談?wù)勔粋€(gè)奇怪的現(xiàn)象

之前文章中提到過GifTextUtil.setText只能讓一個(gè)drawable刷新一個(gè)TextView

為了定位上面的問題,自己寫了一個(gè)TextView和EditText

  • 使用同一個(gè)GifDrawable
  • 都沒有setLayerType(View.LAYER_TYPE_SOFTWARE, null)

現(xiàn)象是

  • 當(dāng)刷新的是EditText時(shí)gif顯示不正常 ,這個(gè)好理解
  • 但是當(dāng)刷新的是TextView時(shí)竟然兩個(gè)View中的gif都顯示的是正常的

也就是說在使用硬件加速的情況下只要有一個(gè)TextView中GifDrawable刷新了,會(huì)連帶著其他的TextView中的相同GifDrawable一起顯示新的圖像

  • GifTextUtil.setText()方法在使用GifDrawable的情況下其實(shí)是可以復(fù)用的,因?yàn)橹灰粋€(gè)刷新了其他的就會(huì)一起刷新
  • 雖然貌似可行但是并不建議GifTextUtil.setText()用在drawable復(fù)用的環(huán)境,因?yàn)檫@里面的原理我是沒想通,不能保證所有設(shè)備都正常

Glide

使用android-gif-drawable可以支持大部分本地gif資源的加載,但是在項(xiàng)目中往往會(huì)使用遠(yuǎn)程資源,這里使用Glide去解決

Glide是Google推薦使用的圖片加載庫,功能強(qiáng)大,API簡單而且擴(kuò)展性還強(qiáng),強(qiáng)烈推薦大家使用,當(dāng)然如果是要做圖文混排的話Glide用起來稍微麻煩了一點(diǎn)

關(guān)鍵

  • Glide加載圖片是個(gè)異步的過程,而且placeHolder和真正的圖片會(huì)加載兩次
  • 文本的設(shè)置應(yīng)該是個(gè)同步過程,先取到drawable,生成ImageSpan再產(chǎn)生一個(gè)Spannable,最后設(shè)置到TextView中
  • 所以利用Glide實(shí)現(xiàn)圖文混排的關(guān)鍵就在于如何把Glide的異步API轉(zhuǎn)換成可以同步產(chǎn)生drawable

代碼分析

主要代碼

 GlidePreDrawable preDrawable = new GlidePreDrawable();
 GlideApp.with(this)
          .load("http://5b0988e595225.cdn.sohucs.com/images/20170919/1ce5d4c52c24432e9304ef942b764d37.gif")
          .placeholder(gifDrawable)
          .into(new DrawableTarget(preDrawable));
  • GlidePreDrawable持有真正的圖片的mDrawable,并代理mDrawable的相關(guān)方法,相當(dāng)于先扔了一個(gè)占位置的到TextView中,等正主到了再刷新一下,操作 GlidePreDrawable就相當(dāng)于操作mDrawable,以此完成同步獲得drawable的任務(wù)
  • DrawableTarget用來更新GlidePreDrawable中包裹的真正的drawable
  • 拿到一個(gè)drawable,后面的代碼就和上一節(jié)的方法一樣了,不再贅述

下面主要看下GlidePreDrawableDrawableTarget干了啥

DrawableTarget

這個(gè)比較簡單,無非是在placeHolder加載完,圖片加載出錯(cuò),圖片加載完等情況下去更新preDrawable中的mDrawable

Glide里面也有個(gè)類叫GifDrawable,完全限定名是com.bumptech.glide.load.resource.gif.GifDrawable
android-gif-drawable的是pl.droidsonroids.gif.GifDrawable注意區(qū)分

public class DrawableTarget extends SimpleTarget<Drawable> {

  private static final String TAG = "GifTarget";
  private GlidePreDrawable preDrawable;

  @Override
  public void onResourceReady(@NonNull Drawable resource,
      @Nullable Transition<? super Drawable> transition) {
    Log.i(TAG, "onResourceReady: " + resource);
    preDrawable.setDrawable(resource);
    if (resource instanceof GifDrawable) {
      ((GifDrawable) resource).setLoopCount(GifDrawable.LOOP_FOREVER);
      ((GifDrawable) resource).start();
    }
    preDrawable.invalidateSelf();
  }

  public DrawableTarget(PreDrawable preDrawable) {
    this.preDrawable = preDrawable;
  }


  @Override
  public void onLoadCleared(@Nullable Drawable placeholder) {
    Log.i(TAG, "onLoadCleared: " + placeholder);
    if (preDrawable.getDrawable() != null) {
      return;
    }
    preDrawable.setDrawable(placeholder);
    if (placeholder instanceof GifDrawable) {
      ((GifDrawable) placeholder).setLoopCount(GifDrawable.LOOP_FOREVER);
      ((GifDrawable) placeholder).start();
    }
    preDrawable.invalidateSelf();

  }

  @Override
  public void onLoadStarted(@Nullable Drawable placeholder) {
    Log.i(TAG, "onLoadCleared: " + placeholder);
    preDrawable.setDrawable(placeholder);
    if (placeholder instanceof GifDrawable) {
      ((GifDrawable) placeholder).setLoopCount(GifDrawable.LOOP_FOREVER);
      ((GifDrawable) placeholder).start();
    }
    preDrawable.invalidateSelf();
  }

  @Override
  public void onLoadFailed(@Nullable Drawable errorDrawable) {
    Log.i(TAG, "onLoadFailed: " + errorDrawable);
    preDrawable.setDrawable(errorDrawable);
    if (errorDrawable instanceof GifDrawable) {
      ((GifDrawable) errorDrawable).setLoopCount(GifDrawable.LOOP_FOREVER);
      ((GifDrawable) errorDrawable).start();
    }
    preDrawable.invalidateSelf();
  }

}

GlidePreDrawable

GlidePreDrawable之前先說下Measurable這個(gè)接口,這個(gè)接口主要是為了EqualHeightSpan中確定drawable占據(jù)的空間,并且避免重復(fù)調(diào)用

public interface Measurable {

  int getWidth();//獲取真正的drawable的寬度

  int getHeight();//獲取真正的drawable的高度

  boolean canMeasure();//當(dāng)可以獲取到寬高是返回true,否則返回false

  boolean needResize();//只有當(dāng)drawable被設(shè)置時(shí)needResize返回true, onBoundsChange之后設(shè)為false,保證設(shè)置drawable邊界的方法不被多次調(diào)用
}
public class GlidePreDrawable extends Drawable implements Drawable.Callback, Measurable {

  private static final String TAG = "PreDrawable";
  private Drawable mDrawable;
  private boolean needResize;

  @Override
  public void draw(Canvas canvas) {
    if (mDrawable != null) {
      mDrawable.draw(canvas);
    }
  }

  @Override
  public void setAlpha(int alpha) {
    if (mDrawable != null) {
      mDrawable.setAlpha(alpha);
    }
  }

  @Override
  public void setColorFilter(ColorFilter cf) {
    if (mDrawable != null) {
      mDrawable.setColorFilter(cf);
    }
  }

  @Override
  public int getOpacity() {
    if (mDrawable != null) {
      return mDrawable.getOpacity();
    }
    return PixelFormat.UNKNOWN;
  }

  public void setDrawable(Drawable drawable) {
    if (this.mDrawable != null) {
      this.mDrawable.setCallback(null);
    }
    drawable.setCallback(this);
    this.mDrawable = drawable;
    needResize = true;
    if (getCallback() != null) {
      getCallback().invalidateDrawable(this);
    }
  }

  @Override
  protected void onBoundsChange(Rect bounds) {
    super.onBoundsChange(bounds);
    needResize = false;
  }

  @Override
  public void invalidateDrawable(Drawable who) {
    if (getCallback() != null) {
      getCallback().invalidateDrawable(this);
    }
  }

  @Override
  public void scheduleDrawable(Drawable who, Runnable what, long when) {
    if (getCallback() != null) {
      getCallback().scheduleDrawable(who, what, when);
    }
  }

  @Override
  public void unscheduleDrawable(Drawable who, Runnable what) {
    if (getCallback() != null) {
      getCallback().unscheduleDrawable(who, what);
    }
  }

  @Override
  public void setBounds(@NonNull Rect bounds) {
    super.setBounds(bounds);
    if (mDrawable != null) {
      mDrawable.setBounds(bounds);
    }
  }

  @Override
  public void setBounds(int left, int top, int right, int bottom) {
    super.setBounds(left, top, right, bottom);
    if (mDrawable != null) {
      mDrawable.setBounds(left, top, right, bottom);
    }
  }

  @Override
  public int getWidth() {
    if (mDrawable != null) {
      return mDrawable.getIntrinsicWidth();
    }
    return 0;
  }

  @Override
  public int getHeight() {
    if (mDrawable != null) {
      return mDrawable.getIntrinsicHeight();
    }
    return 0;
  }

  @Override
  public boolean canMeasure() {
    return mDrawable != null;
  }

  @Override
  public boolean needResize() {
    return mDrawable != null && needResize;
  }

  public Drawable getDrawable() {
    return mDrawable;
  }
}
  • GlidePreDrawable實(shí)現(xiàn)Drawable.Callback接口,持有mDrawable,將自己作為mDrawable的Callback,使得自己可以監(jiān)聽到mDrawable的刷新
  • draw方法直接調(diào)用mDrawable,其他方法也一樣,就是mDrawable的一個(gè)代理方法

總結(jié)

  • 使用android-gif-drawable加載本地gif,注意對EditText設(shè)置View.LAYER_TYPE_SOFTWARE
  • 使用Glide實(shí)現(xiàn)圖文混排,需要先創(chuàng)建一個(gè)真正的drawable的代理使異步API能夠同步產(chǎn)生drawable設(shè)置給TextView

完整代碼

項(xiàng)目地址https://github.com/sunhapper/SpEditTool
因?yàn)椴幌朐趲熘幸雽?code>Glide和android-gif-drawable的依賴,所以本文相關(guān)內(nèi)容都在demo module中
歡迎star,提PR、issue

索引

一行代碼讓TextView中ImageSpan支持Gif(一)
第一篇給出解決方案并分析整體思路

一行代碼讓TextView中ImageSpan支持Gif(二)
第二篇對實(shí)現(xiàn)中的細(xì)節(jié)和踩過的坑進(jìn)行說明

一行代碼讓TextView中ImageSpan支持Gif(三)
第三篇介紹如何使用android-gif-drawable和Glide實(shí)現(xiàn)遠(yuǎn)程gif圖片在TextView中的圖文混排

一行代碼讓TextView中ImageSpan支持Gif(四)
第四篇介紹在RecyclerView等需要drawable復(fù)用的場景下的gif動(dòng)圖顯示

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容