Android PicturePlayerView 基于TextureView的图片播放器

前言

人生的第一篇技术文章,干了几年程序员了,从来都是看人家的,这几天突然萌发了写文章的念头,文笔不行请多多见谅。

现在做直播都需要做大礼物,然后UI扔给我一堆图,要我放起来。最先想到的就是直接播放Gif,但是发现Android上没有专门播放Gif的控件,我又找Gilde,发现是可以播放了但是特别卡,要知道UI给我的图有好几MB全是高清的,还不能压缩丢色彩,最后没办法只能自己写了。

代码已上传至Github

好吧,我现在发现了lottie-android,简直是痛哭流涕%>_<%

**首先附上2张效果图,虽然我无耻的引用了lottie的Logo **


底不透明版

底透明版

正文

一开始做这个控件的时候我用的是SurfaceView,但是我发现我无法将它放到中间的某一层,因为它拥有独立的绘图表面,所以最终选用了TextureView,需要注意的是TextureView必须在硬件加速开启的窗口中。如果你对它不熟悉的话可以参考《Android TextureView简易教程》

首先看一些关键的方法
  • setOpaque(boolean):该方法用于设置TextureView是否不透明。
  • lockCanvas():锁定画布,如果在不解除锁定的情况下再次调用将返回null
  • unlockCanvasAndPost(Canvas):解锁画布同时提交,在这句执行完之后才可以再次调用lockCanvas()
  • canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint):将Bitmap画到画布上,srcdst作用就是将bitmap里的src区域画到canvas里的dst区域。
  • canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR):这句的作用是清空画布,也许你可以在View的onDraw()里试试这句,你会发现整个APP都是黑的o(╯□╰)o。
接下来让我们先实现一个最简单的构造
//这是一个最简单的构造,然而它什么都做不了,当然我们可以把它盖到任何层的上面,因为它是透明的
public class PicturePlayerView extends TextureView implements TextureView.SurfaceTextureListener {

    public PicturePlayerView(Context context) {
        this(context, null);
    }

    public PicturePlayerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PicturePlayerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        setOpaque(false);//设置背景透明,记住这里是[是否不透明]
        setSurfaceTextureListener(this);//设置监听
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        //当TextureView初始化时调用,事实上当你的程序退到后台它会被销毁,你再次打开程序的时候它会被重新初始化
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
        //当TextureView的大小改变时调用
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        //当TextureView被销毁时调用
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        //当TextureView更新时调用,也就是当我们调用unlockCanvasAndPost方法时
    }
}

现在我们创建一个了PicturePlayerView,接下来我们需要考虑如何将图片绘制到TextureView上。

将图片绘制到TextureView需要分2步走
  • 第一步:读取图片到内存中
  • 第二步:将内存中的图片画到画布上,这里在画完之后需要释放Bitmap
    首先实现第一步:这里提供2种方法,为了方便在下面的代码中将采用第二种,从Assets读取图片
//从本地读取图片,这里的path必须是绝对地址
private Bitmap readBitmap(String path) throws IOException {
    return BitmapFactory.decodeFile(path);
}
//从Assets读取图片
private Bitmap readBitmap(String path) throws IOException {
    return BitmapFactory.decodeStream(getResources().getAssets().open(path));
}

然后是第二步:

//将图片画到画布上,图片将被以宽度为比例画上去
private void drawBitmap(Bitmap bitmap) {
    Canvas canvas = lockCanvas(new Rect(0, 0, getWidth(), getHeight()));//锁定画布
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空画布
    Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
    Rect dst = new Rect(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
    canvas.drawBitmap(bitmap, src, dst, mPaint);//将bitmap画到画布上
    unlockCanvasAndPost(canvas);//解锁画布同时提交
}

好了现在我们知道怎么读取图片和怎么将图片画到画布上,但实际上我们拥有的是一组图片,并且在实际中需要将它们在一定时间内以一定的间隔播放出来。

很明显TextureView比起正常的View的优势就是可以在异步将图片画到画布上,我们可以创建一个异步线程,然后通过SystemClock.sleep()这个函数在每画完一帧都暂停一定时间,这样就实现了一个完整的过程。

完整代码请看PicturePlayerView1

public class PicturePlayerView extends TextureView implements TextureView.SurfaceTextureListener {

    private Paint mPaint;//画笔
    private Rect mSrcRect;
    private Rect mDstRect;

    private int mPlayFrame;//当前播放到那一帧,总帧数相关

    private String[] mPaths;//图片绝对地址集合
    private int mFrameCount;//总帧数
    private long mDelayTime;//播放帧间隔

    private PlayThread mPlayThread;

    //... 省略构造方法

    private void init() {
        setOpaque(false);//设置背景透明,记住这里是[是否不透明]

        setSurfaceTextureListener(this);//设置监听

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//创建画笔
        mSrcRect = new Rect();
        mDstRect = new Rect();
    }

    //... 省略SurfaceTextureListener的方法

    //开始播放
    @Override
    public void start(String[] paths, long duration) {
        this.mPaths = paths;
        this.mFrameCount = paths.length;
        this.mDelayTime = duration / mFrameCount;

        //开启线程
        mPlayThread = new PlayThread();
        mPlayThread.start();
    }

    private class PlayThread extends Thread {
        @Override
        public void run() {
            try {
                while (mPlayFrame < mFrameCount) {//如果还没有播放完所有帧
                    Bitmap bitmap = readBitmap(mPaths[mPlayFrame]);
                    drawBitmap(bitmap);
                    recycleBitmap(bitmap);
                    mPlayFrame++;
                    SystemClock.sleep(mDelayTime);//暂停间隔时间
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private Bitmap readBitmap(String path) throws IOException {
        return BitmapFactory.decodeStream(getResources().getAssets().open(path));
    }

    private void drawBitmap(Bitmap bitmap) {
        Canvas canvas = lockCanvas();//锁定画布
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空画布
        mSrcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());//这里我将2个rect抽离出去,防止重复创建
        mDstRect.set(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
        canvas.drawBitmap(bitmap, mSrcRect, mDstRect, mPaint);//将bitmap画到画布上
        unlockCanvasAndPost(canvas);//解锁画布同时提交
    }

    private static void recycleBitmap(Bitmap bitmap) {
        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
        }
    }
}

上面的代码实现一个完整的播放过程,但实际运行起来会有一定的问题,如果你运行过就会发现Fps只有15-16帧,与我们要求的25帧相差甚远。

原因就在于

  1. 我们没有计算readBitmap()等方法消耗的时间。
  • sleep()实际上是不精准的。

第一点我们之后再解决,先说第二点,实际上在第一次开发这个控件的时候我用的也是sleep(),但是后来发现它跳动的幅度很大。

比如我们以25帧为例,那么每帧的时间间隔应该为40ms,但在实际运行中它有可能阻塞30ms,也有可能是50ms,当然也有可能是40ms,一开始我也不明白,后来看到了这篇文章《Sleep函数的真正用意》,我才明白在非实时系统中是不可能有方法能完全精准的阻塞的

之后我通过查看ValueAnimator的源码最终发现了Choreographer,这里可以参考《Choreographer源码解析》,研究了部分源码后我发现它是利用Handler.sendMessageAtTime(long uptimeMillis)这个函数来控制时间,这个函数接收一个时间函数,通过SystemClock.uptimeMillis()获得,事实上我们在Handler调用的send()函数大部分最终都会走到sendMessageAtTime()方法。

在这里有一篇非常好的文章《聊一聊Android的消息机制》,它里面就写明了。

Looper关心的细节

  1. 如果消息队列里目前没有合适的消息可以摘取,那么不能让它所属的线程“傻转”,而应该使之阻塞。
  • 队列里的消息应该按其“到时”的顺序进行排列,最先到时的消息会放在队头,也就是mMessages域所指向的消息,其后的消息依次排开。
  • 阻塞的时间最好能精确一点儿,所以如果暂时没有合适的消息节点可摘时,要考虑链表首个消息节点将在什么时候到时,所以这个消息节点距离当前时刻的时间差,就是我们要阻塞的时长。
  • 有时候外界希望队列能在即将进入阻塞状态之前做一些动作,这些动作可以称为idle动作,我们需要兼顾处理这些idle动作。一个典型的例子是外界希望队列在进入阻塞之前做一次垃圾收集。

看了这篇文章我是茅塞顿开,事实上我通过测试发现,同样是40ms,sendMessageAtTime()能保证间隔在39ms-41ms之间(正常情况下,如果手机卡顿就说不准了
),所以我自己实现了一个Scheduler,这里我就不展开了,就讲下实现步骤,有兴趣可以直接看源码。

具体步骤为

  1. 创建一个Thread
  • 初始化Looper,这里可以直接继承HandlerThread
  • 创建一个Handler
  • 通过SystemClock.uptimeMillis()取得时间,然后向Handler发送消息。
  • 接收到消息后判断是否结束,如果未结束则将当前的时间加上间隔时间(比如40ms)后继续发送消息,不断进行循环过程。
通过sendMessageAtTime()实现的播放器

完整代码请看PicturePlayerView2

public class PicturePlayerView extends TextureView implements TextureView.SurfaceTextureListener {

    private Paint mPaint;//画笔
    private Rect mSrcRect;
    private Rect mDstRect;

    private String[] mPaths;//图片绝对地址集合

    private Scheduler mScheduler;

    //... 省略构造方法

    private void init() {
        setOpaque(false);//设置背景透明,记住这里是[是否不透明]

        setSurfaceTextureListener(this);//设置监听

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//创建画笔
        mSrcRect = new Rect();
        mDstRect = new Rect();
    }

    //... 省略SurfaceTextureListener的方法

    //开始播放
    @Override
    public void start(String[] paths, long duration) {
        this.mPaths = paths;

        //开启线程
        mScheduler = new Scheduler(duration, paths.length,
                new FrameUpdateListener());
        mScheduler.start();
    }

    private Bitmap readBitmap(String path) throws IOException {
        return BitmapFactory.decodeStream(getResources().getAssets().open(path));
    }

    private class FrameUpdateListener implements OnFrameUpdateListener {
        @Override
        public void onFrameUpdate(long frameIndex) {
            try {
                Bitmap bitmap = readBitmap(mPaths[(int) frameIndex]);
                drawBitmap(bitmap);
                recycleBitmap(bitmap);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void drawBitmap(Bitmap bitmap) {
        Canvas canvas = lockCanvas();//锁定画布
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空画布
        mSrcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());//这里我将2个rect抽离出去,防止重复创建
        mDstRect.set(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
        canvas.drawBitmap(bitmap, mSrcRect, mDstRect, mPaint);//将bitmap画到画布上
        unlockCanvasAndPost(canvas);//解锁画布同时提交
    }

    private static void recycleBitmap(Bitmap bitmap) {
        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
        }
    }
}

现在我们通过sendMessageAtTime()解决了第二步,如果你运行过Demo就会发现,现在已经在大部分情况下都能保持在25fps左右。

好了,现在我们可以来解决第一个问题,尽管现在在大部分情况下能保持25fps,但是如果机子较差,或者运行程序过多,你会发现还是不能保持25fps,当然如果机子实在太卡,连drawBitmap()这一步所花费的时间都要超过40ms,那是实在没有任何办法,但如果应该尽量去除多余的花费,让时间尽可能的让给drawBitmap()

我们要如何做呢?很明显我们需要新建一个线程将readBitmap()移到新线程中执行,然后通过一个缓存数组(多线程之间需要加锁)进行交互。

分离线程实现

完整代码请看PicturePlayerView3

public class PicturePlayerView3 extends TextureView implements TextureView.SurfaceTextureListener {
    private static final int MAX_CACHE_NUMBER = 12;//这是代表读取最大缓存帧数,因为一张图片的大小有width*height*4这么大,内存吃不消

    private Paint mPaint;//画笔
    private Rect mSrcRect;
    private Rect mDstRect;

    private List<Bitmap> mCacheBitmaps;//缓存帧集合

    private int mReadFrame;//当前读取到那一帧,总帧数相关

    private String[] mPaths;//图片绝对地址集合
    private int mFrameCount;//总帧数

    private ReadThread mReadThread;
    private Scheduler mScheduler;

    //... 省略构造方法

    private void init() {
        setOpaque(false);//设置背景透明,记住这里是[是否不透明]

        setSurfaceTextureListener(this);//设置监听

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//创建画笔
        mSrcRect = new Rect();
        mDstRect = new Rect();

        mCacheBitmaps = Collections.synchronizedList(new ArrayList<Bitmap>());//多线程需要加锁
    }

    //... 省略SurfaceTextureListener的方法

    //开始播放
    @Override
    public void start(String[] paths, long duration) {
        this.mPaths = paths;
        this.mFrameCount = paths.length;

        //开启线程
        mReadThread = new ReadThread();
        mReadThread.start();
        mScheduler = new Scheduler(duration, mFrameCount,
                new FrameUpdateListener());
    }

    private class ReadThread extends Thread {
        @Override
        public void run() {
            try {
                while (mReadFrame < mFrameCount) {//并且没有读完则继续读取
                    if (mCacheBitmaps.size() >= MAX_CACHE_NUMBER) {//如果读取的超过最大缓存则暂停读取
                        SystemClock.sleep(1);
                        continue;
                    }

                    Bitmap bmp = readBitmap(mPaths[mReadFrame]);
                    mCacheBitmaps.add(bmp);

                    mReadFrame++;

                    if (mReadFrame == 1) {//读取到第一帧后在开始调度器
                        mScheduler.start();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private Bitmap readBitmap(String path) throws IOException {
        return BitmapFactory.decodeStream(getResources().getAssets().open(path));
    }

    private class FrameUpdateListener implements OnFrameUpdateListener {
        @Override
        public void onFrameUpdate(long frameIndex) {
            if (mCacheBitmaps.isEmpty()) {//如果当前没有帧,则直接跳过
                return;
            }

            Bitmap bitmap = mCacheBitmaps.remove(0);//获取第一帧同时从缓存里删除
            drawBitmap(bitmap);
            recycleBitmap(bitmap);
        }
    }

    private void drawBitmap(Bitmap bitmap) {
        Canvas canvas = lockCanvas();//锁定画布
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空画布
        mSrcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());//这里我将2个rect抽离出去,防止重复创建
        mDstRect.set(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
        canvas.drawBitmap(bitmap, mSrcRect, mDstRect, mPaint);//将bitmap画到画布上
        unlockCanvasAndPost(canvas);//解锁画布同时提交
    }

    private static void recycleBitmap(Bitmap bitmap) {
        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
        }
    }
}

到现在为至我们已经成功的实现了一个图片播放器。

但是你以为已经已经结束了吗?

怎么可能!!!

事实上现在还有一个比较严重的问题,这个问题在很大程度上会影响整个app的性能。
这个问题就是内存抖动,什么是内存抖动?

如果你对内存抖动不了解的话,可以通过《Android App解决卡顿慢之内存抖动及内存泄漏(发现和定位)》或者Google的官方文档翻译《Android性能优化典范》了解。

我引用文章的一句话

  • 内存抖动是指在短时间内有大量的对象被创建或者被回收的现象。

意思就是你在循环中或者onDraw()被频繁运行的方法中去创建对象,结果导致频繁的gc,而gc会导致线程卡顿,如果你在onDraw()或者onLayout()方法中去创建对象,AS应该会提示你(Avoid object allocations during draw/layout operations (preallocate and reuse instead))。

我们可以通过一张图来观察它到它的现象,通过这张图可以很清楚的看到中间那些锯齿。


内存抖动

现在我们需要着手解决这个问题,如何解决?通过上面2篇文章我们可以知道要解决这个问题必须尽可能的减少创建对象,去复用之前已经创建的对象,这一点我们可以通过创建对象池解决,可是我们要如何才能复用Bitmap?

其实Google已经给出了解决方案《Managing Bitmap Memory》或者你可以看这个知乎的回答《Android Bitmap inBitmap 图片复用?》

在BitmapFactory.Options对象中有个inBitmap属性,如果你设置inBitmap等于某个Bitmap(当然这里有限制,上面的文章已经讲的很清楚了),你在用这个BitmapFactory.Options去加载Bitmap,它就会复用这块内存,如果这个Bitmap在绘制中,你有可能会看见撕裂现象。
我们要做的就是创建一个Bitmap对象池,将已经画完的Bitmap放回对象池,当我们要读取的时候,从对象池中获取合适的对象赋予inBitmap

最终效果如下,我们可以明显的看到锯齿已经消失,整个播放过程内存都很平滑。


内存平滑.jpg
现在我们要开始实现,先看下BitmapFactory.Options里我们使用的主要属性
  • inBitmap:如果该值不等于空,则在解码时重新使用这个Bitmap。
  • inMutable:Bitmap是否可变的,如果设置了inBitmap,该值必须为true
  • inPreferredConfig:指定解码颜色格式。
  • inJustDecodeBounds:如果设置为true,将不会将图片加载到内存中,但是可以获得宽高。
  • inSampleSize:图片缩放的倍数,如果设置为2代表加载到内存中的图片大小为原来的2分之一,这个值总是和inJustDecodeBounds配合来加载大图片,在这里我直接设置为1,这样做实际上是有问题的,如果图片过大很容易发生OOM。
readBitmap方法修改如下
private Bitmap readBitmap(String path) throws IOException {
    InputStream is = getResources().getAssets().open(path);//这里需要以流的形式读取
    BitmapFactory.Options options = getReusableOptions(is);//获取参数设置
    Bitmap bmp = BitmapFactory.decodeStream(is, null, options);
    is.close();
    return bmp;
}

//实现复用,
private BitmapFactory.Options getReusableOptions(InputStream is) throws IOException {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
    options.inSampleSize = 1;
    options.inJustDecodeBounds = true;//这里设置为不将图片读取到内存中
    is.mark(is.available());
    BitmapFactory.decodeStream(is, null, options);//获得大小
    options.inJustDecodeBounds = false;//设置回来
    is.reset();
    Bitmap inBitmap = getBitmapFromReusableSet(options);
    options.inMutable = true;
    if (inBitmap != null) {//如果有符合条件的设置属性
        options.inBitmap = inBitmap;
    }
    return options;
}

//从复用池中寻找合适的bitmap
private Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
    if (mReusableBitmaps.isEmpty()) {
        return null;
    }
    int count = mReusableBitmaps.size();
    for (int i = 0; i < count; i++) {
        Bitmap item = mReusableBitmaps.get(i);
        if (ImageUtil.canUseForInBitmap(item, options)) {//寻找符合条件的bitmap
            return mReusableBitmaps.remove(i);
        }
    }
    return null;
}

上面的ImageUtil是一个工具类,用于判断是否符合。

然后我们将这段代码替换上去。

完整代码请看PicturePlayerView4

public class PicturePlayerView3 extends TextureView implements TextureView.SurfaceTextureListener {

    private static final int MAX_CACHE_NUMBER = 12;//这是代表读取最大缓存帧数,因为一张图片的大小有width*height*4这么大,内存吃不消
    private static final int MAX_REUSABLE_NUMBER = MAX_CACHE_NUMBER / 2;//这是代表读取最大复用帧数

    private Paint mPaint;//画笔
    private Rect mSrcRect;
    private Rect mDstRect;

    private List<Bitmap> mCacheBitmaps;//缓存帧集合
    private List<Bitmap> mReusableBitmaps;

    private int mReadFrame;//当前读取到那一帧,总帧数相关

    private String[] mPaths;//图片绝对地址集合
    private int mFrameCount;//总帧数

    private ReadThread mReadThread;
    private Scheduler mScheduler;

    //... 省略构造方法

    private void init() {
        setOpaque(false);//设置背景透明,记住这里是[是否不透明]

        setSurfaceTextureListener(this);//设置监听

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//创建画笔
        mSrcRect = new Rect();
        mDstRect = new Rect();

        //多线程需要加锁
        mCacheBitmaps = Collections.synchronizedList(new ArrayList<Bitmap>());
        mReusableBitmaps = Collections.synchronizedList(new ArrayList<Bitmap>());
    }

    //... 省略SurfaceTextureListener的方法

    //开始播放
    @Override
    public void start(String[] paths, long duration) {
        this.mPaths = paths;
        this.mFrameCount = paths.length;

        //开启线程
        mReadThread = new ReadThread();
        mReadThread.start();
        mScheduler = new Scheduler(duration, mFrameCount,
                new FrameUpdateListener(),
                new FrameListener());
    }

    private class ReadThread extends Thread {
        @Override
        public void run() {
            try {
                while (mReadFrame < mFrameCount) {//并且没有读完则继续读取
                    if (mCacheBitmaps.size() >= MAX_REUSABLE_NUMBER) {//如果读取的超过最大缓存则暂停读取
                        SystemClock.sleep(1);
                        continue;
                    }

                    Bitmap bmp = readBitmap(mPaths[mReadFrame]);
                    mCacheBitmaps.add(bmp);

                    mReadFrame++;

                    if (mReadFrame == 1) {//读取到第一帧后在开始调度器
                        mScheduler.start();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private Bitmap readBitmap(String path) throws IOException {
        InputStream is = getResources().getAssets().open(path);//这里需要以流的形式读取
        BitmapFactory.Options options = getReusableOptions(is);//获取参数设置
        Bitmap bmp = BitmapFactory.decodeStream(is, null, options);
        is.close();
        return bmp;
    }

    //实现复用
    private BitmapFactory.Options getReusableOptions(InputStream is) throws IOException {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        options.inSampleSize = 1;
        options.inJustDecodeBounds = true;//这里设置为不将图片读取到内存中
        is.mark(is.available());
        BitmapFactory.decodeStream(is, null, options);//获得大小
        options.inJustDecodeBounds = false;//设置回来
        is.reset();
        Bitmap inBitmap = getBitmapFromReusableSet(options);
        options.inMutable = true;
        if (inBitmap != null) {//如果有符合条件的设置属性
            options.inBitmap = inBitmap;
        }
        return options;
    }

    //从复用池中寻找合适的bitmap
    private Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
        if (mReusableBitmaps.isEmpty()) {
            return null;
        }
        int count = mReusableBitmaps.size();
        for (int i = 0; i < count; i++) {
            Bitmap item = mReusableBitmaps.get(i);
            if (ImageUtil.canUseForInBitmap(item, options)) {//寻找符合条件的bitmap
                return mReusableBitmaps.remove(i);
            }
        }
        return null;
    }

    private void addReusable(Bitmap bitmap) {
        if (mReusableBitmaps.size() >= MAX_REUSABLE_NUMBER) {//如果超过则将其释放
            recycleBitmap(mReusableBitmaps.remove(0));
        }
        mReusableBitmaps.add(bitmap);
    }

    private class FrameUpdateListener implements OnFrameUpdateListener {
        @Override
        public void onFrameUpdate(long frameIndex) {
            if (mCacheBitmaps.isEmpty()) {//如果当前没有帧,则直接跳过
                return;
            }

            Bitmap bitmap = mCacheBitmaps.get(0);//获取第一帧
            drawBitmap(bitmap);

            addReusable(mCacheBitmaps.remove(0));//必须在画完之后在删除,不然会出现画面撕裂
        }
    }

    //当播放线程停止时回调,用处是结束时释放Bitmap
    private class FrameListener extends OnSimpleFrameListener {

        @Override
        public void onStop() {
            try {
                mReadThread.join();//等待播放线程结束
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int count = mCacheBitmaps.size();
            for (int i = 0; i < count; i++) {
                ImageUtil.recycleBitmap(mCacheBitmaps.get(i));
            }
            mCacheBitmaps.clear();
            count = mReusableBitmaps.size();
            for (int i = 0; i < count; i++) {
                ImageUtil.recycleBitmap(mReusableBitmaps.get(i));
            }
            mReusableBitmaps.clear();
        }
    }

    private void drawBitmap(Bitmap bitmap) {
        Canvas canvas = lockCanvas();//锁定画布
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空画布
        mSrcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());//这里我将2个rect抽离出去,防止重复创建
        mDstRect.set(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
        canvas.drawBitmap(bitmap, mSrcRect, mDstRect, mPaint);//将bitmap画到画布上
        unlockCanvasAndPost(canvas);//解锁画布同时提交
    }

    private static void recycleBitmap(Bitmap bitmap) {
        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
        }
    }
}

结尾

事实上到这里文章就已经结束了,我们可以回顾下步骤。

  1. 绘制图片
  • 异步绘制一组图片
  • 使用Handler.sendMessageAtTime()替代SystemClock.sleep(),使动画更流畅
  • 分离线程,尽可能的将时间交给绘制这一步
  • 解决内存抖动问题

核心基本都在这里了,其实还有一些其他的附加功能,比如暂停恢复播放、循环播放,当然它们都不是重点我就不写了,这些都在PicturePlayerView,有兴趣可以研究下。

当然,我也想研究下用GLTextureView实现下,看看效率会不会更高。

题外话

以前看别人写的文章都以为会挺轻松,真正自己写起来才发现真的是难,这篇文章写的也不尽我满意,真的,如果大家有什么建议或者意见都一定要提出来。

下一篇文章我可能会写关于图片操作控件(我可能会分为一系列),类似StickerView的控件,当然我和他用ImageView实现的方式会有些不一样。

最后,希望希望下篇文章能有所进步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容