Android自定義View播放Gif動畫

大頭呆發表於2019-03-04

前言

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

相關文章