GIFView與Android
效果圖
Android的ImageView是不支援GIF播放的,如果需要讓ImageView支援GIF就需要做自定義View。主流圖片載入框架中,如果要載入GIF,一般使用Glide。
播放GIF
一般有兩種方法實現
- 簡單地使用Movie:存在一定的效能問題,適用於少數圖片
- 使用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); // 設定當前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設定當前幀,然後不斷呼叫draw把當前幀畫出來就行了
GIFView設計
實現方法:通過自定義View,每次onDraw的時候得到Canvas,更新當前幀把內容滑到Canvas上
需要支援的功能:
- 播放GIF
- 迴圈播放
- 播放/暫停
- 尺寸控制(wrap_content/match_parent/指定尺寸)
- 縮放(FIT_START、FIT_CENTER、FIT_END、CENTER、CENTER_INSIDE、CENTER_CROP、FIT_XY七種縮放模式)
GIF解碼、播放/暫停、迴圈支援
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;
// 恢復到播放的相對位置
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();
}
至此,最簡單地功能已經實現了,該GIFView已經可以播放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就直接設定寬高,wrap_content就使用gif的寬高。
縮放
推薦先了解一下8種ScaleType分別是怎麼縮放的。
縮放的話尺寸是不受印象的,其中主要設定的變數是繪製的定位點以及寬高伸縮
下面是各種縮放型別的定位點以及寬高縮放比例計算值通過程式碼表示。
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);
}
}
計算得到對應的值後,只需要稍微修改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();
}
}
需要在適當的時候對定位點以及縮放值進行重新的計算
完整程式碼
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;
// 恢復到播放的相對位置
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);
}
}
}
相關文章
- Android 與 LuaAndroid
- Android與汽車Android
- Android之Window與WindowManagerAndroid
- Android 與前端互動Android前端
- Android與JS互調AndroidJS
- android 拖拽與縮放Android
- Flutter 與 Android 的互動FlutterAndroid
- Android 動畫 介紹與使用Android動畫
- capt 與 Android Gradle PluginAPTAndroidGradlePlugin
- Android與ARM處理器Android
- RN 與android原生互動Android
- Android程式與執行緒Android執行緒
- Android中的IntentFilter與安全AndroidIntentFilter
- Android 之 zygote 與程式建立AndroidGo
- Android 元件化探索與思考Android元件化
- Android layer type與WebView白屏AndroidWebView
- Android: HttpClient與Webview共享cookiesAndroidHTTPclientWebViewCookie
- android httpclient與webview cookie同步AndroidHTTPclientWebViewCookie
- Android listview與adapter用法AndroidViewAPT
- Android與Windows的對抗AndroidWindows
- Android自定義action與permission!!!Android
- Android與Java ME的區別與聯絡AndroidJava
- Flutter 與 Android 原生 WebView 對比FlutterAndroidWebView
- Android元件化探索與實踐Android元件化
- Lottie Android 動畫製作與使用Android動畫
- Android與iOS測試注意點AndroidiOS
- Android Handler與Looper原理簡析AndroidOOP
- Android全屏與透明狀態列Android
- RN 與原生通訊(Android篇)Android
- Android webview 與 js(Vue) 互動AndroidWebViewJSVue
- Android系統介紹與框架Android框架
- Android與WebView資料互動AndroidWebView
- HarmonyOS與Android的全面對比Android
- 【rosbridge】ROS與Android通訊ROSAndroid
- android AsyncTask 的分析與運用Android
- Android 原生 WebView 與 JavaScript 互動AndroidWebViewJavaScript
- Android 元件化方案探索與思考Android元件化
- Android 模組化探索與實踐Android