Android自定義控制元件之GifView
我的部落格地址: Android%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8E%A7%E4%BB%B6%E4%B9%8BGifView/" target="_blank" rel="nofollow,noindex">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/
最近的專案需要在主介面顯示Gif動圖,於是查了一下資料,一般是使用開源框架 Glide 或 android-gif-drawable ,前者載入速度較慢,並且沒有單獨的Gif播放與暫停介面,後者使用JNI載入,不會出現OOM問題,速度更快,效能更優。
由於我對自定義控制元件這方面瞭解不深,所以想趁這個機會剛好學習一下,自己寫一個可以流暢顯示Gif動圖並能控制播放的GifView控制元件。
自定義控制元件一般有以下三種方式:
- 組合原生控制元件
使用幾個基本控制元件組合在一起,形成一個新的控制元件。這種方式通常都需要繼承一個合適的 ViewGroup,再給它新增指定功能的控制元件,形成新的空間。通過這種方式建立的控制元件我們還可以給它指定一些可配置的屬性,增強它的可操控性。比如很多應用中普遍使用的標題欄控制元件。
- 繼承原生控制元件
繼承已有的控制元件,建立新控制元件,保留繼承的父控制元件的特性,並且還可以引入新特性。
- 重寫:自繪控制元件
如果繼承原生控制元件或者是組合原生控制元件都不能滿足我們的特殊需求,這種時候就只能夠自己重頭寫一個全新的控制元件了。建立一個全新的 View 重點在於繪製和互動的部分,通常需要繼承 View 類,並重寫 onDraw() 、onMeasure() 等方法,還可以像剛才的組合控制元件一樣,引入自定義屬性來豐富控制元件的可控性。
實踐內容參考此 連結 ,這裡不再贅述。
而這一次的GifView自定義控制元件則採取第三種方式: 自繪 。
首先我們先了解Android自帶的類: android.graphics.Movie
。它管理著Gif動畫中的多個幀,可以將其載入並播放,我們只要換算好時間關係,通過setTime()讓它在draw()的時候繪製出對應的幀影象,即可實現Gif播放的效果。
在動手之前,先通過官網文件瞭解 android.graphics.Movie 這個類,要養成一種閱讀官方資料或原始碼的習慣,在足夠了解的基礎上才能夠更好地進行二次創造。
本來是想自己動手寫的,但是發現Github上已經有人很好地實現了...所以我打算直接跟著他的程式碼進行講解...(沒錯其實是我想偷懶orz)
這是原始碼地址: https://github.com/Cutta/GifView
首先,在res/values目錄下新增自定義屬性,進行屬性配置:
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的基礎上開始編寫我們的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動圖 * Created by yc on 2018/11/18. */ public class GifView extends View { private static final int DEFAULT_MOVIE_VIEW_DURATION = 1000; // 預設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; /** * 建構函式 */ 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) 開始,必須關閉HW加速度才能在Canvas上繪製Movie if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { setLayerType(View.LAYER_TYPE_SOFTWARE, null); } // 從描述檔案中讀出Gif的值,繪製出Movie例項 final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.GifView, defStyle, R.style.Widget_GifView); mMovieResourceId = array.getResourceId(R.styleable.GifView_gif, -1); // -1為預設值 mPaused = array.getBoolean(R.styleable.GifView_paused, false); array.recycle(); if (mMovieResourceId != -1) { movie = Movie.decodeStream(getResources().openRawResource(mMovieResourceId)); } } /** * 設定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; /** * 計算新的movie開始時間,使它從剛剛停止的幀重新播放 */ mMovieStart = android.os.SystemClock.uptimeMillis() - mCurrentAnimationTime; invalidate(); } } /** * 暫停 */ public void pause() { if (!this.mPaused) { this.mPaused = true; invalidate(); } } /** * 判斷Gif動圖當前處於播放還是暫停狀態 */ 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(); /** * 計算水平方向上的擴充套件 */ 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; } } /** * 計算豎直方向上的擴充套件 */ 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; } } /** * 計算擴充套件規模 */ mScale = 1f / Math.max(scaleH, scaleW); mMeasuredMovieWidth = (int) (movieWidth * mScale); mMeasuredMovieHeight = (int) (movieHeight * mScale); setMeasuredDimension(mMeasuredMovieWidth, mMeasuredMovieHeight); } else { /** * Movie為空,設定最小可用大小 */ setMeasuredDimension(getSuggestedMinimumWidth(), getSuggestedMinimumHeight()); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); /** * 計算距離,以便繪製動畫幀 */ 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(); } } } /** * 計算當前動畫時間 */ private void updateAnimationTime() { long now = android.os.SystemClock.uptimeMillis(); // 如果是第一幀,記錄起始時間 if (mMovieStart == 0) { mMovieStart = now; } // 取出動畫的時長 int dur = movie.duration(); if (dur == 0) { dur = DEFAULT_MOVIE_VIEW_DURATION; } // 算出需要顯示第幾幀 mCurrentAnimationTime = (int) ((now - mMovieStart) % dur); } /** * 繪製當前要顯示的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(); } }
使用方式:
- 直接在xml佈局檔案中設定該控制元件的gif屬性指向哪個資源
<com.example.yc.androidsrc.GifView app:gif="@drawable/rain" ... />
- 在activity中通過setGifResource(int movieResourceId)進行設定
final GifView gifV = (GifView) findViewById(R.id.gifV); gifV.setGifResource(R.drawable.rain);
效果圖(錄製屏幕後再轉成Gif導致有點失真了orz 勉強看看):
