前言
人生的第一篇技術(shù)文章,干了幾年程序員了,從來都是看人家的,這幾天突然萌發(fā)了寫文章的念頭,文筆不行請多多見諒。
現(xiàn)在做直播都需要做大禮物,然后UI扔給我一堆圖,要我放起來。最先想到的就是直接播放Gif,但是發(fā)現(xiàn)Android上沒有專門播放Gif的控件,我又找Gilde,發(fā)現(xiàn)是可以播放了但是特別卡,要知道UI給我的圖有好幾MB全是高清的,還不能壓縮丟色彩,最后沒辦法只能自己寫了。
代碼已上傳至Github
好吧,我現(xiàn)在發(fā)現(xiàn)了lottie-android,簡直是痛哭流涕%>_<%
**首先附上2張效果圖,雖然我無恥的引用了lottie的Logo **


正文
一開始做這個控件的時候我用的是SurfaceView,但是我發(fā)現(xiàn)我無法將它放到中間的某一層,因為它擁有獨立的繪圖表面,所以最終選用了TextureView,需要注意的是TextureView必須在硬件加速開啟的窗口中。如果你對它不熟悉的話可以參考《Android TextureView簡易教程》。
首先看一些關(guān)鍵的方法
- setOpaque(boolean):該方法用于設(shè)置TextureView是否不透明。
-
lockCanvas():鎖定畫布,如果在不解除鎖定的情況下再次調(diào)用將返回
null。 -
unlockCanvasAndPost(Canvas):解鎖畫布同時提交,在這句執(zhí)行完之后才可以再次調(diào)用
lockCanvas()。 -
canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint):將Bitmap畫到畫布上,
src和dst作用就是將bitmap里的src區(qū)域畫到canvas里的dst區(qū)域。 -
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR):這句的作用是清空畫布,也許你可以在View的
onDraw()里試試這句,你會發(fā)現(xiàn)整個APP都是黑的o(╯□╰)o。
接下來讓我們先實現(xiàn)一個最簡單的構(gòu)造
//這是一個最簡單的構(gòu)造,然而它什么都做不了,當然我們可以把它蓋到任何層的上面,因為它是透明的
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);//設(shè)置背景透明,記住這里是[是否不透明]
setSurfaceTextureListener(this);//設(shè)置監(jiān)聽
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
//當TextureView初始化時調(diào)用,事實上當你的程序退到后臺它會被銷毀,你再次打開程序的時候它會被重新初始化
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
//當TextureView的大小改變時調(diào)用
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
//當TextureView被銷毀時調(diào)用
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
//當TextureView更新時調(diào)用,也就是當我們調(diào)用unlockCanvasAndPost方法時
}
}
現(xiàn)在我們創(chuàng)建一個了PicturePlayerView,接下來我們需要考慮如何將圖片繪制到TextureView上。
將圖片繪制到TextureView需要分2步走
- 第一步:讀取圖片到內(nèi)存中
- 第二步:將內(nèi)存中的圖片畫到畫布上,這里在畫完之后需要釋放
Bitmap
首先實現(xiàn)第一步:這里提供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);//解鎖畫布同時提交
}
好了現(xiàn)在我們知道怎么讀取圖片和怎么將圖片畫到畫布上,但實際上我們擁有的是一組圖片,并且在實際中需要將它們在一定時間內(nèi)以一定的間隔播放出來。
很明顯TextureView比起正常的View的優(yōu)勢就是可以在異步將圖片畫到畫布上,我們可以創(chuàng)建一個異步線程,然后通過SystemClock.sleep()這個函數(shù)在每畫完一幀都暫停一定時間,這樣就實現(xiàn)了一個完整的過程。
完整代碼請看PicturePlayerView1
public class PicturePlayerView extends TextureView implements TextureView.SurfaceTextureListener {
private Paint mPaint;//畫筆
private Rect mSrcRect;
private Rect mDstRect;
private int mPlayFrame;//當前播放到那一幀,總幀數(shù)相關(guān)
private String[] mPaths;//圖片絕對地址集合
private int mFrameCount;//總幀數(shù)
private long mDelayTime;//播放幀間隔
private PlayThread mPlayThread;
//... 省略構(gòu)造方法
private void init() {
setOpaque(false);//設(shè)置背景透明,記住這里是[是否不透明]
setSurfaceTextureListener(this);//設(shè)置監(jiān)聽
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//創(chuàng)建畫筆
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抽離出去,防止重復創(chuàng)建
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();
}
}
}
上面的代碼實現(xiàn)一個完整的播放過程,但實際運行起來會有一定的問題,如果你運行過就會發(fā)現(xiàn)Fps只有15-16幀,與我們要求的25幀相差甚遠。
原因就在于
- 我們沒有計算
readBitmap()等方法消耗的時間。
-
sleep()實際上是不精準的。
第一點我們之后再解決,先說第二點,實際上在第一次開發(fā)這個控件的時候我用的也是sleep(),但是后來發(fā)現(xiàn)它跳動的幅度很大。
比如我們以25幀為例,那么每幀的時間間隔應該為40ms,但在實際運行中它有可能阻塞30ms,也有可能是50ms,當然也有可能是40ms,一開始我也不明白,后來看到了這篇文章《Sleep函數(shù)的真正用意》,我才明白在非實時系統(tǒng)中是不可能有方法能完全精準的阻塞的。
之后我通過查看ValueAnimator的源碼最終發(fā)現(xiàn)了Choreographer,這里可以參考《Choreographer源碼解析》,研究了部分源碼后我發(fā)現(xiàn)它是利用Handler.sendMessageAtTime(long uptimeMillis)這個函數(shù)來控制時間,這個函數(shù)接收一個時間函數(shù),通過SystemClock.uptimeMillis()獲得,事實上我們在Handler調(diào)用的send()函數(shù)大部分最終都會走到sendMessageAtTime()方法。
在這里有一篇非常好的文章《聊一聊Android的消息機制》,它里面就寫明了。
Looper關(guān)心的細節(jié)
- 如果消息隊列里目前沒有合適的消息可以摘取,那么不能讓它所屬的線程“傻轉(zhuǎn)”,而應該使之阻塞。
- 隊列里的消息應該按其“到時”的順序進行排列,最先到時的消息會放在隊頭,也就是mMessages域所指向的消息,其后的消息依次排開。
- 阻塞的時間最好能精確一點兒,所以如果暫時沒有合適的消息節(jié)點可摘時,要考慮鏈表首個消息節(jié)點將在什么時候到時,所以這個消息節(jié)點距離當前時刻的時間差,就是我們要阻塞的時長。
- 有時候外界希望隊列能在即將進入阻塞狀態(tài)之前做一些動作,這些動作可以稱為idle動作,我們需要兼顧處理這些idle動作。一個典型的例子是外界希望隊列在進入阻塞之前做一次垃圾收集。
看了這篇文章我是茅塞頓開,事實上我通過測試發(fā)現(xiàn),同樣是40ms,sendMessageAtTime()能保證間隔在39ms-41ms之間(正常情況下,如果手機卡頓就說不準了
),所以我自己實現(xiàn)了一個Scheduler,這里我就不展開了,就講下實現(xiàn)步驟,有興趣可以直接看源碼。
具體步驟為
- 創(chuàng)建一個
Thread。
- 初始化
Looper,這里可以直接繼承HandlerThread。 - 創(chuàng)建一個
Handler。 - 通過
SystemClock.uptimeMillis()取得時間,然后向Handler發(fā)送消息。 - 接收到消息后判斷是否結(jié)束,如果未結(jié)束則將當前的時間加上間隔時間(比如40ms)后繼續(xù)發(fā)送消息,不斷進行循環(huán)過程。
通過sendMessageAtTime()實現(xiàn)的播放器
完整代碼請看PicturePlayerView2
public class PicturePlayerView extends TextureView implements TextureView.SurfaceTextureListener {
private Paint mPaint;//畫筆
private Rect mSrcRect;
private Rect mDstRect;
private String[] mPaths;//圖片絕對地址集合
private Scheduler mScheduler;
//... 省略構(gòu)造方法
private void init() {
setOpaque(false);//設(shè)置背景透明,記住這里是[是否不透明]
setSurfaceTextureListener(this);//設(shè)置監(jiān)聽
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//創(chuàng)建畫筆
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抽離出去,防止重復創(chuàng)建
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();
}
}
}
現(xiàn)在我們通過sendMessageAtTime()解決了第二步,如果你運行過Demo就會發(fā)現(xiàn),現(xiàn)在已經(jīng)在大部分情況下都能保持在25fps左右。
好了,現(xiàn)在我們可以來解決第一個問題,盡管現(xiàn)在在大部分情況下能保持25fps,但是如果機子較差,或者運行程序過多,你會發(fā)現(xiàn)還是不能保持25fps,當然如果機子實在太卡,連drawBitmap()這一步所花費的時間都要超過40ms,那是實在沒有任何辦法,但如果應該盡量去除多余的花費,讓時間盡可能的讓給drawBitmap()。
我們要如何做呢?很明顯我們需要新建一個線程將readBitmap()移到新線程中執(zhí)行,然后通過一個緩存數(shù)組(多線程之間需要加鎖)進行交互。
分離線程實現(xiàn)
完整代碼請看PicturePlayerView3
public class PicturePlayerView3 extends TextureView implements TextureView.SurfaceTextureListener {
private static final int MAX_CACHE_NUMBER = 12;//這是代表讀取最大緩存幀數(shù),因為一張圖片的大小有width*height*4這么大,內(nèi)存吃不消
private Paint mPaint;//畫筆
private Rect mSrcRect;
private Rect mDstRect;
private List<Bitmap> mCacheBitmaps;//緩存幀集合
private int mReadFrame;//當前讀取到那一幀,總幀數(shù)相關(guān)
private String[] mPaths;//圖片絕對地址集合
private int mFrameCount;//總幀數(shù)
private ReadThread mReadThread;
private Scheduler mScheduler;
//... 省略構(gòu)造方法
private void init() {
setOpaque(false);//設(shè)置背景透明,記住這里是[是否不透明]
setSurfaceTextureListener(this);//設(shè)置監(jiān)聽
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//創(chuàng)建畫筆
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) {//并且沒有讀完則繼續(xù)讀取
if (mCacheBitmaps.size() >= MAX_CACHE_NUMBER) {//如果讀取的超過最大緩存則暫停讀取
SystemClock.sleep(1);
continue;
}
Bitmap bmp = readBitmap(mPaths[mReadFrame]);
mCacheBitmaps.add(bmp);
mReadFrame++;
if (mReadFrame == 1) {//讀取到第一幀后在開始調(diào)度器
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抽離出去,防止重復創(chuàng)建
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();
}
}
}
到現(xiàn)在為至我們已經(jīng)成功的實現(xiàn)了一個圖片播放器。
但是你以為已經(jīng)已經(jīng)結(jié)束了嗎?
怎么可能?。?!
事實上現(xiàn)在還有一個比較嚴重的問題,這個問題在很大程度上會影響整個app的性能。
這個問題就是內(nèi)存抖動,什么是內(nèi)存抖動?
如果你對內(nèi)存抖動不了解的話,可以通過《Android App解決卡頓慢之內(nèi)存抖動及內(nèi)存泄漏(發(fā)現(xiàn)和定位)》或者Google的官方文檔翻譯《Android性能優(yōu)化典范》了解。
我引用文章的一句話
- 內(nèi)存抖動是指在短時間內(nèi)有大量的對象被創(chuàng)建或者被回收的現(xiàn)象。
意思就是你在循環(huán)中或者onDraw()被頻繁運行的方法中去創(chuàng)建對象,結(jié)果導致頻繁的gc,而gc會導致線程卡頓,如果你在onDraw()或者onLayout()方法中去創(chuàng)建對象,AS應該會提示你(Avoid object allocations during draw/layout operations (preallocate and reuse instead))。
我們可以通過一張圖來觀察它到它的現(xiàn)象,通過這張圖可以很清楚的看到中間那些鋸齒。

現(xiàn)在我們需要著手解決這個問題,如何解決?通過上面2篇文章我們可以知道要解決這個問題必須盡可能的減少創(chuàng)建對象,去復用之前已經(jīng)創(chuàng)建的對象,這一點我們可以通過創(chuàng)建對象池解決,可是我們要如何才能復用Bitmap?
其實Google已經(jīng)給出了解決方案《Managing Bitmap Memory》或者你可以看這個知乎的回答《Android Bitmap inBitmap 圖片復用?》。
在BitmapFactory.Options對象中有個inBitmap屬性,如果你設(shè)置inBitmap等于某個Bitmap(當然這里有限制,上面的文章已經(jīng)講的很清楚了),你在用這個BitmapFactory.Options去加載Bitmap,它就會復用這塊內(nèi)存,如果這個Bitmap在繪制中,你有可能會看見撕裂現(xiàn)象。
我們要做的就是創(chuàng)建一個Bitmap對象池,將已經(jīng)畫完的Bitmap放回對象池,當我們要讀取的時候,從對象池中獲取合適的對象賦予inBitmap。
最終效果如下,我們可以明顯的看到鋸齒已經(jīng)消失,整個播放過程內(nèi)存都很平滑。

現(xiàn)在我們要開始實現(xiàn),先看下BitmapFactory.Options里我們使用的主要屬性
- inBitmap:如果該值不等于空,則在解碼時重新使用這個Bitmap。
-
inMutable:Bitmap是否可變的,如果設(shè)置了
inBitmap,該值必須為true。 - inPreferredConfig:指定解碼顏色格式。
-
inJustDecodeBounds:如果設(shè)置為
true,將不會將圖片加載到內(nèi)存中,但是可以獲得寬高。 -
inSampleSize:圖片縮放的倍數(shù),如果設(shè)置為2代表加載到內(nèi)存中的圖片大小為原來的2分之一,這個值總是和
inJustDecodeBounds配合來加載大圖片,在這里我直接設(shè)置為1,這樣做實際上是有問題的,如果圖片過大很容易發(fā)生OOM。
readBitmap方法修改如下
private Bitmap readBitmap(String path) throws IOException {
InputStream is = getResources().getAssets().open(path);//這里需要以流的形式讀取
BitmapFactory.Options options = getReusableOptions(is);//獲取參數(shù)設(shè)置
Bitmap bmp = BitmapFactory.decodeStream(is, null, options);
is.close();
return bmp;
}
//實現(xiàn)復用,
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;//這里設(shè)置為不將圖片讀取到內(nèi)存中
is.mark(is.available());
BitmapFactory.decodeStream(is, null, options);//獲得大小
options.inJustDecodeBounds = false;//設(shè)置回來
is.reset();
Bitmap inBitmap = getBitmapFromReusableSet(options);
options.inMutable = true;
if (inBitmap != null) {//如果有符合條件的設(shè)置屬性
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;//這是代表讀取最大緩存幀數(shù),因為一張圖片的大小有width*height*4這么大,內(nèi)存吃不消
private static final int MAX_REUSABLE_NUMBER = MAX_CACHE_NUMBER / 2;//這是代表讀取最大復用幀數(shù)
private Paint mPaint;//畫筆
private Rect mSrcRect;
private Rect mDstRect;
private List<Bitmap> mCacheBitmaps;//緩存幀集合
private List<Bitmap> mReusableBitmaps;
private int mReadFrame;//當前讀取到那一幀,總幀數(shù)相關(guān)
private String[] mPaths;//圖片絕對地址集合
private int mFrameCount;//總幀數(shù)
private ReadThread mReadThread;
private Scheduler mScheduler;
//... 省略構(gòu)造方法
private void init() {
setOpaque(false);//設(shè)置背景透明,記住這里是[是否不透明]
setSurfaceTextureListener(this);//設(shè)置監(jiān)聽
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//創(chuàng)建畫筆
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) {//并且沒有讀完則繼續(xù)讀取
if (mCacheBitmaps.size() >= MAX_REUSABLE_NUMBER) {//如果讀取的超過最大緩存則暫停讀取
SystemClock.sleep(1);
continue;
}
Bitmap bmp = readBitmap(mPaths[mReadFrame]);
mCacheBitmaps.add(bmp);
mReadFrame++;
if (mReadFrame == 1) {//讀取到第一幀后在開始調(diào)度器
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);//獲取參數(shù)設(shè)置
Bitmap bmp = BitmapFactory.decodeStream(is, null, options);
is.close();
return bmp;
}
//實現(xiàn)復用
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;//這里設(shè)置為不將圖片讀取到內(nèi)存中
is.mark(is.available());
BitmapFactory.decodeStream(is, null, options);//獲得大小
options.inJustDecodeBounds = false;//設(shè)置回來
is.reset();
Bitmap inBitmap = getBitmapFromReusableSet(options);
options.inMutable = true;
if (inBitmap != null) {//如果有符合條件的設(shè)置屬性
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));//必須在畫完之后在刪除,不然會出現(xiàn)畫面撕裂
}
}
//當播放線程停止時回調(diào),用處是結(jié)束時釋放Bitmap
private class FrameListener extends OnSimpleFrameListener {
@Override
public void onStop() {
try {
mReadThread.join();//等待播放線程結(jié)束
} 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抽離出去,防止重復創(chuàng)建
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();
}
}
}
結(jié)尾
事實上到這里文章就已經(jīng)結(jié)束了,我們可以回顧下步驟。
- 繪制圖片
- 異步繪制一組圖片
- 使用
Handler.sendMessageAtTime()替代SystemClock.sleep(),使動畫更流暢 - 分離線程,盡可能的將時間交給繪制這一步
- 解決內(nèi)存抖動問題
核心基本都在這里了,其實還有一些其他的附加功能,比如暫?;謴筒シ?、循環(huán)播放,當然它們都不是重點我就不寫了,這些都在PicturePlayerView,有興趣可以研究下。
當然,我也想研究下用GLTextureView實現(xiàn)下,看看效率會不會更高。
題外話
以前看別人寫的文章都以為會挺輕松,真正自己寫起來才發(fā)現(xiàn)真的是難,這篇文章寫的也不盡我滿意,真的,如果大家有什么建議或者意見都一定要提出來。
下一篇文章我可能會寫關(guān)于圖片操作控件(我可能會分為一系列),類似StickerView的控件,當然我和他用ImageView實現(xiàn)的方式會有些不一樣。
最后,希望希望下篇文章能有所進步。