Android PicturePlayerView 基于TextureView的圖片播放器

前言

人生的第一篇技術(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畫到畫布上,srcdst作用就是將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幀相差甚遠。

原因就在于

  1. 我們沒有計算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é)

  1. 如果消息隊列里目前沒有合適的消息可以摘取,那么不能讓它所屬的線程“傻轉(zhuǎn)”,而應該使之阻塞。
  • 隊列里的消息應該按其“到時”的順序進行排列,最先到時的消息會放在隊頭,也就是mMessages域所指向的消息,其后的消息依次排開。
  • 阻塞的時間最好能精確一點兒,所以如果暫時沒有合適的消息節(jié)點可摘時,要考慮鏈表首個消息節(jié)點將在什么時候到時,所以這個消息節(jié)點距離當前時刻的時間差,就是我們要阻塞的時長。
  • 有時候外界希望隊列能在即將進入阻塞狀態(tài)之前做一些動作,這些動作可以稱為idle動作,我們需要兼顧處理這些idle動作。一個典型的例子是外界希望隊列在進入阻塞之前做一次垃圾收集。

看了這篇文章我是茅塞頓開,事實上我通過測試發(fā)現(xiàn),同樣是40ms,sendMessageAtTime()能保證間隔在39ms-41ms之間(正常情況下,如果手機卡頓就說不準了
),所以我自己實現(xiàn)了一個Scheduler,這里我就不展開了,就講下實現(xiàn)步驟,有興趣可以直接看源碼。

具體步驟為

  1. 創(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)象,通過這張圖可以很清楚的看到中間那些鋸齒。


內(nèi)存抖動

現(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)存都很平滑。


內(nèi)存平滑.jpg
現(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é)束了,我們可以回顧下步驟。

  1. 繪制圖片
  • 異步繪制一組圖片
  • 使用Handler.sendMessageAtTime()替代SystemClock.sleep(),使動畫更流暢
  • 分離線程,盡可能的將時間交給繪制這一步
  • 解決內(nèi)存抖動問題

核心基本都在這里了,其實還有一些其他的附加功能,比如暫?;謴筒シ?、循環(huán)播放,當然它們都不是重點我就不寫了,這些都在PicturePlayerView,有興趣可以研究下。

當然,我也想研究下用GLTextureView實現(xiàn)下,看看效率會不會更高。

題外話

以前看別人寫的文章都以為會挺輕松,真正自己寫起來才發(fā)現(xiàn)真的是難,這篇文章寫的也不盡我滿意,真的,如果大家有什么建議或者意見都一定要提出來。

下一篇文章我可能會寫關(guān)于圖片操作控件(我可能會分為一系列),類似StickerView的控件,當然我和他用ImageView實現(xiàn)的方式會有些不一樣。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容