簡介
本篇主要是介紹FrameSequenceDrawable的相關(guān)實現(xiàn)原理的文章,F(xiàn)rameSequenceDrawable是Google實現(xiàn)的可以播放Webp動畫的Drawable愿卸,這個并沒有在SDK里面趴荸,但是我們可以在googlesource中看到相關(guān)的代碼,FrameSequenceDrawable相關(guān)代碼地址
播放效果
在介紹之前赶诊,我們可以先看一下播放效果:
我想直接用
如果你說我不想看原理寓调,我就想知道咋播放webp夺英,那么我就幫助你完成一個簡單小庫痛悯,雖然是我封裝的载萌,但是代碼可都是人家google開發(fā)哥哥寫的扭仁,我?guī)湍惆徇\過來乖坠,哈哈
這里是鏈接
如何引入到工程
- Add the JitPack repository to your build file
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
- Add the dependency
dependencies {
compile 'com.github.humorousz:FrameSequenceDrawable:1.0.1-SNAPSHOT'
}
如何使用
- xml
<com.humrousz.sequence.view.AnimatedImageView
android:id="@+id/google_sequence_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/group"
app:loopCount="1"
app:loopBehavior="loop_default|loop_finite|loop_inf"
android:scaleType="centerCrop"
android:src="@drawable/webpRes"/>
- java
public void setImage(){
AnimatedImageView mGoogleImage;
mGoogleImage = findViewById(R.id.google_sequence_image);
//設(shè)置重復(fù)次數(shù)
mGoogleImage.setLoopCount(1);
//重復(fù)行為默認(rèn) 根據(jù)webp圖片循環(huán)次數(shù)決定
mGoogleImage.setLoopDefault();
//重復(fù)行為無限
mGoogleImage.setLoopInf();
//重復(fù)行為為指定 跟setLoopCount有關(guān)
mGoogleImage.setLoopFinite();
//設(shè)置Assets下的圖片
mGoogleImage.setImageResourceFromAssets("newyear.webp");
//設(shè)置圖片通過drawable
mGoogleImage.setImageResource(R.drawable.newyear);
Uri uri = Uri.parse("file:"+Environment.getExternalStorageDirectory().toString()+"/animation");
//通過添加"file:"協(xié)議涩赢,可以展示指定路徑的圖片筒扒,如例子中的本地資源
mGoogleImage.setImageURI(uri);
}
當(dāng)然你也可以不使用我這里的AnimatedImageView花墩,AnimatedImageView是我參考其它的代碼后修改封裝的類冰蘑,直接使用FrameSequenceDrawable+ImageView也是可以的,使用方法如下
ImageView mImage;
InputStream in = null;
in = getResources().getAssets().open("anim.webp");
final FrameSequenceDrawable drawable = new FrameSequenceDrawable(in);
drawable.setLoopCount(1);
drawable.setLoopBehavior(FrameSequenceDrawable.LOOP_FINITE);
drawable.setOnFinishedListener(new FrameSequenceDrawable.OnFinishedListener() {
@Override
public void onFinished(FrameSequenceDrawable frameSequenceDrawable) {
}
});
mImage.setImageDrawable(drawable);
原理介紹
原理簡介
- 利用了兩個Bitmap對象祠肥,其中一個用于繪制到屏幕上武氓,另外一個用于解析下一張要展示的圖片,利用了HandlerThread在子線程解析,每次解析的時候獲取上一張圖片的展示時間县恕,然后使用Drawable自身的scheduleSelf方法在指定時間替換圖片东羹,在達(dá)到替換時間時,會調(diào)用draw方法忠烛,在draw之前先去子線程解析下一張要展示的圖片,然后重復(fù)這個步驟美尸,直到播放結(jié)束或者一直播放
涉及到的類
- FrameSequenceDrawable
這個我們直接使用播放webp動畫的類冤议,它繼承了Drawable并且實現(xiàn)了Animatable, Runnable兩個接口,所以我們可以像使用Drawable一樣的去使用它 - FrameSequence
從名字上來看這個類的意思很明確师坎,那就是幀序列恕酸,它主要負(fù)責(zé)對傳入的webp流進(jìn)行解析,解析的地方是在native層屹耐,所以如果自己想編譯FrameSequenceDrawable源碼的話尸疆,需要編譯JNI文件夾下的相關(guān)文件生成so庫
流程分析
在分析源碼之前,先把整個代碼的流程分步驟簡單介紹一下惶岭,后面根據(jù)這里介紹的流程去逐個分析源碼
- 在FrameSequenceDrawable構(gòu)造函數(shù)中創(chuàng)建解析線程寿弱,使用HandlerThread作為解析線程
- 在觸發(fā)了setVisiable方法之后,會觸發(fā)自身start方法開始解析第一張圖片
- start方法調(diào)用scheduleDecodeLocked開始解析
- mDecodeRunnable的run方法執(zhí)行按灶,解析下一張要展示的圖片症革,調(diào)用Drawable自身的scheduleSelf方法,參數(shù)when會設(shè)置為當(dāng)前圖片的展示時間
- scheduleSelf 會調(diào)用FrameSequenceDrawable所實現(xiàn)Runnable的run方法鸯旁,并且導(dǎo)致draw噪矛,在draw方法中會首先調(diào)用解析線程去解析下一張圖片,然后在繼續(xù)繪制當(dāng)前圖片
- 反復(fù)執(zhí)行繪制和解析步驟铺罢,知道循環(huán)次數(shù)達(dá)到設(shè)置狀態(tài)或者無限循環(huán)
效果示意圖
源碼分析
現(xiàn)在我們對整個流程上的源碼進(jìn)行一些分析
- 首先第一步我們先看看FrameSequenceDrawable的構(gòu)造函數(shù)艇挨,可以發(fā)現(xiàn)源碼中一共有兩個構(gòu)造函數(shù),我為了方便在我分享的github項目里增加了第三個構(gòu)造韭赘,下面我們來一起看一看
//這個是我自己添加的缩滨,利用了FrameSequence可以通過InputStream方法創(chuàng)建FrameSequence功能
public FrameSequenceDrawable(InputStream inputStream){
this(FrameSequence.decodeStream(inputStream));
}
public FrameSequenceDrawable(FrameSequence frameSequence) {
this(frameSequence, sAllocatingBitmapProvider);
}
public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) {
if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException();
mFrameSequence = frameSequence;
mFrameSequenceState = frameSequence.createState();
final int width = frameSequence.getWidth();
final int height = frameSequence.getHeight();
mBitmapProvider = bitmapProvider;
mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
mSrcRect = new Rect(0, 0, width, height);
mPaint = new Paint();
mPaint.setFilterBitmap(true);
mFrontBitmapShader
= new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mBackBitmapShader
= new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mLastSwap = 0;
mNextFrameToDecode = -1;
mFrameSequenceState.getFrame(0, mFrontBitmap, -1);
initializeDecodingThread();
}
我們可以看到在構(gòu)造方法中,創(chuàng)建了mFrontBitmap和mBackBitmap兩個對象泉瞻,它倆的作用就是mFrontBitmap用于繪制脉漏,mBackBitmap用于解析線程下一張要展示的圖片,在每次draw方法之前會把它倆所指向的實際bitmap交換袖牙,F(xiàn)rameSequence就是抽象出去的幀序列對象侧巨,它內(nèi)部封裝了動畫的長、寬鞭达、透明度司忱、循環(huán)次數(shù)皇忿、幀數(shù)等屬性,它的內(nèi)部所有解析和獲取幀的方法都是native坦仍,我們來看看initializeDecodingThread這個方法做了哪些事情
private static void initializeDecodingThread() {
synchronized (sLock) {
if (sDecodingThread != null) return;
sDecodingThread = new HandlerThread("FrameSequence decoding thread",
Process.THREAD_PRIORITY_BACKGROUND);
sDecodingThread.start();
sDecodingThreadHandler = new Handler(sDecodingThread.getLooper());
}
}
這里也很簡單禁添,就是創(chuàng)建了一個HandlerThread,后續(xù)所有調(diào)用線程調(diào)度解析都是通過sDecodingThreadHandler這個去實現(xiàn)的
- setVisible桨踪,動畫的開始
FrameSequenceDrawable的setVisible重載了父類的setVisible,這個會在設(shè)置動畫的時候被調(diào)用芹啥,這里也是動畫調(diào)度開始的地方锻离,我們來看一下它的實現(xiàn)
@Override
public boolean setVisible(boolean visible, boolean restart) {
boolean changed = super.setVisible(visible, restart);
if (!visible) {
stop();
} else if (restart || changed) {
stop();
start();
}
return changed;
}
@Override
//Animatable中的方法
public void start() {
if (!isRunning()) {
synchronized (mLock) {
checkDestroyedLocked();
if (mState == STATE_SCHEDULED) return; // already scheduled
mCurrentLoop = 0;
scheduleDecodeLocked();
}
}
}
private void scheduleDecodeLocked() {
mState = STATE_SCHEDULED;
mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
sDecodingThreadHandler.post(mDecodeRunnable);
}
可以看到,setVisible會調(diào)用start方法墓怀,start方法會調(diào)用到scheduleDecodeLocked方法汽纠,這個方法會計算下一張需要解析的index,然后通過sDecodingThreadHandler調(diào)用mDecodeRunnable去在子線程進(jìn)行解析傀履,下面我們來看看mDecodeRunnable干了一些什么事情
/**
* Runs on decoding thread, only modifies mBackBitmap's pixels
*/
private Runnable mDecodeRunnable = new Runnable() {
@Override
public void run() {
int nextFrame;
Bitmap bitmap;
synchronized (mLock) {
if (mDestroyed) return;
//下一張要解析的index
nextFrame = mNextFrameToDecode;
if (nextFrame < 0) {
return;
}
//后臺解析時用mBackBitmap
bitmap = mBackBitmap;
mState = STATE_DECODING;
}
int lastFrame = nextFrame - 2;
boolean exceptionDuringDecode = false;
long invalidateTimeMs = 0;
try {
//解析下一張圖片到bitmap虱朵,并且返回nextFrame-1的展示時間
invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
} catch (Exception e) {
// Exception during decode: continue, but delay next frame indefinitely.
Log.e(TAG, "exception during decode: " + e);
exceptionDuringDecode = true;
}
if (invalidateTimeMs < MIN_DELAY_MS) {
invalidateTimeMs = DEFAULT_DELAY_MS;
}
boolean schedule = false;
Bitmap bitmapToRelease = null;
//計算是否滿足交換普片的條件
synchronized (mLock) {
if (mDestroyed) {
bitmapToRelease = mBackBitmap;
mBackBitmap = null;
} else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) {
schedule = true;
//計算下次調(diào)度的時間,上一張圖片的展示時間加上上次調(diào)度的時間(mLastSwap就是上次調(diào)度的時間)
mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap;
mState = STATE_WAITING_TO_SWAP;
}
}
if (schedule) {
//在mNextSwap時調(diào)度自己的run方法
scheduleSelf(FrameSequenceDrawable.this, mNextSwap);
}
if (bitmapToRelease != null) {
// destroy the bitmap here, since there's no safe way to get back to
// drawable thread - drawable is likely detached, so schedule is noop.
mBitmapProvider.releaseBitmap(bitmapToRelease);
}
}
};
在上面的代碼中比較關(guān)鍵的部分我已經(jīng)加了注釋钓账,整段代碼的邏輯可以分為三個部分碴犬,第一個部分是設(shè)置條件判斷以及設(shè)置mState為STATE_DECODING
synchronized (mLock) {
if (mDestroyed) return;
//下一張要解析的index
nextFrame = mNextFrameToDecode;
if (nextFrame < 0) {
return;
}
//后臺解析時用mBackBitmap
bitmap = mBackBitmap;
mState = STATE_DECODING;
}
第二部分是解析nextFrame并且獲取nextFrame上一張圖片的展示時間,并且修改mState和計算mNextSwap時間
...
try {
//解析下一張圖片到bitmap梆暮,并且返回lastFrame的展示時間
invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
} catch (Exception e) {
// Exception during decode: continue, but delay next frame indefinitely.
Log.e(TAG, "exception during decode: " + e);
exceptionDuringDecode = true;
}
....
synchronized (mLock) {
if (mDestroyed) {
bitmapToRelease = mBackBitmap;
mBackBitmap = null;
} else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) {
schedule = true;
//計算下次調(diào)度的時間服协,上一張圖片的展示時間加上上次調(diào)度的時間(mLastSwap就是上次調(diào)度的時間)
mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap;
mState = STATE_WAITING_TO_SWAP;
}
}
關(guān)于 mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame)這個方法的返回值到底是哪一幀的時間,我一開始也不是很明確啦粹,但是后來通過思考和后面的邏輯來看偿荷,這個返回值應(yīng)該是nextFrame的上一張圖片的時間,因為下次調(diào)度的時間是這個返回值+ mLastSwap唠椭,后來看了一下native的代碼跳纳,證實了這個想法,getFrame調(diào)用了native的nativeGetFrame方法贪嫂,nativeGetFrame方法又調(diào)用了drawFrame寺庄,c++層的代碼如下
static jlong JNICALL nativeGetFrame(
... 省略
jlong delayMs = frameSequenceState->drawFrame(frameNr,
(Color8888*) pixels, pixelStride, previousFrameNr);
AndroidBitmap_unlockPixels(env, bitmap);
return delayMs;
}
long FrameSequenceState_webp::drawFrame(int frameNr,
Color8888* outputPtr, int outputPixelStride, int previousFrameNr) {
... 省略
WebPIterator currIter;
WebPIterator prevIter;
int ok = WebPDemuxGetFrame(demux, start, &currIter); // Get frame number 'start - 1'.
ALOG_ASSERT(ok, "Could not retrieve frame# %d", start - 1);
// Use preserve buffer only if needed.
Color8888* prevBuffer = (frameNr == 0) ? outputPtr : mPreservedBuffer;
int prevStride = (frameNr == 0) ? outputPixelStride : canvasWidth;
Color8888* currBuffer = outputPtr;
int currStride = outputPixelStride;
for (int i = start; i <= frameNr; i++) {
prevIter = currIter;
ok = WebPDemuxGetFrame(demux, i + 1, &currIter); // Get ith frame.
ALOG_ASSERT(ok, "Could not retrieve frame# %d", i);
...省略
// Return last frame's delay.
const int frameCount = mFrameSequence.getFrameCount();
const int lastFrame = (frameNr + frameCount - 1) % frameCount;
//這里雖然+1應(yīng)該是計算值可能從1開始,因為上面for循環(huán)計算第ith時也加了1
ok = WebPDemuxGetFrame(demux, lastFrame + 1, &currIter);
ALOG_ASSERT(ok, "Could not retrieve frame# %d", lastFrame);
const int lastFrameDelay = currIter.duration;
WebPDemuxReleaseIterator(&currIter);
WebPDemuxReleaseIterator(&prevIter);
return lastFrameDelay;
}
可以看到最后的返回值是lastFrameDelay它的計算幀lastFrame是(frameNr + frameCount - 1) % frameCount計算出來的撩荣,可以看到確實是frameNr的上一張铣揉,frameNr就是我們這里的nextFrame,為什么要糾結(jié)于這一塊的餐曹?因為我們只要理解了這個方法逛拱,就可以抽象FrameSequence,然后使用自己或者其他的解析代碼來解析幀台猴,可以靈活的使用解析庫朽合,還可以同時支持gif和webp
繼續(xù)代碼第三部分俱两,這部分就是在調(diào)度了,在nextSwap的時間
if (schedule) {
//在mNextSwap時調(diào)度自己的run方法
scheduleSelf(FrameSequenceDrawable.this, mNextSwap);
}
if (bitmapToRelease != null) {
// destroy the bitmap here, since there's no safe way to get back to
// drawable thread - drawable is likely detached, so schedule is noop.
mBitmapProvider.releaseBitmap(bitmapToRelease);
}
- scheduleSelf調(diào)用自身的run方法觸發(fā)了繪制
通過上面的流程曹步,到達(dá)了時間后宪彩,就會觸發(fā)scheduleSelf調(diào)用FrameSequenceDrawable自身的run方法并且會觸發(fā)繪制,下面我們就來看看這部分代碼
@Override
public void run() {
// set ready to swap as necessary
boolean invalidate = false;
synchronized (mLock) {
if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) {
mState = STATE_READY_TO_SWAP;
invalidate = true;
}
}
if (invalidate) {
invalidateSelf();
}
}
@Override
public void draw(Canvas canvas) {
synchronized (mLock) {
checkDestroyedLocked();
if (mState == STATE_WAITING_TO_SWAP) {
// may have failed to schedule mark ready runnable,
// so go ahead and swap if swapping is due
if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
mState = STATE_READY_TO_SWAP;
}
}
if (isRunning() && mState == STATE_READY_TO_SWAP) {
// Because draw has occurred, the view system is guaranteed to no longer hold a
// reference to the old mFrontBitmap, so we now use it to produce the next frame
Bitmap tmp = mBackBitmap;
mBackBitmap = mFrontBitmap;
mFrontBitmap = tmp;
BitmapShader tmpShader = mBackBitmapShader;
mBackBitmapShader = mFrontBitmapShader;
mFrontBitmapShader = tmpShader;
mLastSwap = SystemClock.uptimeMillis();
boolean continueLooping = true;
if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
mCurrentLoop++;
if ((mLoopBehavior == LOOP_FINITE && mCurrentLoop == mLoopCount) ||
(mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
continueLooping = false;
}
}
if (continueLooping) {
scheduleDecodeLocked();
} else {
scheduleSelf(mFinishedCallbackRunnable, 0);
}
}
}
if (mCircleMaskEnabled) {
final Rect bounds = getBounds();
final int bitmapWidth = getIntrinsicWidth();
final int bitmapHeight = getIntrinsicHeight();
final float scaleX = 1.0f * bounds.width() / bitmapWidth;
final float scaleY = 1.0f * bounds.height() / bitmapHeight;
canvas.save();
// scale and translate to account for bounds, so we can operate in intrinsic
// width/height (so it's valid to use an unscaled bitmap shader)
canvas.translate(bounds.left, bounds.top);
canvas.scale(scaleX, scaleY);
final float unscaledCircleDiameter = Math.min(bounds.width(), bounds.height());
final float scaledDiameterX = unscaledCircleDiameter / scaleX;
final float scaledDiameterY = unscaledCircleDiameter / scaleY;
// Want to draw a circle, but we have to compensate for canvas scale
mTempRectF.set(
(bitmapWidth - scaledDiameterX) / 2.0f,
(bitmapHeight - scaledDiameterY) / 2.0f,
(bitmapWidth + scaledDiameterX) / 2.0f,
(bitmapHeight + scaledDiameterY) / 2.0f);
mPaint.setShader(mFrontBitmapShader);
canvas.drawOval(mTempRectF, mPaint);
canvas.restore();
} else {
mPaint.setShader(null);
canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint);
}
}
這里代碼的主要就可以分成兩個部分了讲婚,下面繪制的部分我們就不說了尿孔,主要看上面的獲取當(dāng)前需要繪制的圖片和解析下一張圖片的部分
if (mState == STATE_WAITING_TO_SWAP) {
// may have failed to schedule mark ready runnable,
// so go ahead and swap if swapping is due
if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
mState = STATE_READY_TO_SWAP;
}
}
if (isRunning() && mState == STATE_READY_TO_SWAP) {
//因為交換時間到了,所以應(yīng)該繪制mBackBitmap的內(nèi)容了筹麸,而mFrontBitmap所指向的內(nèi)存可以用于解析下一張圖片使用了
//所以交換它們所指向的bitmap
Bitmap tmp = mBackBitmap;
mBackBitmap = mFrontBitmap;
mFrontBitmap = tmp;
BitmapShader tmpShader = mBackBitmapShader;
mBackBitmapShader = mFrontBitmapShader;
mFrontBitmapShader = tmpShader;
mLastSwap = SystemClock.uptimeMillis();
boolean continueLooping = true;
//如果繪制到了最后一張活合,就需要我們根據(jù)條件判斷是否繼續(xù)loop了
if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
mCurrentLoop++;
//第一個判斷的條件是,LoopBehavior是LOOP_FINITE時物赶,根據(jù)是否達(dá)到我們設(shè)置的loopCount為依據(jù)白指,如果達(dá)到就結(jié)束
//第二個判斷的條件是,LoopBehavior是LOOP_DEFAULT時酵紫,根據(jù)Sequence自身的LoopCount來決定告嘲,如果達(dá)到就結(jié)束
if ((mLoopBehavior == LOOP_FINITE && mCurrentLoop == mLoopCount) ||
(mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
continueLooping = false;
}
}
if (continueLooping) {
//繼續(xù)調(diào)度下張
scheduleDecodeLocked();
} else {
scheduleSelf(mFinishedCallbackRunnable, 0);
}
}
同樣關(guān)鍵的部分我已經(jīng)注釋在上面了,主要就是達(dá)到了交換的時間會產(chǎn)生調(diào)度奖地,然后重新繪制橄唬,在重新繪制時,需要繪制的圖片是mBackBitmap参歹,然后mFrontBitmap可以用于解析下一張圖片轧坎,所以把它倆做了一次交換,后面主要就是判斷是否播放到了最后一張泽示,如果播放到了最后一張缸血,那么就會根據(jù)條件判斷是否繼續(xù)循環(huán)播放,最后滿足條件的話調(diào)用scheduleDecodeLocked械筛,這個方法上面有介紹捎泻,就是讓解析線程解析下一張圖片,這樣反復(fù)的進(jìn)行埋哟,整個webp動畫就播放起來了笆豁,整個解析的過程中也不會造成內(nèi)存的飆升,因為使用的內(nèi)存只有mFrontBitmap和mBackBitmap赤赊,這種思想還是很好的闯狱,如果我們想在節(jié)約內(nèi)存,只用一個bitmap抛计,解一張播一張的話會沒有這么流暢哄孤,別問我為什么知道,因為我們項目里現(xiàn)在就是播一張解析一張的吹截。瘦陈。凝危。
好了,到這里整體代碼邏輯的介紹就完成了晨逝,如果你覺得我說的不是那么清晰蛾默,可以留言說出你的疑問,也可以直接閱讀源碼看看到底是咋回事
預(yù)告
其實我看了這個源碼以后捉貌,想了一下我們之前播放webp用的庫支鸡,我通過抽象了FrameSequence這個類,在保持了FrameSequenceDrawable幾乎所有的源碼后趁窃,使用了facebook 的 Fresco庫對FrameSequence這個類進(jìn)行了抽象和實現(xiàn)苍匆,達(dá)到了一個Drawable可以通過簡單的修改可以同時支持webp和gif的功能,介紹的文章在我寫完這個之后會馬上開始~
更新
預(yù)告中的文章已經(jīng)寫完Android播放webp和gif的一種方法(接上篇),歡迎批評指正