Android疑難雜症之(ImageView播放gif遇到的坑)
最近做專案需要用到gif圖片,專案中直接用的Glide的gif載入,還沒有時間去研究Glide的原始碼的,不得不說Glide很強大啊,以前在郭神的部落格中看到了一個播放Gif的ImageView,於是打算跟著敲一遍咯。
原文連結先附上:http://blog.csdn.net/sinyu890807/article/details/11100315
思路:
利用Movie類去播放gif格式檔案,然後不斷的畫在當前ImageView的canvas上,以幀計算。
這樣說可能有點抽象,直接上程式碼了。
不懂的自己去郭神部落格(^__^) 嘻嘻……
下面說說其中的幾個坑的地方:
1、有很多童鞋想必一開始也跟我一樣,就是在XML中給ImageView設定了src=”@drawable\xxxx”,然後我們在程式碼中需要拿到對應資源的id,對程式碼中的利用反射獲取resouceId不明白,下面帶著大家一起擼一擼程式碼。
text.xml檔案:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.yqy.dialogdemo.MainActivity">
<com.yqy.dialogdemo.AnimaImageView
android:layerType="software"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:auto_play="false"
android:src="@drawable/cat"/>
</RelativeLayout>
其中我們的android:src=”@drawable/cat”是一張gif圖片,然後我們在下面的程式碼中需要拿到對應的id。
private void obtainStyledAttr(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray a=context.getTheme().obtainStyledAttributes(attrs,R.styleable.AnimaImageView,defStyleAttr,0);
int resId=getIdentifier(a);
if(resId!=0){
// 當資源id不等於0時,就去獲取該資源的流
InputStream is=getResources().openRawResource(resId);
// 使用Movie類對流進行解碼
mMovie=Movie.decodeStream(is);
//mMovie不等null說明這是一個GIF圖片
if(mMovie!=null){
//是否自動播放
isAutoPlay=a.getBoolean(R.styleable.AnimaImageView_auto_play,false);
/**
* 獲取gif圖片大小
*/
Bitmap bitmap= BitmapFactory.decodeStream(is);
bitmapSize=new BitmapSize(bitmap.getWidth(),bitmap.getHeight());
bitmap.recycle();
if(!isAutoPlay){
// 當不允許自動播放的時候,得到開始播放按鈕的圖片,並註冊點選事件
mStartBotton=BitmapFactory.decodeResource(getResources(),R.mipmap.icon_play);
setOnClickListener(this);
}
}
}
a.recycle();
}
其中有一個方法。
/**
* 通過反射獲取src中的資源id
* @param a
*/
private int getIdentifier(TypedArray a) {
try {
Field mValueFiled = a.getClass().getDeclaredField("mValue");
mValueFiled.setAccessible(true);
TypedValue typedValue= (TypedValue) mValueFiled.get(a);
return typedValue.resourceId;
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return 0;
}
這是郭神給的程式碼,我一開始也有點懵逼,然後看了看ImageView的原始碼就知道了。
首先我們看看ImageView中是怎麼拿到src中的資源的,在ImageView的構造方法中我們看到了這麼一段程式碼:
Drawable d = a.getDrawable(com.android.internal.R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d);
}
可以看到在Android自帶的attr檔案中通過定義的attr拿到了src中的drawable檔案,然後我們看看getDrawable這個方法到底幹了什麼?
@Nullable
public Drawable getDrawable(int index) {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
final TypedValue value = mValue;
if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
if (value.type == TypedValue.TYPE_ATTRIBUTE) {
throw new UnsupportedOperationException(
"Failed to resolve attribute at index " + index + ": " + value);
}
return mResources.loadDrawable(value, value.resourceId, mTheme);
}
return null;
}
看到這段程式碼的時候return mResources.loadDrawable(value, value.resourceId, mTheme);我們發現點不進去了,因為隱藏掉了,但是我們大致可以看到首先final TypedValue value = mValue;然後value.resourceId,“resourceId”應該就是我們需要的檔案id了。
我們點開TypedValue 類,看看其成員變數resourceId:
/** If Value came from a resource, this holds the corresponding resource id. */
@AnyRes
public int resourceId;
英語不是很好(^__^) 嘻嘻……,大致是“如果Value來自一個資源,那麼這個變數就擁有資源的id”,看到這裡我們終於發現了這就是我們設定的src中的資源id。
於是我們開始動工了首先拿到ImageView中的TypedValue物件”mValue”:
/**
* 通過反射獲取src中的資源id
* @param a
*/
private int getIdentifier(TypedArray a) {
try {
Field mValueFiled = a.getClass().getDeclaredField("mValue");
mValueFiled.setAccessible(true);
TypedValue typedValue= (TypedValue) mValueFiled.get(a);
return typedValue.resourceId;
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return 0;
}
現在看到這段程式碼是不是soeasy了,我想機智的你肯定分分鐘搞定了,(ps:看來大神也不是那麼好當的,背後付出了肯定很多很多,這也提醒我們想做一個資深的Android程式設計師,還是需要去好好研究下原始碼的(^__^) ……)
我第二個遇到的坑就是我照著程式碼擼了一遍後,發現還是顯示不出gif圖片,接著我們直接上了郭神的程式碼,還是不行,這時我想到了硬體加速的問題。
我一開始是直接加在了application中:
<application
android:hardwareAccelerated="false"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
執行後果然可行,然後我想加在這範圍是不是太大了,然後我換到了xml佈局檔案中:
<com.yqy.dialogdemo.AnimaImageView
android:layerType="none"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:auto_play="false"
android:src="@drawable/cat"/>
加在這裡的時候我發現不行了???很鬱悶,接著我直接加到了程式碼中:
private void obtainStyledAttr(Context context, AttributeSet attrs, int defStyleAttr) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
this.setLayerType(View.LAYER_TYPE_SOFTWARE,null);
}
繼續執行就可以了。
然後我們修改了第二種方式:
<com.yqy.dialogdemo.AnimaImageView
android:layerType="software"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:auto_play="false"
android:src="@drawable/cat"/>
執行可以了。。(^__^) 嘻嘻……!!
附上全部程式碼:
package com.yqy.dialogdemo;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Movie;
import android.os.Build;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.widget.ImageView;
import java.io.InputStream;
import java.lang.reflect.Field;
/**
* @author EX_YINQINGYANG
* @version [Android PABank C01, @2016-09-29]
* @date 2016-09-29
* @description 可以播放gif動畫的ImageView
*/
public class AnimaImageView extends ImageView implements View.OnClickListener {
/**
* 是否自動播放
*/
private boolean isAutoPlay;
/**
* 播放GIF動畫的關鍵類
*/
private Movie mMovie;
/**
* gif寬高
*/
private BitmapSize bitmapSize;
/**
* 播放按鈕
*/
private Bitmap mStartBotton;
/**
* 是否正在播放gif
*/
private boolean isPlaying;
/**
* gif開始時間
*/
private long mMovieStart;
public AnimaImageView(Context context) {
super(context);
}
public AnimaImageView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public AnimaImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
this.setLayerType(View.LAYER_TYPE_SOFTWARE,null);
}
obtainStyledAttr(context,attrs,defStyleAttr);
}
private void obtainStyledAttr(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray a=context.getTheme().obtainStyledAttributes(attrs,R.styleable.AnimaImageView,defStyleAttr,0);
int resId=getIdentifier(a);
if(resId!=0){
// 當資源id不等於0時,就去獲取該資源的流
InputStream is=getResources().openRawResource(resId);
// 使用Movie類對流進行解碼
mMovie=Movie.decodeStream(is);
//mMovie不等null說明這是一個GIF圖片
if(mMovie!=null){
//是否自動播放
isAutoPlay=a.getBoolean(R.styleable.AnimaImageView_auto_play,false);
/**
* 獲取gif圖片大小
*/
Bitmap bitmap= BitmapFactory.decodeStream(is);
bitmapSize=new BitmapSize(bitmap.getWidth(),bitmap.getHeight());
bitmap.recycle();
if(!isAutoPlay){
// 當不允許自動播放的時候,得到開始播放按鈕的圖片,並註冊點選事件
mStartBotton=BitmapFactory.decodeResource(getResources(),R.mipmap.icon_play);
setOnClickListener(this);
}
}
}
a.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//當時gif圖片的時候,控制元件寬高為gif檔案大小
if(mMovie!=null){
setMeasuredDimension(bitmapSize.width,bitmapSize.height);
}
}
@Override
protected void onDraw(Canvas canvas) {
//當為一張普通的圖片的時候
if(mMovie==null){
super.onDraw(canvas);
}else{
//如果自動播放的話,就直接播放
if(isAutoPlay){
playMovie(canvas);
invalidate();
}else{
//如果已經點選了播放按鈕的話就開始播放gif
if(isPlaying){
if(playMovie(canvas)){
isPlaying=false;
}
invalidate();
}else{
// 還沒開始播放就只繪製GIF圖片的第一幀,並繪製一個開始按鈕
mMovie.setTime(0);
mMovie.draw(canvas, 0, 0);
int offsetW = (bitmapSize.width - mStartBotton.getWidth()) / 2;
int offsetH = (bitmapSize.height - mStartBotton.getHeight()) / 2;
canvas.drawBitmap(mStartBotton, offsetW, offsetH, null);
}
}
}
}
/**
* 開始播放GIF動畫,播放完成返回true,未完成返回false。
*
* @param canvas
* @return 播放完成返回true,未完成返回false。
*/
private boolean playMovie(Canvas canvas) {
//獲取當前時間
long now = SystemClock.uptimeMillis();
if (mMovieStart == 0) {
mMovieStart = now;
}
int duration = mMovie.duration();
if (duration == 0) {
duration = 1000;
}
int relTime = (int) ((now - mMovieStart) % duration);
mMovie.setTime(relTime);//不斷的設定gif的播放位置
mMovie.draw(canvas, 0, 0);//將movie畫在canvas上
//如果(當前時間-gif開始的時間=gif總時長)說明播放完畢了
if ((now - mMovieStart) >= duration) {
mMovieStart = 0;
return true;
}
return false;
}
/**
* 通過反射獲取src中的資源id
* @param a
*/
private int getIdentifier(TypedArray a) {
try {
Field mValueFiled = a.getClass().getDeclaredField("mValue");
mValueFiled.setAccessible(true);
TypedValue typedValue= (TypedValue) mValueFiled.get(a);
return typedValue.resourceId;
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return 0;
}
/**
* 當點選圖片的時候播放gif
*/
@Override
public void onClick(View v) {
isPlaying = true;
invalidate();
}
/**
* BitmapSize
*/
class BitmapSize{
private int width;
private int height;
public BitmapSize(int width, int height) {
this.width = width;
this.height = height;
}
}
}