前言
GIF是一種很常見的動態(tài)圖片格式成福,在Android中它的使用場景非常多碾局,大到啟動頁動畫、小到一個Loading展示奴艾,都可以用GIF動畫來完成净当,使用也很方便,直接從美工那邊拿過來用就成蕴潦。如果項目趕時間或者自定義原生動畫太麻煩像啼,GIF都是一個很好的選擇,相比于最新的WEBP格式的動畫潭苞,也有更好的兼容性(畢竟已經(jīng)出現(xiàn)很多年了)忽冻。
關(guān)于圖片加載我一直用的是Google推薦的Glide,圖片加載和緩存都做的很好此疹,同樣也支持GIF動畫甚颂。不過Glide默認(rèn)就是循環(huán)播放Gif,沒有開放相關(guān)的接口來控制Gif秀菱。這就使的我們不能很好地控制Gif的播放振诬,比如控制播放開始時間、播放次數(shù)衍菱,播放暫停赶么、播放開始、結(jié)束事件的監(jiān)聽脊串,雖然用Glide可能做到(網(wǎng)上說可以辫呻,但我沒找到方法)清钥,但操作也會很麻煩。
分析
除了第三方的庫放闺,Android自帶的類android.graphics.Movie
也可以用來加載播放Gif動畫祟昭,而且實現(xiàn)起來很簡單。
Movie decodeStream(InputStream is)
Movie decodeFile(String pathName)
Movie decodeByteArray(byte[] data, int offset,int length)
按來源分別可以從Gif文件的輸入流怖侦,文件路徑篡悟,字節(jié)數(shù)組中得到Movie的實列。然后我們可以通過操作Movie對象來操作Gif文件匾寝。
下面介紹下幾個方法:
int width()
movie的寬搬葬,值等于gif圖片的寬,單位:px艳悔。
int height()
movie的高急凰,值等于gif圖片的高,單位:px猜年。
int duration()
movie播放一次的時長抡锈,也就是gif播放一次的時長,單位:毫秒乔外。
boolean isOpaque()
Gif圖片是否帶透明
boolean setTime(int relativeMilliseconds)
設(shè)置movie當(dāng)前處在什么時間企孩,然后找到對應(yīng)時間的圖片幀,范圍0 ~ duration袁稽。返回是否成功找到那一幀勿璃。
draw(Canvas canvas, float , float y)
draw(Canvas canvas, float x, float y, Paint paint)
在Canves中畫出當(dāng)前幀對應(yīng)的圖像。x推汽,y對應(yīng)Movie左上角在Canves中的坐標(biāo)补疑。
以上就是Movie平常會用到大部分方法,下面就利用這些自定義VIew實現(xiàn)播放Gif動畫歹撒。
實現(xiàn)
首先定義一些需要的屬性莲组,用于在布局文件中設(shè)置gif
<declare-styleable name="GIFVIEW">
<!--gif文件引用-->
<attr name="gifSrc" format="reference" />
<!--是否加載完自動播放-->
<attr name="authPlay" format="boolean" />
<!--播放次放,默認(rèn)永遠播放-->
<attr name="playCount" format="integer" />
</declare-styleable>
然后定義Gifde的播放監(jiān)聽器,來監(jiān)聽各個時段的事件暖夭,都很簡單就不再介紹了:
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文件,嘗試轉(zhuǎn)換為bitmap顯示
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), movieResourceId);
if (bitmap != null) {
setImageBitmap(bitmap);
return;
}
}
movieDuration = movie.duration() == 0 ? DEFAULT_DURATION : movie.duration();
requestLayout();
}
調(diào)用requestLayout重新計算View大小裕菠,并重新繪制咬清。
@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);
}
}
開始播放:
public void play(int counts) {
this.counts = counts;
reset();
if (mOnPlayListener != null) {
mOnPlayListener.onPlayStart();
}
invalidate();
}
不斷調(diào)用onDraw方法來繪制Gif當(dāng)前時間的圖片幀:
@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);
}
最核心的方法就是計算當(dāng)前時間需要播放處于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);
}
}
}
繼續(xù)Gif播放:
if (mPaused && mMoviePauseTime > 0) {
mPaused = false;
dealyTime = dealyTime + SystemClock.uptimeMillis() - mMoviePauseTime;
invalidate();
if (mOnPlayListener != null) {
mOnPlayListener.onPlayRestart();
}
}
經(jīng)過這些處理旧烧,我們就能更好地控制Gif的播放流程了影钉。下面簡單看下成品圖:
進階
倒敘播放
相信看了上面GifImageView的實現(xiàn)原理后,倒敘播放的實現(xiàn)也是很容易的掘剪。
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());
}
如下圖平委,狗子的頭已經(jīng)從原來的左邊轉(zhuǎn)到右邊變成了現(xiàn)在的右邊轉(zhuǎn)到左邊(???)。
像播放視頻一樣播放Gif動畫
這部分是我在寫完GifView后想到的一點進階功能夺谁,既然我們已經(jīng)實現(xiàn)了播放和暫停廉赔,即能控制在某個時間點播放指定的Gif圖片幀,如果再加入進度條予权,快進等功能,那么不就能做到和視頻播放器一樣的功能了嗎浪册?限于篇幅扫腺,我只簡單實現(xiàn)了進度條功能,更多功能實現(xiàn)請移步Github村象,地址:GifView笆环。