2019-06-11

Android實(shí)現(xiàn)高性能的幀動(dòng)畫(huà)禮物播放效果

引言:我們都知道Android實(shí)現(xiàn)動(dòng)畫(huà)的常見(jiàn)方式有那么幾種矮嫉,比如屬性動(dòng)畫(huà)臊诊,值動(dòng)畫(huà)等俯渤,這些動(dòng)畫(huà)都能實(shí)現(xiàn)一定的動(dòng)畫(huà)效果,比如平移侨嘀,縮放和透明等臭挽,但是對(duì)于復(fù)雜的動(dòng)畫(huà)效果用這些去實(shí)現(xiàn)則顯得比較困難和麻煩,效果也不是很好咬腕,比如各種禮物動(dòng)畫(huà)等欢峰,這些動(dòng)畫(huà)肯定要求比較酷炫點(diǎn)的,而這些使用Android常規(guī)的動(dòng)畫(huà)是很難實(shí)現(xiàn)想要的效果的涨共。

如果要實(shí)現(xiàn)比較好的動(dòng)畫(huà)效果纽帖,實(shí)現(xiàn)方式有那么幾種:

  • Lottie:Lottie是Airbnb開(kāi)源的一個(gè)面向 iOS、Android举反、React Native 的動(dòng)畫(huà)庫(kù)懊直,可實(shí)現(xiàn)非常復(fù)雜的動(dòng)畫(huà),使用也及其簡(jiǎn)單火鼻,極大釋放人力室囊,值得一試。目前QQ禮物就是使用了這個(gè)魁索。Lottie
  • SVGA:這是YY開(kāi)源的一個(gè)動(dòng)畫(huà)框架融撞,占用資源少,動(dòng)畫(huà)文件大小也比較小粗蔚,集成很方便尝偎,目前虎牙直播等YY系在線上使用。SVGA
  • GIF動(dòng)畫(huà):GIF動(dòng)畫(huà)實(shí)現(xiàn)簡(jiǎn)單,對(duì)于APP端來(lái)說(shuō)直接用glide加載就可以了致扯,Gif圖實(shí)質(zhì)上就是把一幀幀的靜態(tài)圖片打包到一起肤寝,打成一個(gè)壓縮包,實(shí)際上這個(gè)壓縮包一點(diǎn)都不小抖僵,在播放性能和內(nèi)存控制上還是差好多鲤看。
  • 幀動(dòng)畫(huà):幀動(dòng)畫(huà)相比前面幾個(gè)來(lái)說(shuō)其實(shí)也不是最好的選擇,但是幀動(dòng)畫(huà)的好處是可以實(shí)現(xiàn)的非绸烧耄酷炫的動(dòng)畫(huà)效果刨摩,壞處是占用資源大寺晌,我們都知道圖片是最占用內(nèi)存的了世吨,使用幀動(dòng)畫(huà)相比前面幾個(gè)來(lái)說(shuō)占用內(nèi)存肯定是比較多的,這個(gè)就要求我們使用幀動(dòng)畫(huà)播放的時(shí)候要管理好內(nèi)存呻征,避免oom耘婚。

本來(lái)我是想使用Lottie動(dòng)畫(huà)或者SVGA實(shí)現(xiàn)動(dòng)畫(huà)效果的,畢竟占用內(nèi)存資源相當(dāng)少陆赋,一個(gè)文件可以說(shuō)才幾百K沐祷,而且實(shí)現(xiàn)方式都相當(dāng)簡(jiǎn)單,都封裝的很完善了攒岛,而且播放的禮物效果也還不錯(cuò)赖临,但是由于想要更好的動(dòng)畫(huà)效果,所以需要使用到幀動(dòng)畫(huà)灾锯,最后通過(guò)使用surfaceview實(shí)現(xiàn)了使用幀動(dòng)畫(huà)播放大量的圖片并且很好的控制了內(nèi)存兢榨,實(shí)現(xiàn)效果感覺(jué)不錯(cuò)所以就在這里做下總結(jié)和分享,參考顺饮。

效果:這里給了兩個(gè)播放效果吵聪,限制于素材,其中播放的星空?qǐng)D片都是從網(wǎng)絡(luò)上下載的兼雄,所以看起來(lái)不像第一個(gè)具有連貫性吟逝。

?

)

實(shí)現(xiàn):如果用Android實(shí)現(xiàn)幀動(dòng)畫(huà)的API去實(shí)現(xiàn)禮物幀動(dòng)畫(huà),則必定會(huì)出現(xiàn)內(nèi)存溢出赦肋,因?yàn)閳D片比較大块攒,比較占內(nèi)存,內(nèi)存不好控制佃乘,而且播放性能也不是很好囱井,因?yàn)槎Y物幀動(dòng)畫(huà)一般幀數(shù)都會(huì)比較多,禮物幀動(dòng)畫(huà)圖片是不可能放到項(xiàng)目目錄下的恕稠,不然圖片資源比較大會(huì)加大apk的大小而且也不利于擴(kuò)展琅绅。在這里使用SurfaceView去實(shí)現(xiàn)禮物幀動(dòng)畫(huà)功能,效果還不錯(cuò)鹅巍,內(nèi)存控制的也比較好千扶,主要利用了SurfaceView不與其宿主窗口共享同一個(gè)繪圖表面以及它的雙緩沖等功能料祠,詳細(xì)看下面對(duì)SurfaceView的介紹。

我們可以判斷有WiFi的情況下在后臺(tái)從網(wǎng)絡(luò)上下載圖片等資源保存到SDCard中澎羞,沒(méi)有WiFi網(wǎng)絡(luò)的情況下比如點(diǎn)擊禮物播放或者發(fā)送禮物等情況下去下載相對(duì)應(yīng)的動(dòng)畫(huà)壓縮文件再去解壓髓绽。然后從SDCard中讀取圖片資源,如果有音樂(lè)則可以在播放動(dòng)畫(huà)的時(shí)候同時(shí)播放音樂(lè)妆绞,這個(gè)可以根據(jù)音樂(lè)時(shí)間設(shè)置播放動(dòng)畫(huà)的時(shí)間從而保持一致(根據(jù)音樂(lè)時(shí)間計(jì)算出圖片每幀之間的間隔時(shí)間)顺呕。這邊我就不寫(xiě)下載解壓之類的代碼了,直接把禮物文件放到手機(jī)的SDCard中括饶,禮物文件這些可以看下圖株茶,或者在最下面點(diǎn)擊前往Github下載代碼,我會(huì)把這些文件放到項(xiàng)目目錄中(frameAnimation文件夾)图焰,大家如果下載代碼查看效果可以直接將這些文件所在的文件夾放到手機(jī)的SDCard中启盛。

SurfaceView: 這里我簡(jiǎn)單的介紹下SurfaceView,它不與其宿主窗口共享同一個(gè)繪圖表面技羔,我們都知道宿主窗口比如Activity中的view的繪制是層次結(jié)構(gòu)的僵闯,里面的任何一個(gè)view改變都會(huì)導(dǎo)致整個(gè)視圖結(jié)構(gòu)重繪一次,對(duì)于復(fù)雜的動(dòng)畫(huà)效果效率比較低下藤滥,而SurfaceView自身?yè)碛歇?dú)立的繪圖表面鳖粟,這樣SurfaceView的UI就可以在一個(gè)獨(dú)立的線程中進(jìn)行行繪制,SurfaceView的窗口刷新的時(shí)候不需要重繪應(yīng)用程序的窗口拙绊,可以不占用主線程的資源從而可以實(shí)現(xiàn)比較復(fù)雜的UI效果向图。

要了解 SurfaceView ,還須了解它的另外兩個(gè)組件:Surface 和 SurfaceHolder时呀,他們?nèi)咧g的關(guān)系實(shí)質(zhì)上就是 MVC张漂,Model就是數(shù)據(jù)模型的意思也就是這里的Surface;View即視圖也就是這里的SurfaceView谨娜;SurfaceHolder很明顯可以理解為Controller(控制器)航攒。在SurfaceView中你可以通過(guò)SurfaceHolder接口訪問(wèn)它內(nèi)部的surface,而我們執(zhí)行繪制的方法就是操作這個(gè) Surface內(nèi)部的Canvas趴梢,處理Canvas畫(huà)的效果和動(dòng)畫(huà)漠畜,大小,像素等坞靶,getHolder()方法可以得到這個(gè)SurfaceHolder憔狞,通過(guò)SurfaceHolder來(lái)控制surface的尺寸和格式,或者修改監(jiān)視surface的變化等等彰阴●遥可以通過(guò)SurfaceHolder.Callback來(lái)監(jiān)聽(tīng)Surface的生命周期。

雙緩沖:SurfaceView在更新視圖時(shí)用了兩個(gè)Canvas,一張frontCanvas和一張backCanvas簇抵,每次實(shí)際顯示的是frontCanvas庆杜,backCanvas存儲(chǔ)的是上一次更改前的視圖,當(dāng)使用lockCanvas()獲取畫(huà)布時(shí)碟摆,得到的實(shí)際上是backCanvas而不是正在顯示的frontCanvas晃财,當(dāng)你在獲取到的backCanvas上繪制完成后,再使用unlockCanvasAndPost(canvas)提交backCanvas視圖典蜕,那么這張backCanvas將替換正在顯示的frontCanvas被顯示出來(lái)断盛,原來(lái)的frontCanvas將切換到后臺(tái)作為backCanvas,這樣做的好處是在繪制期間不會(huì)出現(xiàn)黑屏愉舔。想要詳細(xì)了解可以查看SurfaceView 與view區(qū)別詳解 钢猛。

代碼流程分析

GiftFragmentAnimationDirector director = new GiftFragmentAnimationDirector();
mGiftFrameAnimation = director.createGiftFrameAnimation(new GiftFrameAnimationBuilder(mSurfaceView));

首先我這邊通過(guò)建造者模式創(chuàng)建一個(gè)類GiftFrameAnimation,這個(gè)類就是具體的功能實(shí)現(xiàn)類了屑宠,我們先來(lái)看看GiftFrameAnimationBuilder厢洞。

public class GiftFrameAnimationBuilder implements IBuildGiftFragmentAnimation {

    private final GiftFrameAnimation mGiftFrameAnimation;
    
    public GiftFrameAnimationBuilder(SurfaceView surfaceView) {
        //創(chuàng)建GiftFrameAnimation仇让,同時(shí)初始化SurfaceView
        mGiftFrameAnimation = new GiftFrameAnimation(surfaceView);
    }
    
    @Override
    public void buildSupportInBitmap() {
        //支持inBitmap典奉,改善內(nèi)存抖動(dòng)的問(wèn)題
        mGiftFrameAnimation.setSupportInBitmap(true);
    }
    
    @Override
    public void buildCacheCount() {
        //設(shè)置緩存圖片個(gè)數(shù)
        mGiftFrameAnimation.setCacheCount(5);
    }
    
    @Override
    public void buildScaleType() {
        //設(shè)置圖片的展現(xiàn)形式
        mGiftFrameAnimation.setScaleType(GiftFrameAnimation.SCALE_TYPE_CENTER_CROP);
    }
    
    @Override
    public void buildtRepeatMode() {
        // 設(shè)置圖片播放模式,播放一次或者重復(fù)播放
        mGiftFrameAnimation.setRepeatMode(GiftFrameAnimation.MODE_ONCE);
    }
    
    @Override
    public void buildFrameInterval() {
        //設(shè)置幀動(dòng)畫(huà)每幀之間的時(shí)間間隔
        mGiftFrameAnimation.setFrameInterval(50);
    }
    
    @Override
    public void buildMatrix() {
        // 給定繪制bitmap的matrix不能和設(shè)置ScaleType同時(shí)起作用
        //        mGiftFrameAnimation.setMatrix(null);
    }
    
    @Override
    public GiftFrameAnimation createGiftFragmentAnimation() {
        // 返回設(shè)置好的GiftFrameAnimation
        return mGiftFrameAnimation;
    }
}

在這里主要是對(duì)GiftFrameAnimation進(jìn)行了一些初始化丧叽,設(shè)置一些具體的屬性卫玖,比如初始化surfaceView、圖片播放的間隔時(shí)間踊淳,圖片的播放模式以及scaleType等假瞬,接下來(lái)我們就對(duì)這些配置具體分析。

public GiftFrameAnimation(SurfaceView surfaceView) {
    this.mSurfaceView = surfaceView;
    this.mSurfaceHolder = surfaceView.getHolder();
    mCallBack = new MyCallBack();
    mSurfaceHolder.setFormat(PixelFormat.TRANSLUCENT);
    mSurfaceView.setZOrderOnTop(true);
    mSurfaceHolder.addCallback(mCallBack);
    mBitmapCache = new SparseArray<>();
    mContext = surfaceView.getContext();
    mDrawMatrix = new Matrix();
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
}

這是在創(chuàng)建GiftFrameAnimation的同時(shí)初始化SurfaceView迂尝,主要是獲取SurfaceHolder脱茉、SurfaceHolder.Callback、將SurfaceView背景設(shè)置為透明以及初始化緩存集合類和用于進(jìn)行繪制Bitmap的Matrix和Paint垄开。

public void setSupportInBitmap(boolean support) {
    this.mSupportInBitmap = support;
}

這個(gè)是用來(lái)設(shè)置是否支持inBitmap琴许,支持inBitmap會(huì)非常顯著的改善內(nèi)存抖動(dòng)的問(wèn)題,因?yàn)榇嬖赽itmap復(fù)用的問(wèn)題溉躲,當(dāng)設(shè)置支持inBitmap時(shí)榜田,請(qǐng)務(wù)必保證幀動(dòng)畫(huà)所有的圖片分辨率和顏色位數(shù)完全一致。默認(rèn)為true锻梳,詳情可查看箭券。

public void setFrameInterval(int time) {
    this.mFrameInterval = time;
}

設(shè)置幀動(dòng)畫(huà)圖片之間的播放間隔時(shí)間, 如果給定了播放總時(shí)間mFrameTime不為0疑枯,則這個(gè)設(shè)置無(wú)效辩块,要按照計(jì)算mFrmeTime / pictureNumber得出。因?yàn)槲以谖募械膉son文件中指定了圖片的播放總時(shí)間,這是為了有背景音樂(lè)時(shí)播放動(dòng)畫(huà)時(shí)間和背景播放音樂(lè)時(shí)間保持一致废亭。

public void setCacheCount(int count) {
    this.mCacheCount = count;
}

這是設(shè)置緩存到內(nèi)存中的圖片的個(gè)數(shù)古今,便于圖片的快速加載繪制。

public void setMatrix(@NonNull Matrix matrix) {
    mDrawMatrix = matrix;
    mScaleType = SCALE_TYPE_MATRIX;
}

給定繪制bitmap的matrix滔以,不能和ScaleType同時(shí)起作用捉腥。

/**
 * 表示給定的matrix
 */
private final int SCALE_TYPE_MATRIX = 0;
/**
 * 完全拉伸,不保持原始圖片比例你画,鋪滿
 */
public static final int SCALE_TYPE_FIT_XY = 1;
/**
 * 保持原始圖片比例抵碟,整體拉伸圖片至少填充滿X或者Y軸的一個(gè)
 * 并最終依附在視圖的上方或者左方
 */
public static final int SCALE_TYPE_FIT_START = 2;
/**
 * 保持原始圖片比例,整體拉伸圖片至少填充滿X或者Y軸的一個(gè)
 * 并最終依附在視圖的中心
 */
public static final int SCALE_TYPE_FIT_CENTER = 3;
/**
 * 保持原始圖片比例坏匪,整體拉伸圖片至少填充滿X或者Y軸的一個(gè)
 * 并最終依附在視圖的下方或者右方
 */
public static final int SCALE_TYPE_FIT_END = 4;
/**
 * 將圖片置于視圖中央拟逮,不縮放
 */
public static final int SCALE_TYPE_CENTER = 5;
/**
 * 整體縮放圖片,保持原始比例适滓,將圖片置于視圖中央敦迄,
 * 確保填充滿整個(gè)視圖,超出部分將會(huì)被裁剪
 */
public static final int SCALE_TYPE_CENTER_CROP = 6;
/**
 * 整體縮放圖片凭迹,保持原始比例罚屋,將圖片置于視圖中央,
 * 確保X或者Y至少有一個(gè)填充滿屏幕
 */
public static final int SCALE_TYPE_CENTER_INSIDE = 7;

@IntDef({SCALE_TYPE_FIT_XY, SCALE_TYPE_FIT_START, SCALE_TYPE_FIT_CENTER, SCALE_TYPE_FIT_END,
        SCALE_TYPE_CENTER, SCALE_TYPE_CENTER_CROP, SCALE_TYPE_CENTER_INSIDE})
@Retention(RetentionPolicy.SOURCE)
public @interface ScaleType {
}

public void setScaleType(@ScaleType int type) {
    this.mScaleType = type;
}

這是設(shè)置圖片的scaleType嗅绸,這邊主要定義了8種選擇脾猛,主要是通過(guò)下面的代碼根據(jù)指定的scaleType配置繪制bitmap的Matrix實(shí)現(xiàn)的。

private void configureDrawMatrix(Bitmap bitmap) {
    final int srcWidth = bitmap.getWidth();
    final int srcHeight = bitmap.getHeight();
    final int dstWidth = mSurfaceView.getWidth();
    final int dstHeight = mSurfaceView.getHeight();
    final boolean nothingChanged = srcWidth == mLastFrameWidth
            && srcHeight == mLastFrameHeight
            && mLastFrameScaleType == mScaleType
            && mLastSurfaceWidth == dstWidth
            && mLastSurfaceHeight == dstHeight;
    if (nothingChanged) {
        return;
    }
    mLastFrameScaleType = mScaleType;
    mLastFrameWidth = srcWidth;
    mLastFrameHeight = srcHeight;
    mLastSurfaceWidth = dstWidth;
    mLastSurfaceHeight = dstHeight;
    if (mScaleType == SCALE_TYPE_MATRIX) {
        return;
    } else if (mScaleType == SCALE_TYPE_CENTER) {
        mDrawMatrix.setTranslate(
                Math.round((dstWidth - srcWidth) * 0.5f),
                Math.round((dstHeight - srcHeight) * 0.5f));
    } else if (mScaleType == SCALE_TYPE_CENTER_CROP) {
        float scale;
        float dx = 0, dy = 0;
        //按照高縮放
        if (dstHeight * srcWidth > dstWidth * srcHeight) {
            scale = (float) dstHeight / (float) srcHeight;
            dx = (dstWidth - srcWidth * scale) * 0.5f;
        } else {
            scale = (float) dstWidth / (float) srcWidth;
            dy = (dstHeight - srcHeight * scale) * 0.5f;
        }
        mDrawMatrix.setScale(scale, scale);
        mDrawMatrix.postTranslate(dx, dy);
    } else if (mScaleType == SCALE_TYPE_CENTER_INSIDE) {
        float scale;
        float dx;
        float dy;
        //小于dst時(shí)不縮放
        if (srcWidth <= dstWidth && srcHeight <= dstHeight) {
            scale = 1.0f;
        } else {
            scale = Math.min((float) dstWidth / (float) srcWidth,
                    (float) dstHeight / (float) srcHeight);
        }
        dx = Math.round((dstWidth - srcWidth * scale) * 0.5f);
        dy = Math.round((dstHeight - srcHeight * scale) * 0.5f);

        mDrawMatrix.setScale(scale, scale);
        mDrawMatrix.postTranslate(dx, dy);
    } else {
        RectF srcRect = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
        RectF dstRect = new RectF(0, 0, mSurfaceView.getWidth(), mSurfaceView.getHeight());
        mDrawMatrix.setRectToRect(srcRect, dstRect, MATRIX_SCALE_ARRAY[mScaleType - 1]);
    }
}

private final Matrix.ScaleToFit[] MATRIX_SCALE_ARRAY = {
        Matrix.ScaleToFit.FILL,
        Matrix.ScaleToFit.START,
        Matrix.ScaleToFit.CENTER,
        Matrix.ScaleToFit.END
};

上面大致是介紹了一下GiftFrameAnimation一些設(shè)置鱼鸠,下面就來(lái)主要介紹下代碼功能實(shí)現(xiàn)的整體的流程分析猛拴。

mGiftFrameAnimation.start(giftId, 0);

public void start(String giftId, int startFramePositon) {
    if (mCallBack.isDrawing) {
        stop();
    }
    mPathList = getPathList(giftId);
    if (mPathList == null || mPathList.size() == 0) {
        return;
    }
    mTotalCount = mPathList.size();
    //緩存圖片個(gè)數(shù)不能超過(guò)總圖片數(shù)
    if (mCacheCount > mTotalCount) {
        mCacheCount = mTotalCount;
    }
    mStartFramePositon = startFramePositon;
    if (mStartFramePositon >= mTotalCount) {
        mStartFramePositon = 0;
    }
    startDecodeThread();
}

這是開(kāi)始播放幀動(dòng)畫(huà),第一個(gè)參數(shù)giftId是獲取圖片文件的位置的標(biāo)志蚀狰,比如我保存在SD卡上的圖片文件名1000愉昆,那么我將giftId參數(shù)設(shè)為1000,則會(huì)去讀取這個(gè)文件夾下的圖片進(jìn)行播放麻蹋,下面我都指定之歌第二個(gè)參數(shù)播放開(kāi)始的圖片跛溉,比如傳0,也就是表示從第一個(gè)圖片開(kāi)始播放哥蔚。

private List<String> getPathList(String giftId) {
    List<String> list = new ArrayList<>();
    GiftModel giftModel = GainGiftUtils.getGiftModel(giftId);
    if (giftModel == null) {
        if (mAnimationStateListener != null) {
            mAnimationStateListener.giftFileNotExists();
        }
        return list;
    }
    String backgroundColor = giftModel.backgroundColor;
    String musicPath = giftModel.backgroundMusic;
    list = giftModel.imageArray;
    int size = list.size();
    mFrmeTime = giftModel.playTimeMillisecond;
    if (mFrmeTime == 0) {
        mFrmeTime = mFrameInterval * size;
        // 為了避免幀動(dòng)畫(huà)的播放跟背景音樂(lè)時(shí)長(zhǎng)不一致倒谷,故禁掉音樂(lè)播放
        musicPath = null;
    } else {
        //根據(jù)json文件中給定的播放時(shí)間計(jì)算每幀圖片之間的播放間隔時(shí)間
        mFrameInterval = mFrmeTime / size;
    }
    if (mAnimationStateListener != null) {
        mAnimationStateListener.giftFrameType(musicPath, mFrmeTime, backgroundColor);
    }
    return list;
}

這個(gè)是獲取保存在SD卡上的圖片的路徑的集合,首先通過(guò)讀取1000.json文件中的json糙箍,然后進(jìn)行判斷json中所記錄的文件是否存在渤愁,可解析成GiftModel,這時(shí)可以獲取圖片路徑集合深夯、音樂(lè)文件路徑抖格,背景顏色以及播放總時(shí)間诺苹。

public class GiftModel {
    public String backgroundMusic;
    public int playTimeMillisecond;
    public String backgroundColor;
    public List<String> imageArray;
}

接下來(lái)我們?cè)賮?lái)看下startDecodeThread

private void startDecodeThread() {
    new Thread() {
        @Override
        public void run() {
            super.run();
            Looper.prepare();
            mDecodeHandler = new Handler(Looper.myLooper()) {
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    if (msg.what == STOP_ANIMATION) {
                        decodeBitmap(STOP_ANIMATION);
                        getLooper().quit();
                        return;
                    }
                    decodeBitmap(msg.what);
                }
            };
            decodeBitmap(START_ANIMATION);
            Looper.loop();
        }
    }.start();
}

這里開(kāi)啟了一個(gè)線程去處理, 根據(jù)不同指令 進(jìn)行不同操作雹拄,START_ANIMATION表示開(kāi)始動(dòng)畫(huà)標(biāo)志收奔,STOP_ANIMATION表示動(dòng)畫(huà)結(jié)束標(biāo)志,至于decodeBitmap(msg.what)則是用來(lái)處理繼續(xù)緩存下個(gè)Bitmap的滓玖。

private void decodeBitmap(int position) {
    if (position == START_ANIMATION) {
        //初始化存儲(chǔ)
        if (mSupportInBitmap) {
            mOptions = new BitmapFactory.Options();
            mOptions.inMutable = true;
            mOptions.inSampleSize = 1;
        }
        for (int i = mStartFramePositon; i < mCacheCount + mStartFramePositon; i++) {
            int putPosition = i;
            if (putPosition > mTotalCount - 1) {
                putPosition = putPosition % mTotalCount;
            }
            mBitmapCache.put(putPosition, decodeBitmapReal(mPathList.get(putPosition)));
        }
        mCallBack.startAnim();
    } else if (position == STOP_ANIMATION) {
        mCallBack.stopAnim();
    } else if (mPlayMode == MODE_ONCE) {
        if (position + mCacheCount <= mTotalCount - 1) {
            //由于surface的雙緩沖坪哄,不能直接復(fù)用上一幀的bitmap,因?yàn)樯弦粠腷itmap可能還沒(méi)有post
            writeInBitmap(position);
            mBitmapCache.put(position + mCacheCount, decodeBitmapReal(mPathList.get(position + mCacheCount)));
        }
        //循環(huán)播放
    } else if (mPlayMode == MODE_INFINITE) {
        //由于surface的雙緩沖势篡,不能直接復(fù)用上一幀的bitmap翩肌,上一幀的bitmap可能還沒(méi)有post
        writeInBitmap(position);
        //播放到尾部時(shí),取mod
        if (position + mCacheCount > mTotalCount - 1) {
            mBitmapCache.put((position + mCacheCount) % mTotalCount, decodeBitmapReal(mPathList.get((position + mCacheCount) % mTotalCount)));
        } else {
            mBitmapCache.put(position + mCacheCount, decodeBitmapReal(mPathList.get(position + mCacheCount)));
        }
    }
}

先來(lái)看看START_ANIMATION開(kāi)始播放動(dòng)畫(huà)這塊禁悠,mStartFramePositon就是我們一開(kāi)始調(diào)用start的時(shí)候傳進(jìn)來(lái)的播放開(kāi)始位置念祭,這邊根據(jù)設(shè)置的緩存?zhèn)€數(shù)對(duì)圖片進(jìn)行了mBitmapCache緩存,然后調(diào)用了mCallBack.startAnim()碍侦,我們進(jìn)入看看

private void startAnim() {
    if (mAnimationStateListener != null) {
        mAnimationStateListener.onStart();
    }
    isDrawing = true;
    position = mStartFramePositon;
    //繪制線程
    drawThread = new Thread() {
        @Override
        public void run() {
            super.run();
            while (isDrawing) {
                try {
                    long now = System.currentTimeMillis();
                    drawBitmap();
                    //控制兩幀之間的間隔
                    sleep(mFrameInterval - (System.currentTimeMillis() - now) > 0 ? mFrameInterval - (System.currentTimeMillis() - now) : 0);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            }
        }
    };
    drawThread.start();
}

可以看到這里開(kāi)啟了一個(gè)線程去執(zhí)行drawBitmap()粱坤,這里就是圖片的進(jìn)行繪制的方法了

private void drawBitmap() {
    //當(dāng)循環(huán)播放時(shí),獲取真實(shí)的position
    if (mPlayMode == MODE_INFINITE && position >= mTotalCount) {
        position = position % mTotalCount;
    }
    if (position >= mTotalCount) {
        mDecodeHandler.sendEmptyMessage(STOP_ANIMATION);
        clearSurface();
        return;
    }
    if (mBitmapCache.get(position, null) == null) {
        stopAnim();
        return;
    }
    final Bitmap currentBitmap = mBitmapCache.get(position);
    mDecodeHandler.sendEmptyMessage(position);
    mCanvas = mSurfaceHolder.lockCanvas();
    if (mCanvas == null) {
        return;
    }
    mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
    mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
    configureDrawMatrix(currentBitmap);
    mCanvas.drawBitmap(currentBitmap, mDrawMatrix, mPaint);
    mSurfaceHolder.unlockCanvasAndPost(mCanvas);
    position++;
}

這里首先獲取了播放的圖片位置position瓷产,首先判斷了當(dāng)前的播放模式是不是MODE_INFINITE重復(fù)播放站玄,如果是,則計(jì)算出當(dāng)前的播放位置拦英,避免超出圖片數(shù)蜒什,當(dāng)position大約等于圖片總數(shù)mTotalCount時(shí),則表示播放完畢了疤估,這時(shí)通過(guò)mDecodeHandler.sendEmptyMessage(STOP_ANIMATION)發(fā)送了一個(gè)停止動(dòng)畫(huà)標(biāo)志,這個(gè)在startDecodeThread()中進(jìn)行處理霎冯,也就是上面的decodeBitmap的position == STOP_ANIMATION铃拇;然后又發(fā)送了一個(gè)mDecodeHandler.sendEmptyMessage(position),也就是decodeBitmap(msg.what)通知緩存下一個(gè)圖片沈撞。

再來(lái)看看繪制過(guò)程慷荔,通過(guò)mSurfaceHolder.lockCanvas()獲取Surface的Canvas后,SurfaceView會(huì)利用Surface的一個(gè)同步鎖鎖住畫(huà)布Canvas缠俺,直到調(diào)用unlockCanvasAndPost(Canvas canvas)函數(shù)显晶,才解鎖畫(huà)布并提交改變,將圖形顯示壹士,這里的同步機(jī)制保證Surface的Canvas在繪制過(guò)程中不會(huì)被改變(被摧毀磷雇、修改),避免多個(gè)不同的線程同時(shí)操作同一個(gè)Canvas對(duì)象躏救。

我們往前看decodeBitmap()方法唯笙,接下來(lái)我們繼續(xù)看看position == STOP_ANIMATION螟蒸,也就是停止播放動(dòng)畫(huà),這里調(diào)用了mCallBack.stopAnim()崩掘。

private void stopAnim() {
    if (!isDrawing) {
        return;
    }
    isDrawing = false;
    position = 0;
    mBitmapCache.clear();
    clearSurface();
    if (mDecodeHandler != null) {
        mDecodeHandler.sendEmptyMessage(STOP_ANIMATION);
    }
    if (drawThread != null) {
        drawThread.interrupt();
    }
    if (mAnimationStateListener != null) {
        mAnimationStateListener.onFinish();
    }
    mInBitmapFlag = 0;
    mInBitmap = null;
}

private void clearSurface() {
    try {
        mCanvas = mSurfaceHolder.lockCanvas();
        if (mCanvas != null) {
            mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
            mSurfaceHolder.unlockCanvasAndPost(mCanvas);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

停止播放動(dòng)畫(huà)這里主要是清除緩存七嫌,終止繪制線程,調(diào)用clearSurface()獲取mCanvas清理畫(huà)布苞慢。

在加載幀動(dòng)畫(huà)的同時(shí)會(huì)播放背景音樂(lè)诵原,這里附上加載播放音樂(lè)文件的MediaManager類。

public class MediaManager {
    private static MediaPlayer mPlayer;

    private static boolean isPause;

    public static void playSound(String filePathString, OnCompletionListener onCompletionListener) {
        // TODO Auto-generated method stub
        if (mPlayer == null) {
            mPlayer = new MediaPlayer();
            //保險(xiǎn)起見(jiàn)挽放,設(shè)置報(bào)錯(cuò)監(jiān)聽(tīng)
            mPlayer.setOnErrorListener(new OnErrorListener() {

                @Override
                public boolean onError(MediaPlayer mp, int what, int extra) {
                    // TODO Auto-generated method stub
                    mPlayer.reset();
                    return false;
                }
            });
        } else {
            mPlayer.reset();//就回復(fù)
        }

        try {
            mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mPlayer.setOnCompletionListener(onCompletionListener);
            mPlayer.setDataSource(filePathString);
            mPlayer.prepare();
            mPlayer.start();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (SecurityException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalStateException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public static void stop(){
        if (mPlayer != null && mPlayer.isPlaying()) {
            mPlayer.stop();
        }
    }

    //停止函數(shù)
    public static void pause() {
        if (mPlayer != null && mPlayer.isPlaying()) {
            mPlayer.pause();
            isPause = true;
        }
    }

    //繼續(xù)
    public static void resume() {
        if (mPlayer != null && isPause) {
            mPlayer.start();
            isPause = false;
        }
    }


    public static void release() {
        if (mPlayer != null) {
            mPlayer.release();
            mPlayer = null;
        }
    }
}

好了皮假,上面就是關(guān)于實(shí)現(xiàn)的具體介紹了,如果想要詳細(xì)了解和查看功能效果請(qǐng)點(diǎn)傳送門(mén)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末骂维,一起剝皮案震驚了整個(gè)濱河市惹资,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌航闺,老刑警劉巖褪测,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異潦刃,居然都是意外死亡侮措,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)乖杠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)分扎,“玉大人,你說(shuō)我怎么就攤上這事胧洒∥废牛” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵卫漫,是天一觀的道長(zhǎng)菲饼。 經(jīng)常有香客問(wèn)我,道長(zhǎng)列赎,這世上最難降的妖魔是什么宏悦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮包吝,結(jié)果婚禮上饼煞,老公的妹妹穿的比我還像新娘。我一直安慰自己诗越,他們只是感情好砖瞧,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著掺喻,像睡著了一般芭届。 火紅的嫁衣襯著肌膚如雪储矩。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 50,084評(píng)論 1 291
  • 那天褂乍,我揣著相機(jī)與錄音持隧,去河邊找鬼。 笑死逃片,一個(gè)胖子當(dāng)著我的面吹牛屡拨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播褥实,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼呀狼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了损离?” 一聲冷哼從身側(cè)響起哥艇,我...
    開(kāi)封第一講書(shū)人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎僻澎,沒(méi)想到半個(gè)月后貌踏,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡窟勃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年祖乳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秉氧。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡眷昆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出汁咏,到底是詐尸還是另有隱情亚斋,我是刑警寧澤,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布梆暖,位于F島的核電站伞访,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏轰驳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一弟灼、第九天 我趴在偏房一處隱蔽的房頂上張望级解。 院中可真熱鬧,春花似錦田绑、人聲如沸勤哗。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)芒划。三九已至冬竟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間民逼,已是汗流浹背泵殴。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拼苍,地道東北人笑诅。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像疮鲫,于是被迫代替她去往敵國(guó)和親吆你。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容

  • 本篇文章是基于谷歌有關(guān)Graphic的一篇概覽文章的翻譯:http://source.android.com/de...
    lee_3do閱讀 7,109評(píng)論 2 21
  • Android 的音視頻入門(mén)學(xué)習(xí)妇多,首先了解一下繪制圖片。在 Android 平臺(tái)繪制一張圖片燕侠,使用至少 3 種不同...
    安仔夏天勤奮閱讀 2,351評(píng)論 0 8
  • 1. Outline 本文主要從以下三個(gè)大的方面來(lái)說(shuō)明一下2D Graphic 繪圖的一些相關(guān)函數(shù)及應(yīng)用。 Col...
    lee_3do閱讀 3,020評(píng)論 0 11
  • ¥開(kāi)啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開(kāi)一個(gè)線程咸包,因...
    小菜c閱讀 6,376評(píng)論 0 17
  • Test杖虾!小白試試怎么使用這個(gè)網(wǎng)頁(yè)及app
    oyhkwell閱讀 133評(píng)論 0 0