一行代碼讓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ò),使用也很簡(jiǎn)單,不過不支持遠(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找到了解決方案

  • 對(duì)于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簡(jiǎn)單而且擴(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è)比較簡(jiǎn)單,無非是在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,注意對(duì)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)椴幌朐趲熘幸雽?duì)Glideandroid-gif-drawable的依賴,所以本文相關(guān)內(nèi)容都在demo module中
歡迎star,提PR、issue

索引

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

一行代碼讓TextView中ImageSpan支持Gif(二)
第二篇對(duì)實(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ù)用的場(chǎng)景下的gif動(dòng)圖顯示

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末邦泄,一起剝皮案震驚了整個(gè)濱河市锰悼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異桶蝎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)终佛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門俊嗽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人铃彰,你說我怎么就攤上這事绍豁。” “怎么了牙捉?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵竹揍,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我邪铲,道長(zhǎng)芬位,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任带到,我火速辦了婚禮昧碉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己被饿,他們只是感情好四康,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著狭握,像睡著了一般闪金。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上论颅,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天哎垦,我揣著相機(jī)與錄音,去河邊找鬼恃疯。 笑死漏设,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的澡谭。 我是一名探鬼主播愿题,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼损俭,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蛙奖!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起杆兵,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤雁仲,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后琐脏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體攒砖,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年日裙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吹艇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡昂拂,死狀恐怖受神,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情格侯,我是刑警寧澤鼻听,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站联四,受9級(jí)特大地震影響撑碴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜朝墩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一醉拓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦亿卤、人聲如沸玫镐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恐似。三九已至,卻和暖如春傍念,著一層夾襖步出監(jiān)牢的瞬間矫夷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工憋槐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留双藕,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓阳仔,卻偏偏與公主長(zhǎng)得像忧陪,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子近范,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容