1. 程式人生 > >Android疑難雜症之(ImageView播放gif遇到的坑)

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;
        }
    }
}