前言

在平时的项目开发中,我们或多或少会遇到加载gif图片这样的需求,但是Android的ImageView又无法直接加载Gif图片,面对这样的需求我们一般都会想到使用支持加载gif动图的Glide第三方库来进行实现,但是使用过程中发现Glide在加载大的gif图片时会出现卡顿,而且加载速度很慢,这很影响用户体验,所以又从网上找到另一个专门应对gif图片加载的另外一个开源库GifView,但是使用中发现当频繁的加载过大的图片的时候,会很容易出现OOM,最后机缘巧合之下了解到了android-gif-drawable这个开源库,它也是用来进行gif图片的加载显示的,底层解码使用C实现,极大的提高了解码效率,并且是通过JNI来渲染帧的,相比Glide等框架提高了gif图片加载的速度,同时很大程度上避免了OOM。

android-gif-drawable的集成

在app的build.gradle文件中添加如下依赖:

dependencies {
    compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.15'
}

android-gif-drawable的使用

android-gif-drawable有四种控件:GifImageView、GifImageButton、GifTextView、GifTextureView。这里以GifImageView为例进行介绍。

加载本地GIF图片

1.直接在xml布局文件中进行指定,如下:

<pl.droidsonroids.gif.GifImageView
    android:id="@+id/fragment_gif_local"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/dog"/>

GifImageView会自动识别”Android:src”或者”android:background”的内容是否Gif文件,如果是Gif就播放Gif文件,如果是普通的静态图片,例如是png,jpg的,这个时候,gifImageView等这些控件的效果和ImageView是一样的。这样其实Gif就已经可以播放了,就这么简单。

2.通过代码动态的指定要加载的gif图,代码如下:

setImageResource(int resId)
setBackgroundResource(int resId)

除了上面这两种方法以外,还支持多种来源,如下:

//1. 构建GifDrawable对象,根据来源不同选择不同的构造方法进行创建
// 从Assets中获取
GifDrawable gifFromAssets = new GifDrawable(getAssets(), "anim.gif");
// 从drawable或者raw中获取
GifDrawable gifFromResource = new GifDrawable(getResources(), R.drawable.anim);
// 从文件中获取
File gifFile = new File(getFilesDir(), "anim.gif");
GifDrawable gifFromFile = new GifDrawable(gifFile);
//从输入流中获取,如果GifDrawable不再使用,输入流会自动关闭。另外,你还可以通过调用recycle()关闭不再使用的输入流
InputStream inputStream = new FileInputStream(gifFile);
BufferedInputStream bis = new BufferedInputStream(inputStream, 1024 * 1024);
GifDrawable gifFromStream = new GifDrawable(bis);
//2. 设置给GifImageView控件
gifImageView.setImageDrawable(gifFromResDrawable);

GifDrawable是用于该开源库的Drawable类。构造方法大致有9种:

//1. asset file
GifDrawable gifFromAssets = new GifDrawable( getAssets(), "anim.gif" );

//2. resource (drawable or raw)
GifDrawable gifFromResource = new GifDrawable( getResources(), R.drawable.anim );

//3. byte array
byte[] rawGifBytes = ...
GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );

//4. FileDescriptor
FileDescriptor fd = new RandomAccessFile( "/path/anim.gif", "r" ).getFD();
GifDrawable gifFromFd = new GifDrawable( fd );

//5. file path
GifDrawable gifFromPath = new GifDrawable( "/path/anim.gif" );

//6. file
File gifFile = new File(getFilesDir(),"anim.gif");
GifDrawable gifFromFile = new GifDrawable(gifFile);

//7. AssetFileDescriptor
AssetFileDescriptor afd = getAssets().openFd( "anim.gif" );
GifDrawable gifFromAfd = new GifDrawable( afd );

//8. InputStream (it must support marking)
InputStream sourceIs = ...
BufferedInputStream bis = new BufferedInputStream( sourceIs, GIF_LENGTH );
GifDrawable gifFromStream = new GifDrawable( bis );

//9. direct ByteBuffer
ByteBuffer rawGifBytes = ...
GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );

加载网络Gif图片

如果gif是网络图片,这个库不支持直接加载一个url,但是提供了一个GifDrawable 类,可以通过传入本地gif图片的路径,输入流等方式构造创建GifDrawable对象(参见上面的9种构造方法),这里我们采用的办法是将Gif图片下载到缓存目录中,然后从磁盘缓存中获取该Gif动图进行显示。
1、下载工具DownloadUtils.java

public class DownloadUtils {
    private final int DOWN_START = 1; // Handler消息类型(开始下载)
    private final int DOWN_POSITION = 2; // Handler消息类型(下载位置)
    private final int DOWN_COMPLETE = 3; // Handler消息类型(下载完成)
    private final int DOWN_ERROR = 4; // Handler消息类型(下载失败)
    private OnDownloadListener onDownloadListener;

    public void setOnDownloadListener(OnDownloadListener onDownloadListener) {
        this.onDownloadListener = onDownloadListener;
    }

    /**
     * 下载文件
     *
     * @param url      文件路径
     * @param filepath 保存地址
     */
    public void download(String url, String filepath) {
        MyRunnable mr = new MyRunnable();
        mr.url = url;
        mr.filepath = filepath;
        new Thread(mr).start();
    }

    @SuppressWarnings("unused")
    private void sendMsg(int what) {
        sendMsg(what, null);
    }

    private void sendMsg(int what, Object mess) {
        Message m = myHandler.obtainMessage();
        m.what = what;
        m.obj = mess;
        m.sendToTarget();
    }

    Handler myHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case DOWN_START: // 开始下载
                    int filesize = (Integer) msg.obj;
                    onDownloadListener.onDownloadConnect(filesize);
                    break;
                case DOWN_POSITION: // 下载位置
                    int pos = (Integer) msg.obj;
                    onDownloadListener.onDownloadUpdate(pos);
                    break;
                case DOWN_COMPLETE: // 下载完成
                    String url = (String) msg.obj;
                    onDownloadListener.onDownloadComplete(url);
                    break;
                case DOWN_ERROR: // 下载失败
                    Exception e = (Exception) msg.obj;
                    e.printStackTrace();
                    onDownloadListener.onDownloadError(e);
                    break;
            }
            super.handleMessage(msg);
        }
    };

    class MyRunnable implements Runnable {
        private String url = "";
        private String filepath = "";

        @Override
        public void run() {
            try {
                doDownloadTheFile(url, filepath);
            } catch (Exception e) {
                sendMsg(DOWN_ERROR, e);
            }
        }
    }

    /**
     * 下载文件
     *
     * @param url      下载路劲
     * @param filepath 保存路径
     * @throws Exception
     */
    private void doDownloadTheFile(String url, String filepath) throws Exception {
        if (!URLUtil.isNetworkUrl(url)) {
            sendMsg(DOWN_ERROR, new Exception("不是有效的下载地址:" + url));
            return;
        }
        URL myUrl = new URL(url);
        URLConnection conn = myUrl.openConnection();
        conn.connect();
        InputStream is = null;
        int filesize = 0;
        try {
            is = conn.getInputStream();
            filesize = conn.getContentLength();// 根据响应获取文件大小
            sendMsg(DOWN_START, filesize);
        } catch (Exception e) {
            sendMsg(DOWN_ERROR, new Exception(new Exception("无法获取文件")));
            return;
        }
        FileOutputStream fos = new FileOutputStream(filepath); // 创建写入文件内存流,
        // 通过此流向目标写文件
        byte buf[] = new byte[1024];
        int numread = 0;
        int temp = 0;
        while ((numread = is.read(buf)) != -1) {
            fos.write(buf, 0, numread);
            fos.flush();
            temp += numread;
            sendMsg(DOWN_POSITION, temp);
        }
        is.close();
        fos.close();
        sendMsg(DOWN_COMPLETE, filepath);
    }

    interface OnDownloadListener{
        public void onDownloadUpdate(int percent);

        public void onDownloadError(Exception e);

        public void onDownloadConnect(int filesize);

        public void onDownloadComplete(Object result);
    }
}

2、调用DonwloadUtils进行下载,下载完成后加载本地图片

//1. 下载gif图片(文件名自定义可以通过Hash值作为key)
DownloadUtils downloadUtils = new DownloadUtils();
downloadUtils.download(gifUrlArray[0],
                getDiskCacheDir(getContext())+"/0.gif");
//2. 下载完毕后通过“GifDrawable”进行显示
downloadUtils.setOnDownloadListener(new DownloadUtils.OnDownloadListener() {
            @Override
            public void onDownloadUpdate(int percent) {
            }
            @Override
            public void onDownloadError(Exception e) {
            }
            @Override
            public void onDownloadConnect(int filesize) {
            }
            //下载完毕后进行显示
            @Override
            public void onDownloadComplete(Object result) {
                try {
                    GifDrawable gifDrawable = new GifDrawable(getDiskCacheDir(getContext())+"/0.gif");
                    mGifOnlineImageView.setImageDrawable(gifDrawable);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
//获取缓存的路径
public String getDiskCacheDir(Context context) {
    String cachePath = null;
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
            || !Environment.isExternalStorageRemovable()) {
        // 路径:/storage/emulated/0/Android/data/<application package>/cache
        cachePath = context.getExternalCacheDir().getPath();
    } else {
        // 路径:/data/data/<application package>/cache
        cachePath = context.getCacheDir().getPath();
    }
    return cachePath;
}

Gif动画控制

GifDrawable实现了Animatable和MediaPlayerControl的接口,所以我们可以使用它们的方法,如:
stop() - 停止动画, 可以在任何线程调用
start() - 开始动画, 可以在任何线程调用
isRunning() - 返回动画是否正在运行
reset() - rewinds the animation, does not restart stopped one
setSpeed(float factor) - 设置动画的播放速度, 例如: 传入参数 2.0f 将会2倍速度播放
seekTo(int position) - 跳转到指定的位置播放
getDuration() - 返回动画的总时间
getCurrentPosition() - 返回当前已经播放的时间
recycle() - 释放内存和Bitmap的作用方法一样
isRecycled() - 检查是否已经被回收
getError() - 返回最新的错误细节

获取Gif的元数据

GifDrawable提供以下方法,用于获取Gif的元数据:
getLoopCount() - 返回循环的次数
getNumberOfFrames() - 返回动画的总帧数(最小为1)
getComment() - 返回注释文本(如果没有返回null)
getFrameByteCount() - 返回可用于存储每帧的像素的最小字节数
getAllocationByteCount() - 返回分配给用于存储gifdrawable像素的内存字节数
getInputSourceByteCount() - 返回支持输入数据的字节数
toString() - 返回关于图像大小和帧数量(用于调试目的)

获取Gif某一帧的Bitmap图片

主要分为2步:
1.获取gif文件含有的总帧数 :使用GifDrawable 的getNumberOfFrames();既可以获取gif的总帧数。
2.在获取gif总帧数后,如果我们要获取gif某一帧的Bitmap:使用GifDrawble 的seekToFrameAndGet(index)方法,index就是需要获取的第多少帧的索引index。
方法如下:

public static Bitmap getBitmapArrayByGif(Context context, String assertPath, int index) {
    try {
        ArrayList<Bitmap> list = new ArrayList<>();
        GifDrawable gifFromAssets = new GifDrawable(context.getAssets(), assertPath);//代表android中assert的gif文件名
        int totalCount = gifFromAssets.getNumberOfFrames();
        if (totalCount < index) {
            index = totalCount - 1;
        }
        return gifFromAssets.seekToFrameAndGet(index);
    } catch (Exception e) {
        return null;
}

使用MediaPlayerControl控制Gif动画的播放

private GifImageView mGifImageView;

    @Override
    protected void onCreate (Bundle savedInstanceState) {
        mGifImageView = (GifImageView) findViewById(R.id.activity_gif_giv);
        try {
            GifDrawable gifDrawable = new GifDrawable(getAssets(), "anim.gif");
            mGifImageView.setImageDrawable(gifDrawable);
            final MediaController mediaController = new MediaController(this);
            mediaController.setMediaPlayer((GifDrawable) mGifImageView.getDrawable());
            /**
             * 也许你会像我一样,当看到上面一行代码时会纳闷,为什么setMediaPalyer传入的参数会是一个
             * GifDrawable对象呢,它需要的参数类型是MediaPlayerControl。。。
             * 还记得我们前面提到GifDrawable实现了MediaPlayerControl接口嘛!!!
             */
            mediaController.setAnchorView(mGifImageView);
            mGifImageView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mediaController.show();
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

一个GifDrawable实例被多个GifImageView关联,那么通常只胡最后一个GifImageView会播放动画,而前面 的GifImageView只会显示第一帧

为了解决这个问题multicallback出场了:

MultiCallback multiCallback = new MultiCallback();
        try {
            final GifDrawable gifDrawable = new GifDrawable(getAssets(), "anim.gif");
            mGifImageView.setImageDrawable(gifDrawable);
            multiCallback.addView(mGifImageView);

            mGifImageView2.setImageDrawable(gifDrawable);
            multiCallback.addView(mGifImageView2);
            gifDrawable.setCallback(multiCallback);
        } catch (IOException e) {
            e.printStackTrace();
}

这样两个动画都动起来了。MultiCallBack还可以同时控制两个GifImageView,比如,动画播放5秒后,全部停止:

multiCallback.scheduleDrawable(gifDrawable, new Runnable() {
           @Override
           public void run() {
              gifDrawable.stop();
           }
        }, 5 * 1000);