Android自定義View播放Gif動畫

前言

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笆环。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市厚者,隨后出現(xiàn)的幾起案子躁劣,更是在濱河造成了極大的恐慌,老刑警劉巖库菲,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件账忘,死亡現(xiàn)場離奇詭異,居然都是意外死亡熙宇,警方通過查閱死者的電腦和手機鳖擒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烫止,“玉大人蒋荚,你說我怎么就攤上這事」萑洌” “怎么了期升?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長互躬。 經(jīng)常有香客問我播赁,道長,這世上最難降的妖魔是什么吼渡? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任行拢,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舟奠。我一直安慰自己竭缝,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布沼瘫。 她就那樣靜靜地躺著抬纸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪耿戚。 梳的紋絲不亂的頭發(fā)上湿故,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音膜蛔,去河邊找鬼坛猪。 笑死,一個胖子當(dāng)著我的面吹牛皂股,可吹牛的內(nèi)容都是我干的墅茉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼呜呐,長吁一口氣:“原來是場噩夢啊……” “哼就斤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蘑辑,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤洋机,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后洋魂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绷旗,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年副砍,在試婚紗的時候發(fā)現(xiàn)自己被綠了刁标。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡址晕,死狀恐怖膀懈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谨垃,我是刑警寧澤启搂,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站刘陶,受9級特大地震影響胳赌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜匙隔,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一疑苫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦捍掺、人聲如沸撼短。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽曲横。三九已至,卻和暖如春不瓶,著一層夾襖步出監(jiān)牢的瞬間禾嫉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工蚊丐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留熙参,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓麦备,卻偏偏與公主長得像孽椰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子泥兰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容