我是靠谱客的博主 甜美画笔,这篇文章主要介绍Android中播放webp动画的一种方式:FrameSequenceDrawable,现在分享给大家,希望可以做个参考。

简介

本篇主要是介绍FrameSequenceDrawable的相关实现原理的文章,FrameSequenceDrawable是Google实现的可以播放Webp动画的Drawable,这个并没有在SDK里面,但是我们可以在googlesource中看到相关的代码,FrameSequenceDrawable相关代码地址

播放效果

在介绍之前,我们可以先看一下播放效果:
webp.gif

我想直接用

如果你说我不想看原理,我就想知道咋播放webp,那么我就帮助你完成一个简单小库,虽然是我封装的,但是代码可都是人家google开发哥哥写的,我帮你搬运过来,哈哈
这里是链接

如何引入到工程

  • Add the JitPack repository to your build file
复制代码
1
2
3
4
5
6
7
allprojects { repositories { ... maven { url 'https://jitpack.io' } } }
  • Add the dependency

    复制代码
    1
    2
    3
    4
    dependencies { compile 'com.github.humorousz:FrameSequenceDrawable:1.0.1-SNAPSHOT' }

如何使用

  • xml
复制代码
1
2
3
4
5
6
7
8
9
10
<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
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void setImage(){ AnimatedImageView mGoogleImage; mGoogleImage = findViewById(R.id.google_sequence_image); //设置重复次数 mGoogleImage.setLoopCount(1); //重复行为默认 根据webp图片循环次数决定 mGoogleImage.setLoopDefault(); //重复行为无限 mGoogleImage.setLoopInf(); //重复行为为指定 跟setLoopCount有关 mGoogleImage.setLoopFinite(); //设置Assets下的图片 mGoogleImage.setImageResourceFromAssets("newyear.webp"); //设置图片通过drawable mGoogleImage.setImageResource(R.drawable.newyear); Uri uri = Uri.parse("file:"+Environment.getExternalStorageDirectory().toString()+"/animation"); //通过添加"file:"协议,可以展示指定路径的图片,如例子中的本地资源 mGoogleImage.setImageURI(uri); }

当然你也可以不使用我这里的AnimatedImageView,AnimatedImageView是我参考其它的代码后修改封装的类,直接使用FrameSequenceDrawable+ImageView也是可以的,使用方法如下

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
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方法在指定时间替换图片,在达到替换时间时,会调用draw方法,在draw之前先去子线程解析下一张要展示的图片,然后重复这个步骤,直到播放结束或者一直播放
    #### 涉及到的类
  • FrameSequenceDrawable
    这个我们直接使用播放webp动画的类,它继承了Drawable并且实现了Animatable, Runnable两个接口,所以我们可以像使用Drawable一样的去使用它
  • FrameSequence
    从名字上来看这个类的意思很明确,那就是帧序列,它主要负责对传入的webp流进行解析,解析的地方是在native层,所以如果自己想编译FrameSequenceDrawable源码的话,需要编译JNI文件夹下的相关文件生成so库

流程分析

在分析源码之前,先把整个代码的流程分步骤简单介绍一下,后面根据这里介绍的流程去逐个分析源码
- 在FrameSequenceDrawable构造函数中创建解析线程,使用HandlerThread作为解析线程
- 在触发了setVisiable方法之后,会触发自身start方法开始解析第一张图片
- start方法调用scheduleDecodeLocked开始解析
- mDecodeRunnable的run方法执行,解析下一张要展示的图片,调用Drawable自身的scheduleSelf方法,参数when会设置为当前图片的展示时间
- scheduleSelf 会调用FrameSequenceDrawable所实现Runnable的run方法,并且导致draw,在draw方法中会首先调用解析线程去解析下一张图片,然后在继续绘制当前图片
- 反复执行绘制和解析步骤,知道循环次数达到设置状态或者无限循环

效果示意图

1.png

2.png

3.png

源码分析

现在我们对整个流程上的源码进行一些分析
- 首先第一步我们先看看FrameSequenceDrawable的构造函数,可以发现源码中一共有两个构造函数,我为了方便在我分享的github项目里增加了第三个构造,下面我们来一起看一看

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//这个是我自己添加的,利用了FrameSequence可以通过InputStream方法创建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(); }

我们可以看到在构造方法中,创建了mFrontBitmap和mBackBitmap两个对象,它俩的作用就是mFrontBitmap用于绘制,mBackBitmap用于解析线程下一张要展示的图片,在每次draw方法之前会把它俩所指向的实际bitmap交换,FrameSequence就是抽象出去的帧序列对象,它内部封装了动画的长、宽、透明度、循环次数、帧数等属性,它的内部所有解析和获取帧的方法都是native,我们来看看initializeDecodingThread这个方法做了哪些事情

复制代码
1
2
3
4
5
6
7
8
9
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()); } }

这里也很简单,就是创建了一个HandlerThread,后续所有调用线程调度解析都是通过sDecodingThreadHandler这个去实现的
- setVisible,动画的开始
FrameSequenceDrawable的setVisible重载了父类的setVisible,这个会在设置动画的时候被调用,这里也是动画调度开始的地方,我们来看一下它的实现

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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会调用start方法,start方法会调用到scheduleDecodeLocked方法,这个方法会计算下一张需要解析的index,然后通过sDecodingThreadHandler调用mDecodeRunnable去在子线程进行解析,下面我们来看看mDecodeRunnable干了一些什么事情

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/** * 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; //计算下次调度的时间,上一张图片的展示时间加上上次调度的时间(mLastSwap就是上次调度的时间) mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap; mState = STATE_WAITING_TO_SWAP; } } if (schedule) { //在mNextSwap时调度自己的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); } } };

在上面的代码中比较关键的部分我已经加了注释,整段代码的逻辑可以分为三个部分,第一个部分是设置条件判断以及设置mState为STATE_DECODING

复制代码
1
2
3
4
5
6
7
8
9
10
11
synchronized (mLock) { if (mDestroyed) return; //下一张要解析的index nextFrame = mNextFrameToDecode; if (nextFrame < 0) { return; } //后台解析时用mBackBitmap bitmap = mBackBitmap; mState = STATE_DECODING; }

第二部分是解析nextFrame并且获取nextFrame上一张图片的展示时间,并且修改mState和计算mNextSwap时间

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
... 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; //计算下次调度的时间,上一张图片的展示时间加上上次调度的时间(mLastSwap就是上次调度的时间) mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap; mState = STATE_WAITING_TO_SWAP; } }

关于 mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame)这个方法的返回值到底是哪一帧的时间,我一开始也不是很明确,但是后来通过思考和后面的逻辑来看,这个返回值应该是nextFrame的上一张图片的时间,因为下次调度的时间是这个返回值+ mLastSwap,后来看了一下native的代码,证实了这个想法,getFrame调用了native的nativeGetFrame方法,nativeGetFrame方法又调用了drawFrame,c++层的代码如下

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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应该是计算值可能从1开始,因为上面for循环计算第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,为什么要纠结于这一块的?因为我们只要理解了这个方法,就可以抽象FrameSequence,然后使用自己或者其他的解析代码来解析帧,可以灵活的使用解析库,还可以同时支持gif和webp
继续代码第三部分,这部分就是在调度了,在nextSwap的时间

复制代码
1
2
3
4
5
6
7
8
9
if (schedule) { //在mNextSwap时调度自己的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调用自身的run方法触发了绘制
    通过上面的流程,到达了时间后,就会触发scheduleSelf调用FrameSequenceDrawable自身的run方法并且会触发绘制,下面我们就来看看这部分代码
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@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); } }

这里代码的主要就可以分成两个部分了,下面绘制的部分我们就不说了,主要看上面的获取当前需要绘制的图片和解析下一张图片的部分

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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) { //因为交换时间到了,所以应该绘制mBackBitmap的内容了,而mFrontBitmap所指向的内存可以用于解析下一张图片使用了 //所以交换它们所指向的bitmap Bitmap tmp = mBackBitmap; mBackBitmap = mFrontBitmap; mFrontBitmap = tmp; BitmapShader tmpShader = mBackBitmapShader; mBackBitmapShader = mFrontBitmapShader; mFrontBitmapShader = tmpShader; mLastSwap = SystemClock.uptimeMillis(); boolean continueLooping = true; //如果绘制到了最后一张,就需要我们根据条件判断是否继续loop了 if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) { mCurrentLoop++; //第一个判断的条件是,LoopBehavior是LOOP_FINITE时,根据是否达到我们设置的loopCount为依据,如果达到就结束 //第二个判断的条件是,LoopBehavior是LOOP_DEFAULT时,根据Sequence自身的LoopCount来决定,如果达到就结束 if ((mLoopBehavior == LOOP_FINITE && mCurrentLoop == mLoopCount) || (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) { continueLooping = false; } } if (continueLooping) { //继续调度下张 scheduleDecodeLocked(); } else { scheduleSelf(mFinishedCallbackRunnable, 0); } }

同样关键的部分我已经注释在上面了,主要就是达到了交换的时间会产生调度,然后重新绘制,在重新绘制时,需要绘制的图片是mBackBitmap,然后mFrontBitmap可以用于解析下一张图片,所以把它俩做了一次交换,后面主要就是判断是否播放到了最后一张,如果播放到了最后一张,那么就会根据条件判断是否继续循环播放,最后满足条件的话调用scheduleDecodeLocked,这个方法上面有介绍,就是让解析线程解析下一张图片,这样反复的进行,整个webp动画就播放起来了,整个解析的过程中也不会造成内存的飙升,因为使用的内存只有mFrontBitmap和mBackBitmap,这种思想还是很好的,如果我们想在节约内存,只用一个bitmap,解一张播一张的话会没有这么流畅,别问我为什么知道,因为我们项目里现在就是播一张解析一张的。。。
好了,到这里整体代码逻辑的介绍就完成了,如果你觉得我说的不是那么清晰,可以留言说出你的疑问,也可以直接阅读源码看看到底是咋回事

预告

其实我看了这个源码以后,想了一下我们之前播放webp用的库,我通过抽象了FrameSequence这个类,在保持了FrameSequenceDrawable几乎所有的源码后,使用了facebook 的 Fresco库对FrameSequence这个类进行了抽象和实现,达到了一个Drawable可以通过简单的修改可以同时支持webp和gif的功能,介绍的文章在我写完这个之后会马上开始~

更新

预告中的文章已经写完Android播放webp和gif的一种方法(接上篇),欢迎批评指正

最后

以上就是甜美画笔最近收集整理的关于Android中播放webp动画的一种方式:FrameSequenceDrawable的全部内容,更多相关Android中播放webp动画内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(55)

评论列表共有 0 条评论

立即
投稿
返回
顶部