Android自定義控件之GifView

我的博客地址:https://rebornc.github.io/2018/11/19/Android%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8E%A7%E4%BB%B6%E4%B9%8BGifView/

最近的項(xiàng)目需要在主界面顯示Gif動(dòng)圖贺氓,于是查了一下資料授霸,一般是使用開源框架 Glideandroid-gif-drawable 审姓,前者加載速度較慢云稚,并且沒有單獨(dú)的Gif播放與暫停接口,后者使用JNI加載,不會(huì)出現(xiàn)OOM問題,速度更快疲牵,性能更優(yōu)。

由于我對自定義控件這方面了解不深榆鼠,所以想趁這個(gè)機(jī)會(huì)剛好學(xué)習(xí)一下纲爸,自己寫一個(gè)可以流暢顯示Gif動(dòng)圖并能控制播放的GifView控件。

自定義控件一般有以下三種方式:

  • 組合原生控件

使用幾個(gè)基本控件組合在一起妆够,形成一個(gè)新的控件缩焦。這種方式通常都需要繼承一個(gè)合適的 ViewGroup,再給它添加指定功能的控件责静,形成新的空間。通過這種方式創(chuàng)建的控件我們還可以給它指定一些可配置的屬性盖桥,增強(qiáng)它的可操控性灾螃。比如很多應(yīng)用中普遍使用的標(biāo)題欄控件。

  • 繼承原生控件

繼承已有的控件揩徊,創(chuàng)建新控件腰鬼,保留繼承的父控件的特性,并且還可以引入新特性塑荒。

  • 重寫:自繪控件

如果繼承原生控件或者是組合原生控件都不能滿足我們的特殊需求熄赡,這種時(shí)候就只能夠自己重頭寫一個(gè)全新的控件了。創(chuàng)建一個(gè)全新的 View 重點(diǎn)在于繪制和交互的部分齿税,通常需要繼承 View 類彼硫,并重寫 onDraw() 、onMeasure() 等方法,還可以像剛才的組合控件一樣拧篮,引入自定義屬性來豐富控件的可控性词渤。

實(shí)踐內(nèi)容參考此鏈接,這里不再贅述串绩。

而這一次的GifView自定義控件則采取第三種方式:自繪缺虐。

首先我們先了解Android自帶的類:android.graphics.Movie。它管理著Gif動(dòng)畫中的多個(gè)幀礁凡,可以將其加載并播放高氮,我們只要換算好時(shí)間關(guān)系,通過setTime()讓它在draw()的時(shí)候繪制出對應(yīng)的幀圖像顷牌,即可實(shí)現(xiàn)Gif播放的效果剪芍。

在動(dòng)手之前,先通過官網(wǎng)文檔了解 android.graphics.Movie 這個(gè)類韧掩,要養(yǎng)成一種閱讀官方資料或源碼的習(xí)慣紊浩,在足夠了解的基礎(chǔ)上才能夠更好地進(jìn)行二次創(chuàng)造。

本來是想自己動(dòng)手寫的疗锐,但是發(fā)現(xiàn)Github上已經(jīng)有人很好地實(shí)現(xiàn)了...所以我打算直接跟著他的代碼進(jìn)行講解...(沒錯(cuò)其實(shí)是我想偷懶orz)

這是源碼地址:https://github.com/Cutta/GifView

首先坊谁,在res/values目錄下添加自定義屬性,進(jìn)行屬性配置:

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="GifView">
        <attr name="gif" format="reference" />
        <attr name="paused" format="boolean" />
    </declare-styleable>

    <declare-styleable name="CustomTheme">
        <attr name="gifViewStyle" format="reference" />
    </declare-styleable>
</resources>

如果你對自定義控件的屬性配置不夠了解滑臊,可以閱讀博客1或者博客2口芍。

然后,在繼承View的基礎(chǔ)上開始編寫我們的GifView了雇卷。

GifView.class

package com.example.yc.androidsrc;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Movie;
import android.os.Build;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
 * 自定義控件鬓椭,用于顯示Gif動(dòng)圖
 * Created by yc on 2018/11/18.
 */

public class GifView extends View {

    private static final int DEFAULT_MOVIE_VIEW_DURATION = 1000; // 默認(rèn)1秒

    private int mMovieResourceId;
    private Movie movie;

    private long mMovieStart;
    private int mCurrentAnimationTime;

    private float mLeft;
    private float mTop;

    private float mScale;
    
    private int mMeasuredMovieWidth;
    private int mMeasuredMovieHeight;

    private volatile boolean mPaused;
    private boolean mVisible = true;

    /**
     * 構(gòu)造函數(shù)
     */
    public GifView(Context context) {
        this(context, null);
    }

    public GifView(Context context, AttributeSet attrs) {
        this(context, attrs, R.styleable.CustomTheme_gifViewStyle);
    }

    public GifView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        setViewAttributes(context, attrs, defStyle);
    }

    @SuppressLint("NewApi")
    private void setViewAttributes(Context context, AttributeSet attrs, int defStyle) {
        
        // 從 HONEYCOMB(Api Level:11) 開始,必須關(guān)閉HW加速度才能在Canvas上繪制Movie
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        }
        // 從描述文件中讀出Gif的值关划,繪制出Movie實(shí)例
        final TypedArray array = context.obtainStyledAttributes(attrs,
                R.styleable.GifView, defStyle, R.style.Widget_GifView);

        mMovieResourceId = array.getResourceId(R.styleable.GifView_gif, -1); // -1為默認(rèn)值
        mPaused = array.getBoolean(R.styleable.GifView_paused, false);

        array.recycle();

        if (mMovieResourceId != -1) {
            movie = Movie.decodeStream(getResources().openRawResource(mMovieResourceId));
        }
    }

    /**
     * 設(shè)置Gif資源
     */
    public void setGifResource(int movieResourceId) {
        this.mMovieResourceId = movieResourceId;
        movie = Movie.decodeStream(getResources().openRawResource(mMovieResourceId));
        requestLayout();
    }

    /**
     * 獲取Gif資源
     */
    public int getGifResource() {
        return this.mMovieResourceId;
    }

    /**
     * 播放
     */
    public void play() {
        if (this.mPaused) {
            this.mPaused = false;

            /**
             * 計(jì)算新的movie開始時(shí)間小染,使它從剛剛停止的幀重新播放
             */
            mMovieStart = android.os.SystemClock.uptimeMillis() - mCurrentAnimationTime;

            invalidate();
        }
    }

    /**
     * 暫停
     */
    public void pause() {
        if (!this.mPaused) {
            this.mPaused = true;

            invalidate();
        }

    }

    /**
     * 判斷Gif動(dòng)圖當(dāng)前處于播放還是暫停狀態(tài)
     */
    
    public boolean isPaused() {
        return this.mPaused;
    }

    public boolean isPlaying() {
        return !this.mPaused;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        if (movie != null) {
            int movieWidth = movie.width();
            int movieHeight = movie.height();

            /**
             * 計(jì)算水平方向上的擴(kuò)展
             */
            float scaleH = 1f;
            int measureModeWidth = MeasureSpec.getMode(widthMeasureSpec);

            if (measureModeWidth != MeasureSpec.UNSPECIFIED) {
                int maximumWidth = MeasureSpec.getSize(widthMeasureSpec);
                if (movieWidth > maximumWidth) {
                    scaleH = (float) movieWidth / (float) maximumWidth;
                }
            }

            /**
             * 計(jì)算豎直方向上的擴(kuò)展
             */
            float scaleW = 1f;
            int measureModeHeight = MeasureSpec.getMode(heightMeasureSpec);

            if (measureModeHeight != MeasureSpec.UNSPECIFIED) {
                int maximumHeight = MeasureSpec.getSize(heightMeasureSpec);
                if (movieHeight > maximumHeight) {
                    scaleW = (float) movieHeight / (float) maximumHeight;
                }
            }

            /**
             * 計(jì)算擴(kuò)展規(guī)模
             */
            mScale = 1f / Math.max(scaleH, scaleW);

            mMeasuredMovieWidth = (int) (movieWidth * mScale);
            mMeasuredMovieHeight = (int) (movieHeight * mScale);

            setMeasuredDimension(mMeasuredMovieWidth, mMeasuredMovieHeight);

        } else {
            /**
             * Movie為空,設(shè)置最小可用大小
             */
            setMeasuredDimension(getSuggestedMinimumWidth(), getSuggestedMinimumHeight());
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        /**
         * 計(jì)算距離贮折,以便繪制動(dòng)畫幀
         */
        mLeft = (getWidth() - mMeasuredMovieWidth) / 2f;
        mTop = (getHeight() - mMeasuredMovieHeight) / 2f;

        mVisible = getVisibility() == View.VISIBLE;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (movie != null) {
            if (!mPaused) {
                updateAnimationTime();
                drawMovieFrame(canvas);
                invalidateView();
            } else {
                drawMovieFrame(canvas);
            }
        }
    }


    @SuppressLint("NewApi")
    private void invalidateView() {
        if (mVisible) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                postInvalidateOnAnimation();
            } else {
                invalidate();
            }
        }
    }

    /**
     * 計(jì)算當(dāng)前動(dòng)畫時(shí)間
     */
    private void updateAnimationTime() {
        long now = android.os.SystemClock.uptimeMillis();
        // 如果是第一幀裤翩,記錄起始時(shí)間
        if (mMovieStart == 0) {
            mMovieStart = now;
        }
        // 取出動(dòng)畫的時(shí)長
        int dur = movie.duration();
        if (dur == 0) {
            dur = DEFAULT_MOVIE_VIEW_DURATION;
        }
        // 算出需要顯示第幾幀
        mCurrentAnimationTime = (int) ((now - mMovieStart) % dur);
    }

    /**
     * 繪制當(dāng)前要顯示的Gif幀
     */
    private void drawMovieFrame(Canvas canvas) {

        movie.setTime(mCurrentAnimationTime);

        canvas.save(Canvas.MATRIX_SAVE_FLAG);
        canvas.scale(mScale, mScale);
        movie.draw(canvas, mLeft / mScale, mTop / mScale);
        canvas.restore();
    }

    @SuppressLint("NewApi")
    @Override
    public void onScreenStateChanged(int screenState) {
        super.onScreenStateChanged(screenState);
        mVisible = screenState == SCREEN_STATE_ON;
        invalidateView();
    }

    @SuppressLint("NewApi")
    @Override
    protected void onVisibilityChanged(View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);
        mVisible = visibility == View.VISIBLE;
        invalidateView();
    }

    @Override
    protected void onWindowVisibilityChanged(int visibility) {
        super.onWindowVisibilityChanged(visibility);
        mVisible = visibility == View.VISIBLE;
        invalidateView();
    }

}

使用方式:

  1. 直接在xml布局文件中設(shè)置該控件的gif屬性指向哪個(gè)資源
<com.example.yc.androidsrc.GifView
        app:gif="@drawable/rain"
        ... />
  1. 在activity中通過setGifResource(int movieResourceId)進(jìn)行設(shè)置
final GifView gifV = (GifView) findViewById(R.id.gifV);
gifV.setGifResource(R.drawable.rain);

效果圖(錄制屏幕后再轉(zhuǎn)成Gif導(dǎo)致有點(diǎn)失真了orz 勉強(qiáng)看看):


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市调榄,隨后出現(xiàn)的幾起案子踊赠,更是在濱河造成了極大的恐慌,老刑警劉巖每庆,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件筐带,死亡現(xiàn)場離奇詭異,居然都是意外死亡缤灵,警方通過查閱死者的電腦和手機(jī)伦籍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進(jìn)店門蓝晒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鸽斟,你說我怎么就攤上這事拔创。” “怎么了富蓄?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵剩燥,是天一觀的道長。 經(jīng)常有香客問我立倍,道長灭红,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任口注,我火速辦了婚禮变擒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘寝志。我一直安慰自己娇斑,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布材部。 她就那樣靜靜地躺著毫缆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪乐导。 梳的紋絲不亂的頭發(fā)上苦丁,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天,我揣著相機(jī)與錄音物臂,去河邊找鬼旺拉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛棵磷,可吹牛的內(nèi)容都是我干的蛾狗。 我是一名探鬼主播,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼仪媒,長吁一口氣:“原來是場噩夢啊……” “哼淘太!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起规丽,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎撇贺,沒想到半個(gè)月后赌莺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡松嘶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年艘狭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,989評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巢音,死狀恐怖遵倦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情官撼,我是刑警寧澤梧躺,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站傲绣,受9級特大地震影響掠哥,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜秃诵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一续搀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧菠净,春花似錦禁舷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至煞抬,卻和暖如春霜大,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背革答。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工战坤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人残拐。 一個(gè)月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓途茫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親溪食。 傳聞我的和親對象是個(gè)殘疾皇子囊卜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評論 2 345

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,498評論 25 707
  • 用兩張圖告訴你,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料错沃? 從這篇文章中你...
    hw1212閱讀 12,693評論 2 59
  • 1栅组、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明先生_x閱讀 15,968評論 3 119
  • 我的爸爸 佟慶豪 在我的眼里,我的爸爸可以干很多事情枢析,會(huì)很多東西玉掸,如同游戲里的角色,有好多技能醒叁。 技能一 他會(huì)做飯...
    童聲童話閱讀 306評論 0 4
  • 缺少什么就會(huì)向往什么~平淡的生活過了二十多年司浪,難免總想追求刺激與不同泊业,像是在平直的軌道上運(yùn)行許久的火車,總有想跳脫...
    淺碧青荇閱讀 495評論 5 3