Android自定義View播放Gif動畫

前言

GIF是一種很常見的動態圖片格式,在Android中它的使用場景非常多,大到啟動頁動畫、小到一個Loading展示,都可以用GIF動畫來完成,使用也很方便,直接從美工那邊拿過來用就成。如果項目趕時間或者自定義原生動畫太麻煩,GIF都是一個很好的選擇,相比于最新的WEBP格式的動畫,也有更好的兼容性(畢竟已經出現很多年了)。

關于圖片加載我一直用的是Google推薦的Glide,圖片加載和緩存都做的很好,同樣也支持GIF動畫。不過Glide默認就是循環播放Gif,沒有開放相關的接口來控制Gif。這就使的我們不能很好地控制Gif的播放,比如控制播放開始時間、播放次數,播放暫停、播放開始、結束事件的監聽,雖然用Glide可能做到(網上說可以,但我沒找到方法),但操作也會很麻煩。

分析

除了第三方的庫,Android自帶的類android.graphics.Movie也可以用來加載播放Gif動畫,而且實現起來很簡單。

  • Movie decodeStream(InputStream is)

  • Movie decodeFile(String pathName)

  • Movie decodeByteArray(byte[] data, int offset,int length)

按來源分別可以從Gif文件的輸入流,文件路徑,字節數組中得到Movie的實列。然后我們可以通過操作Movie對象來操作Gif文件。
下面介紹下幾個方法:

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>

然后定義Gifde的播放監聽器,來監聽各個時段的事件,都很簡單就不再介紹了:

  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大小,并重新繪制。

   @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();
    }

不斷調用onDraw方法來繪制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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容