前言
人生的第一篇技术文章,干了几年程序员了,从来都是看人家的,这几天突然萌发了写文章的念头,文笔不行请多多见谅。
现在做直播都需要做大礼物,然后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画到画布上,
src
和dst
作用就是将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帧相差甚远。
原因就在于
- 我们没有计算
readBitmap()
等方法消耗的时间。
-
sleep()
实际上是不精准的。
第一点我们之后再解决,先说第二点,实际上在第一次开发这个控件的时候我用的也是sleep()
,但是后来发现它跳动的幅度很大。
比如我们以25帧为例,那么每帧的时间间隔应该为40ms,但在实际运行中它有可能阻塞30ms,也有可能是50ms,当然也有可能是40ms,一开始我也不明白,后来看到了这篇文章《Sleep函数的真正用意》,我才明白在非实时系统中是不可能有方法能完全精准的阻塞的。
之后我通过查看ValueAnimator的源码最终发现了Choreographer
,这里可以参考《Choreographer源码解析》,研究了部分源码后我发现它是利用Handler.sendMessageAtTime(long uptimeMillis)
这个函数来控制时间,这个函数接收一个时间函数,通过SystemClock.uptimeMillis()
获得,事实上我们在Handler
调用的send()
函数大部分最终都会走到sendMessageAtTime()
方法。
在这里有一篇非常好的文章《聊一聊Android的消息机制》,它里面就写明了。
Looper关心的细节
- 如果消息队列里目前没有合适的消息可以摘取,那么不能让它所属的线程“傻转”,而应该使之阻塞。
- 队列里的消息应该按其“到时”的顺序进行排列,最先到时的消息会放在队头,也就是mMessages域所指向的消息,其后的消息依次排开。
- 阻塞的时间最好能精确一点儿,所以如果暂时没有合适的消息节点可摘时,要考虑链表首个消息节点将在什么时候到时,所以这个消息节点距离当前时刻的时间差,就是我们要阻塞的时长。
- 有时候外界希望队列能在即将进入阻塞状态之前做一些动作,这些动作可以称为idle动作,我们需要兼顾处理这些idle动作。一个典型的例子是外界希望队列在进入阻塞之前做一次垃圾收集。
看了这篇文章我是茅塞顿开,事实上我通过测试发现,同样是40ms,sendMessageAtTime()
能保证间隔在39ms-41ms之间(正常情况下,如果手机卡顿就说不准了
),所以我自己实现了一个Scheduler,这里我就不展开了,就讲下实现步骤,有兴趣可以直接看源码。
具体步骤为
- 创建一个
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
。
最终效果如下,我们可以明显的看到锯齿已经消失,整个播放过程内存都很平滑。
现在我们要开始实现,先看下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();
}
}
}
结尾
事实上到这里文章就已经结束了,我们可以回顾下步骤。
- 绘制图片
- 异步绘制一组图片
- 使用
Handler.sendMessageAtTime()
替代SystemClock.sleep()
,使动画更流畅 - 分离线程,尽可能的将时间交给绘制这一步
- 解决内存抖动问题
核心基本都在这里了,其实还有一些其他的附加功能,比如暂停恢复播放、循环播放,当然它们都不是重点我就不写了,这些都在PicturePlayerView,有兴趣可以研究下。
当然,我也想研究下用GLTextureView实现下,看看效率会不会更高。
题外话
以前看别人写的文章都以为会挺轻松,真正自己写起来才发现真的是难,这篇文章写的也不尽我满意,真的,如果大家有什么建议或者意见都一定要提出来。
下一篇文章我可能会写关于图片操作控件(我可能会分为一系列),类似StickerView的控件,当然我和他用ImageView实现的方式会有些不一样。
最后,希望希望下篇文章能有所进步。