前言
之前两篇文章介绍了如何让ImageSpan中的drawable如何去刷新TextView,那么如何创建一个gif的drawable呢?这篇文章主要介绍利用 android-gif-drawable和glide产生GifDrawable的方法
android-gif-drawable
android-gif-drawable
使用jni方法去渲染gif图片,效率不错,使用也很简单,不过不支持远程图片的加载
使用
GifDrawable
类的构造方法可以接受File,Uri,InputStream,byte[],ResourcesId...等多种类型的参数,直接new就可以创建一个可以动的Drawable
利用GifDrawable实现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);
//利用之前介绍的静态方法使spEditText中的gif动起来
GifTextUtil.setText(spEditText,spannable);
- 这里传入了一个资源的id,当然它是一张gif图片
-
EqualHeightSpan
是自己写的一个让图片保持和文字等高的ImageSpan
- 最后使用
GifTextUtil.setText
让spEditText
的gif动起来
遇到的坑
按道理上面几行代码就可以做到让gif动起来了,结果却让我大失所望,spEditText
的invalidate()
确实被调用了,但是图片却不能正常刷新
折腾两个晚上,逐渐将问题锁定在android-gif-drawable
身上,果然最终在https://github.com/koral--/android-gif-drawable/issues/368找到了解决方案
- 对于
EditText
,需要setLayerType(View.LAYER_TYPE_SOFTWARE, null)
才能正常刷新 -
TextView
不需要特殊处理
谈谈一个奇怪的现象
之前文章中提到过GifTextUtil.setText
只能让一个drawable刷新一个TextView
为了定位上面的问题,自己写了一个TextView和EditText
- 使用同一个GifDrawable
- 都没有
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
现象是
- 当刷新的是EditText时gif显示不正常 ,这个好理解
- 但是当刷新的是TextView时竟然两个View中的gif都显示的是正常的
也就是说在使用硬件加速的情况下只要有一个TextView中GifDrawable刷新了,会连带着其他的TextView中的相同GifDrawable一起显示新的图像
-
GifTextUtil.setText()
方法在使用GifDrawable
的情况下其实是可以复用的,因为只要一个刷新了其他的就会一起刷新 - 虽然貌似可行但是并不建议
GifTextUtil.setText()
用在drawable复用的环境,因为这里面的原理我是没想通,不能保证所有设备都正常
Glide
使用android-gif-drawable
可以支持大部分本地gif资源的加载,但是在项目中往往会使用远程资源,这里使用Glide
去解决
Glide
是Google推荐使用的图片加载库,功能强大,API简单而且扩展性还强,强烈推荐大家使用,当然如果是要做图文混排的话Glide用起来稍微麻烦了一点
关键
- Glide加载图片是个异步的过程,而且placeHolder和真正的图片会加载两次
- 文本的设置应该是个同步过程,先取到drawable,生成ImageSpan再产生一个Spannable,最后设置到TextView中
- 所以利用Glide实现图文混排的关键就在于如何把Glide的异步API转换成可以同步产生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
的相关方法,相当于先扔了一个占位置的到TextView中,等正主到了再刷新一下,操作GlidePreDrawable
就相当于操作mDrawable
,以此完成同步获得drawable的任务 -
DrawableTarget
用来更新GlidePreDrawable中包裹的真正的drawable - 拿到一个drawable,后面的代码就和上一节的方法一样了,不再赘述
下面主要看下GlidePreDrawable
和DrawableTarget
干了啥
DrawableTarget
这个比较简单,无非是在placeHolder加载完,图片加载出错,图片加载完等情况下去更新preDrawable
中的mDrawable
Glide里面也有个类叫GifDrawable
,完全限定名是com.bumptech.glide.load.resource.gif.GifDrawable
android-gif-drawable
的是pl.droidsonroids.gif.GifDrawable
注意区分
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
这个接口,这个接口主要是为了EqualHeightSpan
中确定drawable占据的空间,并且避免重复调用
public interface Measurable {
int getWidth();//获取真正的drawable的宽度
int getHeight();//获取真正的drawable的高度
boolean canMeasure();//当可以获取到宽高是返回true,否则返回false
boolean needResize();//只有当drawable被设置时needResize返回true, onBoundsChange之后设为false,保证设置drawable边界的方法不被多次调用
}
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
实现Drawable.Callback
接口,持有mDrawable
,将自己作为mDrawable
的Callback,使得自己可以监听到mDrawable
的刷新 -
draw
方法直接调用mDrawable
,其他方法也一样,就是mDrawable
的一个代理方法
总结
- 使用android-gif-drawable加载本地gif,注意对EditText设置
View.LAYER_TYPE_SOFTWARE
- 使用Glide实现图文混排,需要先创建一个真正的drawable的代理使异步API能够同步产生drawable设置给TextView
完整代码
项目地址https://github.com/sunhapper/SpEditTool
因为不想在库中引入对Glide
和android-gif-drawable
的依赖,所以本文相关内容都在demo module中
欢迎star,提PR、issue
索引
一行代码让TextView中ImageSpan支持Gif(一)
第一篇给出解决方案并分析整体思路
一行代码让TextView中ImageSpan支持Gif(二)
第二篇对实现中的细节和踩过的坑进行说明
一行代码让TextView中ImageSpan支持Gif(三)
第三篇介绍如何使用android-gif-drawable和Glide实现远程gif图片在TextView中的图文混排
一行代码让TextView中ImageSpan支持Gif(四)
第四篇介绍在RecyclerView等需要drawable复用的场景下的gif动图显示