GIFView與Android

效果圖

GIFView效果圖

Android的ImageView是不支持GIF播放的额港,如果需要讓ImageView支持GIF就需要做自定義View移斩。主流圖片加載框架中,如果要加載GIF肠套,一般使用Glide猖任。

播放GIF

一般有兩種方法實現(xiàn)

  • 簡單地使用Movie:存在一定的性能問題朱躺,適用于少數(shù)圖片
  • 使用NDK對GIF進行解碼:性能較好,適用于列表類的GIF播放宇弛。android-gif-drawable

Movie類

public native int width();  // 獲取GIF圖片寬度
public native int height();  // 獲取GIF圖片高度
public native int duration(); // 獲取GIF圖片時長
public native boolean setTime(int time); // 設(shè)置當前GIF幀
public void draw(Canvas canvas, float x, float y, Paint paint); // 把當前幀畫到Canvas上
public void draw(Canvas canvas, float x, float y); // 把當前幀畫到Canvas上

// 三種解GIF圖的方式
public static Movie decodeStream(InputStream is);
public static native Movie decodeByteArray(byte[] bytes, int start, int length);
public static Movie decodeFile(String pathName);

該類的使用很簡單涯肩,通過setTime設(shè)置當前幀巢钓,然后不斷調(diào)用draw把當前幀畫出來就行了

GIFView設(shè)計

實現(xiàn)方法:通過自定義View症汹,每次onDraw的時候得到Canvas贷腕,更新當前幀把內(nèi)容滑到Canvas上

需要支持的功能:

  • 播放GIF
  • 循環(huán)播放
  • 播放/暫停
  • 尺寸控制(wrap_content/match_parent/指定尺寸)
  • 縮放(FIT_START、FIT_CENTER破婆、FIT_END胸囱、CENTER、CENTER_INSIDE裳扯、CENTER_CROP谤职、FIT_XY七種縮放模式)

GIF解碼允蜈、播放/暫停、循環(huán)支持

private Movie mMovie;
private long mStartTime;
private long mPauseTime;
private boolean mIsLoop;
private boolean mIsStart;

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mMovie == null) {
        return;
    }

    long now = SystemClock.uptimeMillis();
    int currentTime = (int) (now - mStartTime);

    if (currentTime >= mMovie.duration()) {
        if (mIsLoop) {
            mStartTime = SystemClock.uptimeMillis();
            currentTime = 0;
            mIsStart = true;
        } else if (mIsStart) {
            currentTime = mMovie.duration();
            mIsStart = false;
        }
    }

    mMovie.setTime(currentTime);
    mMovie.draw(canvas, 0, 0);
    if (mIsStart) {
        postInvalidate();
    }
}

public void pause() {
    if (!mIsStart) {
        return;
    }
    mIsStart = false;
    // 記錄播放的位置
    mPauseTime = SystemClock.uptimeMillis() - mStartTime;
    postInvalidate();
}


public void resume() {
    if (mIsStart) {
        return;
    }
    mIsStart = true;
    // 恢復(fù)到播放的相對位置
    mStartTime = SystemClock.uptimeMillis() - mPauseTime;
    postInvalidate();
}

public void setMovie(Movie movie) {
    mMovie = movie;
    mStartTime = SystemClock.uptimeMillis();
    mIsStart = true;
    postInvalidate();
}

public void setSource(int id) {
    setSource(getResources().openRawResource(id));
}

public void setSource(byte[] bytes, int start, int len) {
    setMovie(Movie.decodeByteArray(bytes, start, len));
}

public void setSource(InputStream inputStream) {
    setMovie(Movie.decodeStream(inputStream));
}

public void setSource(String pathName) {
    setMovie(Movie.decodeFile(pathName));
}

public void setLoop(boolean loop) {
    mIsLoop = loop;
    postInvalidate();
}

至此,最簡單地功能已經(jīng)實現(xiàn)了爆安,該GIFView已經(jīng)可以播放GIF圖片了仔引。

尺寸控制(wrap_content/match_parent/指定尺寸)

int width = 0;
int height = 0;
if (mMovie != null) {
    int wMode = MeasureSpec.getMode(widthMeasureSpec);
    int hMode = MeasureSpec.getMode(heightMeasureSpec);
    int wSize = MeasureSpec.getSize(widthMeasureSpec);
    int hSize = MeasureSpec.getSize(heightMeasureSpec);

    if (wMode == MeasureSpec.EXACTLY) {
        width = wSize;
    } else {
        width = mMovie.width();
    }

    if (hMode == MeasureSpec.EXACTLY) {
        height = hSize;
    } else {
        height = mMovie.height();
    }

}
setMeasuredDimension(width, height);

尺寸控制也簡單咖耘,指定寬高/match_parent就直接設(shè)置寬高儿倒,wrap_content就使用gif的寬高。

縮放

推薦先了解一下8種ScaleType分別是怎么縮放的彻犁。

縮放的話尺寸是不受印象的汞幢,其中主要設(shè)置的變量是繪制的定位點以及寬高伸縮

下面是各種縮放類型的定位點以及寬高縮放比例計算值通過代碼表示微谓。

private int mLeft;
private int mTop;
private float mScaleX;
private float mScaleY;

private void calcScale() {
    if (mMovie == null) {
        return;
    }
    float imageW = mMovie.width();
    float imageH = mMovie.height();
    float viewW = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
    float viewH = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

    if (mScaleType == ImageView.ScaleType.FIT_XY) {
        mScaleX = viewW / imageW;
        mScaleY = viewH / imageH;
    } else if (mScaleType == ImageView.ScaleType.FIT_START
            || mScaleType == ImageView.ScaleType.FIT_CENTER
            || mScaleType == ImageView.ScaleType.FIT_END) {
        mScaleY = mScaleX = viewH / imageH;
    } else if (mScaleType == ImageView.ScaleType.CENTER) {
        mScaleX = mScaleY = 1;
    } else if (mScaleType == ImageView.ScaleType.CENTER_CROP) {
        mScaleX = viewW / imageW;
        mScaleY = viewH / imageH;
        mScaleX = mScaleY = Math.max(mScaleX, mScaleY);
    } else if (mScaleType == ImageView.ScaleType.CENTER_INSIDE) {
        mScaleX = viewW / imageW;
        mScaleY = viewH / imageH;
        mScaleX = mScaleY = Math.min(mScaleX, mScaleY);
    }
}

private void calcLocation() {
    if (mMovie == null) {
        return;
    }
    int imageW = mMovie.width();
    int imageH = mMovie.height();
    int viewW = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
    int viewH = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
    int left = getPaddingLeft();
    int top = getPaddingTop();

    if (mScaleType == ImageView.ScaleType.FIT_XY) {
        mLeft = left;
        mTop = top;
    } else if (mScaleType == ImageView.ScaleType.FIT_START) {
        mLeft = left;
        mTop = top;
    } else if (mScaleType == ImageView.ScaleType.FIT_CENTER) {
        mLeft = (int) (left + (viewW - imageW * mScaleX) / 2);
        mTop = top;
    } else if (mScaleType == ImageView.ScaleType.FIT_END) {
        mLeft = (int) (left + viewW - imageW * mScaleX);
        mTop = top;
    } else if (mScaleType == ImageView.ScaleType.CENTER) {
        mLeft = -(imageW - viewW) / 2;
        mTop = -(imageH - viewH) / 2;
    } else if (mScaleType == ImageView.ScaleType.CENTER_CROP) {
        mLeft = (int) -(Math.abs(viewW - imageW * mScaleX) / 2);
        mTop = (int) -(Math.abs(viewH - imageH * mScaleY) / 2);
    } else if (mScaleType == ImageView.ScaleType.CENTER_INSIDE) {
        mLeft = (int) (left + (viewW - imageW * mScaleX) / 2);
        mTop = (int) (top + (viewH - imageH * mScaleY) / 2);
    }
}

計算得到對應(yīng)的值后仲智,只需要稍微修改onDraw方法


@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mMovie == null) {
        return;
    }

    long now = SystemClock.uptimeMillis();
    int currentTime = (int) (now - mStartTime);

    if (currentTime >= mMovie.duration()) {
        if (mIsLoop) {
            mStartTime = SystemClock.uptimeMillis();
            currentTime = 0;
            mIsStart = true;
        } else if (mIsStart) {
            currentTime = mMovie.duration();
            mIsStart = false;
        }
    }

    mMovie.setTime(currentTime);
    canvas.save(Canvas.MATRIX_SAVE_FLAG);
    canvas.scale(mScaleX, mScaleY);
    mMovie.draw(canvas, mLeft / mScaleX, mTop / mScaleY);
    canvas.restore();
    if (mIsStart) {
        postInvalidate();
    }
}

需要在適當?shù)臅r候?qū)Χㄎ稽c以及縮放值進行重新的計算

完整代碼

attrs.xml

<declare-styleable name="GIFView">
    <attr name="view_gif_loop" format="boolean" />
    <attr name="view_gif_source" format="reference" />
</declare-styleable>

GIFView

public class GIFView extends View {


    private Movie mMovie;
    private long mStartTime;
    private long mPauseTime;
    private boolean mIsLoop;
    private boolean mIsStart;


    private int mLeft;
    private int mTop;
    private float mScaleX;
    private float mScaleY;
    private ImageView.ScaleType mScaleType = ImageView.ScaleType.CENTER_CROP;
    private Runnable mCalcRunnable = new Runnable() {
        @Override
        public void run() {
            calcScale();
            calcLocation();
        }
    };


    public GIFView(Context context) {
        this(context, null);
    }

    public GIFView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GIFView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        initAttrs(attrs);

    }

    private void initAttrs(AttributeSet attrs) {
        TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.GIFView);


        mIsLoop = typedArray.getBoolean(R.styleable.GIFView_view_gif_loop, false);
        int id = typedArray.getResourceId(R.styleable.GIFView_view_gif_source, -1);
        if (id != -1) {
            setSource(id);
        }

        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = 0;
        int height = 0;
        if (mMovie != null) {
            int wMode = MeasureSpec.getMode(widthMeasureSpec);
            int hMode = MeasureSpec.getMode(heightMeasureSpec);
            int wSize = MeasureSpec.getSize(widthMeasureSpec);
            int hSize = MeasureSpec.getSize(heightMeasureSpec);

            if (wMode == MeasureSpec.EXACTLY) {
                width = wSize;
            } else {
                width = mMovie.width();
            }

            if (hMode == MeasureSpec.EXACTLY) {
                height = hSize;
            } else {
                height = mMovie.height();
            }

        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mMovie == null) {
            return;
        }

        long now = SystemClock.uptimeMillis();
        int currentTime = (int) (now - mStartTime);

        if (currentTime >= mMovie.duration()) {
            if (mIsLoop) {
                mStartTime = SystemClock.uptimeMillis();
                currentTime = 0;
                mIsStart = true;
            } else if (mIsStart) {
                currentTime = mMovie.duration();
                mIsStart = false;
            }
        }

        mMovie.setTime(currentTime);
        canvas.save(Canvas.MATRIX_SAVE_FLAG);
        canvas.scale(mScaleX, mScaleY);
        mMovie.draw(canvas, mLeft / mScaleX, mTop / mScaleY);
        canvas.restore();
        if (mIsStart) {
            postInvalidate();
        }
    }


    public void pause() {
        if (!mIsStart) {
            return;
        }
        mIsStart = false;
        // 記錄播放的位置
        mPauseTime = SystemClock.uptimeMillis() - mStartTime;
        postInvalidate();
    }


    public void resume() {
        if (mIsStart) {
            return;
        }
        mIsStart = true;
        // 恢復(fù)到播放的相對位置
        mStartTime = SystemClock.uptimeMillis() - mPauseTime;
        postInvalidate();
    }


    /**
     * 獲取當前播放的幀
     *
     * @return
     */
    public Bitmap getCurrentFrame() {
        if (mMovie == null) {
            return null;
        }
        Bitmap bitmap = Bitmap.createBitmap(mMovie.width(), mMovie.height(), Bitmap.Config.RGB_565);

        Canvas canvas = new Canvas(bitmap);

        canvas.scale(mScaleX, mScaleY);

        mMovie.draw(canvas, mLeft, mTop);

        return bitmap;

    }


    public Movie getMovie() {
        return mMovie;
    }


    public void setMovie(Movie movie) {
        mMovie = movie;
        mStartTime = SystemClock.uptimeMillis();
        mIsStart = true;
        if (getMeasuredHeight() == 0 || getMeasuredWidth() == 0) {
            post(mCalcRunnable);
        } else {
            mCalcRunnable.run();
        }
        requestLayout();
        postInvalidate();
    }

    public void setSource(int id) {
        setSource(getResources().openRawResource(id));
    }

    public void setSource(byte[] bytes, int start, int len) {
        setMovie(Movie.decodeByteArray(bytes, start, len));
    }

    public void setSource(InputStream inputStream) {
        setMovie(Movie.decodeStream(inputStream));
    }

    public void setSource(String pathName) {
        setMovie(Movie.decodeFile(pathName));
    }

    public void setLoop(boolean loop) {
        mIsLoop = loop;
        postInvalidate();
    }

    public void setScaleType(ImageView.ScaleType scaleType) {
        if (scaleType == ImageView.ScaleType.MATRIX) {
            throw new UnsupportedOperationException("不支持MATRIX類型縮放");
        }
        this.mScaleType = scaleType;
        if (getMeasuredHeight() == 0 || getMeasuredWidth() == 0) {
            post(mCalcRunnable);
        } else {
            mCalcRunnable.run();
        }
    }

    private void calcScale() {
        if (mMovie == null) {
            return;
        }
        float imageW = mMovie.width();
        float imageH = mMovie.height();
        float viewW = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        float viewH = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

        if (mScaleType == ImageView.ScaleType.FIT_XY) {
            mScaleX = viewW / imageW;
            mScaleY = viewH / imageH;
        } else if (mScaleType == ImageView.ScaleType.FIT_START
                || mScaleType == ImageView.ScaleType.FIT_CENTER
                || mScaleType == ImageView.ScaleType.FIT_END) {
            mScaleY = mScaleX = viewH / imageH;
        } else if (mScaleType == ImageView.ScaleType.CENTER) {
            mScaleX = mScaleY = 1;
        } else if (mScaleType == ImageView.ScaleType.CENTER_CROP) {
            mScaleX = viewW / imageW;
            mScaleY = viewH / imageH;
            mScaleX = mScaleY = Math.max(mScaleX, mScaleY);
        } else if (mScaleType == ImageView.ScaleType.CENTER_INSIDE) {
            mScaleX = viewW / imageW;
            mScaleY = viewH / imageH;
            mScaleX = mScaleY = Math.min(mScaleX, mScaleY);
        }
    }

    private void calcLocation() {
        if (mMovie == null) {
            return;
        }
        int imageW = mMovie.width();
        int imageH = mMovie.height();
        int viewW = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        int viewH = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
        int left = getPaddingLeft();
        int top = getPaddingTop();

        if (mScaleType == ImageView.ScaleType.FIT_XY) {
            mLeft = left;
            mTop = top;
        } else if (mScaleType == ImageView.ScaleType.FIT_START) {
            mLeft = left;
            mTop = top;
        } else if (mScaleType == ImageView.ScaleType.FIT_CENTER) {
            mLeft = (int) (left + (viewW - imageW * mScaleX) / 2);
            mTop = top;
        } else if (mScaleType == ImageView.ScaleType.FIT_END) {
            mLeft = (int) (left + viewW - imageW * mScaleX);
            mTop = top;
        } else if (mScaleType == ImageView.ScaleType.CENTER) {
            mLeft = -(imageW - viewW) / 2;
            mTop = -(imageH - viewH) / 2;
        } else if (mScaleType == ImageView.ScaleType.CENTER_CROP) {
            mLeft = (int) -(Math.abs(viewW - imageW * mScaleX) / 2);
            mTop = (int) -(Math.abs(viewH - imageH * mScaleY) / 2);
        } else if (mScaleType == ImageView.ScaleType.CENTER_INSIDE) {
            mLeft = (int) (left + (viewW - imageW * mScaleX) / 2);
            mTop = (int) (top + (viewH - imageH * mScaleY) / 2);
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蛀恩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子壳咕,更是在濱河造成了極大的恐慌顽馋,老刑警劉巖寸谜,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異他爸,居然都是意外死亡果善,警方通過查閱死者的電腦和手機巾陕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晾匠,“玉大人梯刚,你說我怎么就攤上這事【湎玻” “怎么了咳胃?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵旷太,是天一觀的道長供璧。 經(jīng)常有香客問我,道長来惧,這世上最難降的妖魔是什么演顾? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任钠至,我火速辦了婚禮,結(jié)果婚禮上棉钧,老公的妹妹穿的比我還像新娘宪卿。我一直安慰自己,他們只是感情好奢驯,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布瘪阁。 她就那樣靜靜地躺著邮偎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪豁跑。 梳的紋絲不亂的頭發(fā)上泻云,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音卸夕,去河邊找鬼快集。 笑死,一個胖子當著我的面吹牛乖寒,可吹牛的內(nèi)容都是我干的院溺。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼马澈,長吁一口氣:“原來是場噩夢啊……” “哼弄息!你這毒婦竟也來了摹量?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤凝果,失蹤者是張志新(化名)和其女友劉穎睦尽,沒想到半個月后当凡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡浪慌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年权纤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片外邓。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡坐榆,死狀恐怖冗茸,靈堂內(nèi)的尸體忽然破棺而出匹中,到底是詐尸還是另有隱情,我是刑警寧澤挂绰,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布葵蒂,位于F島的核電站重虑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏永高。R本人自食惡果不足惜提针,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一辐脖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧艇抠,春花似錦炭剪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至潮模,卻和暖如春擎厢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背动遭。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工厘惦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宵蕉,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓别智,卻偏偏與公主長得像缝左,于是被迫代替她去往敵國和親渺杉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

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