前言
GIF是一种很常见的动态图片格式,在Android中它的使用场景非常多,大到启动页动画、小到一个Loading展示,都可以用GIF动画来完成,使用也很方便,直接从美工那边拿过来用就成。如果项目赶时间或者自定义原生动画太麻烦,GIF都是一个很好的选择,相比于最新的WEBP格式的动画,也有更好的兼容性(毕竟已经出现很多年了)。
关于图片加载我一直用的是Google推荐的Glide,图片加载和缓存都做的很好,同样也支持GIF动画。不过Glide默认就是循环播放Gif,没有开放相关的接口来控制Gif。这就使的我们不能很好地控制Gif的播放,比如控制播放开始时间、播放次数,播放暂停、播放开始、结束事件的监听,虽然用Glide可能做到(网上说可以,但我没找到方法),但操作也会很麻烦。
分析
除了第三方的库,Android自带的类android.graphics.Movie
也可以用来加载播放Gif动画,而且实现起来很简单。按数据来源分别可以从Gif文件的输入流,文件路径,字节数组中得到Movie的实列。然后我们可以通过操作Movie对象来操作Gif文件。
Movie decodeStream(InputStream is)
Movie decodeFile(String pathName)
Movie decodeByteArray(byte[] data, int offset,int length)
下面介绍下几个movie的重要方法:
int width()
movie的宽,值等于gif图片的宽,单位:px。int height()
movie的高,值等于gif图片的高,单位:px。int duration()
movie播放一次的时长,也就是gif播放一次的时长,单位:毫秒。boolean isOpaque()
Gif图片是否带透明boolean setTime(int relativeMilliseconds)
设置movie当前处在什么时间,然后找到对应时间的图片帧,范围0 ~ duration。返回是否成功找到那一帧。draw(Canvas canvas, float , float y)
draw(Canvas canvas, float x, float y, Paint paint)
在Canves中画出当前帧对应的图像。x,y对应Movie左上角在Canves中的坐标。
以上就是Movie平常会用到大部分方法,下面就利用这些自定义VIew实现播放Gif动画。
实现
首先定义一些需要的属性,用于在布局文件中设置gif
<declare-styleable name="GIFVIEW"> <!--gif文件引用--> <attr name="gifSrc" format="reference" /> <!--是否加载完自动播放--> <attr name="authPlay" format="boolean" /> <!--播放次放,默认永远播放--> <attr name="playCount" format="integer" /> </declare-styleable>
然后定义Gif的播放监听器,来监听各个时段的事件,命名的含义都很简单就不再介绍了:
public interface OnPlayListener { void onPlayStart(); void onPlaying(int percent); void onPlayPause(boolean pauseSuccess); void onPlayRestart(); void onPlayEnd(); }
声明类,直接继承ImageView,这样我们不仅可以显示Gif动画,也可以显示普通图片:public class GifImageView extends AppCompatImageView
然后加载Gif图片资源
public void setGifResource(int movieResourceId, OnPlayListener onPlayListener) { mOnPlayListener = onPlayListener; movie = Movie.decodeStream(getResources().openRawResource(movieResourceId)); if (movie == null) { //如果movie为空,那么就不是gif文件,尝试转换为bitmap显示 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), movieResourceId); if (bitmap != null) { setImageBitmap(bitmap); return; } } movieDuration = movie.duration() == 0 ? DEFAULT_DURATION : movie.duration(); requestLayout(); }
调用requestLayout重新计算View大小,并重新绘制。如果是gif格式则View宽高等于movie的宽高,不是则调用父类的测量方法。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (movie != null) { int movieWidth = movie.width(); int movieHeight = movie.height(); setMeasuredDimension(movieWidth, movieHeight); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }
开始播放,其实就是调用invalidate从而调用onDraw方法就行UI绘制:
public void play(int counts) { this.counts = counts; reset(); if (mOnPlayListener != null) { mOnPlayListener.onPlayStart(); } invalidate(); }
不断调用onDraw方法来绘制Gif当前时间的图片帧(同样需要判断是否是gif格式,不是则调用父类的绘制方法):
@Override protected void onDraw(Canvas canvas) { if (movie != null) { if (!mPaused && hasStart) { drawMovieFrame(canvas); invalidateView(); } else { drawMovieFrame(canvas); } } else { super.onDraw(canvas); } } /** * 画出gif帧 */ private void drawMovieFrame(Canvas canvas) { movie.setTime(getCurrentFrameTime()); movie.draw(canvas, 0.0f, 0.0f); }
最核心的方法就是计算当前时间需要绘制处于movie中的对应时间的图片帧。
private int getCurrentFrameTime() { if (movieDuration == 0) return 0; //因为有暂停,所以需要减去暂停时间 long now = SystemClock.uptimeMillis() - dealyTime; int nowCount = (int) ((now - mMovieStart) / movieDuration); if (counts != -1 && nowCount >= counts) { hasStart = false; if (mOnPlayListener != null) { mOnPlayListener.onPlayEnd(); } } int currentTime = (int) ((now - mMovieStart) % movieDuration); int percent = currentTime * 100 / movieDuration; if (mOnPlayListener != null && hasStart) { mOnPlayListener.onPlaying(percent); } return currentTime; }
暂停Gif播放:
public void pause() { if (movie != null && !mPaused && hasStart) { mPaused = true; invalidate(); mMoviePauseTime = SystemClock.uptimeMillis(); if (mOnPlayListener != null) { mOnPlayListener.onPlayPause(true); } } else { if (mOnPlayListener != null) { mOnPlayListener.onPlayPause(false); } } }
继续Gif播放:
if (mPaused && mMoviePauseTime > 0) { mPaused = false; dealyTime = dealyTime + SystemClock.uptimeMillis() - mMoviePauseTime; invalidate(); if (mOnPlayListener != null) { mOnPlayListener.onPlayRestart(); } }
经过这些处理,我们就能更好地控制Gif的播放流程了。下面简单看下成品图:
进阶
倒叙播放
相信看了上面GifImageView的实现原理后,倒叙播放的实现也是很容易的。
public void playReserver() { if (movie != null) { reset(); reverse = true; if (mOnPlayListener != null) { mOnPlayListener.onPlayStart(); } invalidate(); } }
if (reverse) { movie.setTime(movieDuration - getCurrentFrameTime()); } else { movie.setTime(getCurrentFrameTime()); }
如下图,狗子的头已经从原来的左边转到右边变成了现在的右边转到左边(ಠᴗಠ)。
像播放视频一样播放Gif动画
这部分是我在写完GifView后想到的一点进阶功能,既然我们已经实现了播放和暂停,即能控制在某个时间点播放指定的Gif图片帧,如果再加入进度条,快进等功能,那么不就能做到和视频播放器一样的功能了吗?限于篇幅,我只简单实现了进度条功能,更多功能实现请移步Github,地址:GifView。
作者:大头呆