Android實(shí)現(xiàn)同頻共幀動(dòng)畫效果
同頻共幀
我們聽過“同頻共振”,其原理是多個(gè)物體物體以同樣的頻率振動(dòng),但是本篇實(shí)現(xiàn)的效果是“同頻共幀”,含義是:動(dòng)畫以同樣的頻率和同樣的幀展示在多個(gè)不同View上。
特點(diǎn):
- 動(dòng)畫效果
- 同樣的頻率
- 同樣的幀 (嚴(yán)格意義上是小于1個(gè)vsync信號(hào)的幀)
- 多個(gè)不同View同時(shí)展示
之前的文章中我們實(shí)現(xiàn)了很多動(dòng)效,但幾乎都是基于View本身實(shí)現(xiàn)的,但是在Android中,Drawable最容易擴(kuò)展的動(dòng)效工具,通過Drawable提供的接口,我們可以接入libpag、lottie、SVG、APNG、gif,LazyAnimationDrawable、AnimationDrawable等動(dòng)效,更加方便移植,同時(shí)Drawable支持setHotspot和setState接口,可以實(shí)現(xiàn)復(fù)雜度較低的交互效果。
這種動(dòng)效其實(shí)在手機(jī)版QQ上就有,如果你給自己的頭像設(shè)置為一個(gè)動(dòng)態(tài)圖,那么在群聊連發(fā)多條消息,那么你就會(huì)發(fā)現(xiàn),在同一個(gè)頁面上你的頭像動(dòng)畫是同步展示的。
現(xiàn)狀 & 痛點(diǎn)
現(xiàn)狀
我們以幀動(dòng)畫問題展開,要知道幀動(dòng)畫有難以容忍的內(nèi)存占用問題、以及主線程解碼問題,同時(shí)包體積問題也相當(dāng)嚴(yán)重,為此市面上出現(xiàn)了很多方案。libpag、lottie、VapPlayer、AlphaPlayer、APNG、GIF、SVGA、AnimationDrawable等。但你在開發(fā)時(shí)就會(huì)發(fā)現(xiàn),每一種引擎都有自己獨(dú)特的優(yōu)勢,也有自己獨(dú)特的劣勢,你往往想著用一種引擎統(tǒng)一所有動(dòng)效實(shí)現(xiàn),但往往現(xiàn)實(shí)不允許。
我們來說說幾大引擎的優(yōu)缺點(diǎn):
libPag: 目前支持功能最多的動(dòng)效引擎,普通動(dòng)畫性能也非常不錯(cuò),相比其他引擎快很多。該引擎使用自研渲染引擎和解碼器實(shí)現(xiàn),但是對于預(yù)合成動(dòng)效(超長動(dòng)效和復(fù)雜動(dòng)效可能會(huì)用到),由于其使用的是軟解,在低配設(shè)備上比VapPlayer和AlphaPlayer卡的多,另外lib so相比其他引擎也是大很多。
VapPlayer/AlphaPlayer : 這兩種其都是通過alpha 遮罩實(shí)現(xiàn),大部分情況下使用的是設(shè)備硬解碼器,不過,VapPlayer缺乏硬解碼器篩選機(jī)制,偶爾有時(shí)會(huì)拿到軟解碼器,另外其本身存在音畫同步問題,至于AlphaPlayer把播放交給系統(tǒng)和三方播放器,避免了此類問題。但是,如果是音視頻類app,他們都有共同的問題,終端設(shè)備上硬解碼器的實(shí)例數(shù)量是受限制的,甚至有的設(shè)備解碼器同一時(shí)刻只能使用一個(gè),使用這兩種解碼器就會(huì)造成業(yè)務(wù)播放器綠屏、起播失敗、解碼器卡住等問題。不過解決辦法是將特效和業(yè)務(wù)播放器資源類型隔離,如果業(yè)務(wù)播放器是使用h264,那么你可以動(dòng)效使用h264\mpeg2等其他編碼類型。
lottie: lottie目前是比較廣為人知的動(dòng)效引擎,使用也相當(dāng)廣泛。但其存在跨平臺(tái)兼容性,缺少很多特效,其性能是不如libpag的,不過總體能覆蓋到大部分場景。另一個(gè)開發(fā)中常常會(huì)遇到的問題是,UI設(shè)計(jì)人員對于lottie的compose layer理解存在問題,往往會(huì)出現(xiàn)將lottie動(dòng)畫做成和幀動(dòng)畫一樣的動(dòng)畫,顯然,compose layer的思想是多張圖片合成,那就意味著圖片本身應(yīng)該有大有小,按一定軌跡運(yùn)動(dòng)和漸變,而不是一幀一幀簡單播放。
APNG、GIF : 這類動(dòng)畫屬于資源型動(dòng)畫,其本身存在很多缺點(diǎn),比如占內(nèi)存和耗cpu,不過簡單的場景還是可以使用的。
SVGA:很多平臺(tái)對這種動(dòng)畫抱有期待,特別是其矢量性質(zhì)和低內(nèi)存的特點(diǎn),然而,其本身面臨標(biāo)準(zhǔn)不統(tǒng)一的問題,造成跨平臺(tái)的能力不足。
LazyAnimationDrawable:幾乎所有的動(dòng)畫對低配設(shè)備都不友好,幀動(dòng)畫比上不足比下有余,低配設(shè)備上,為了解決libpag、VapPlayer、lottie對低配設(shè)備上音視頻類app不友好的問題,使用AnimationDrawble顯然是不行的,因此我們往往會(huì)實(shí)現(xiàn)了自己的AnimationDrawable,使其具備兜底的能力: 異步解碼 + 展示一幀預(yù)加載下一幀的能力,其實(shí)也就是LazyAnimationDrawable。
痛點(diǎn)
以上我們羅列了很多問題,看似和我們的主要目的毫無關(guān)系,其實(shí)我們可以想想,如果使用上述引擎,哪種方式可以實(shí)現(xiàn)兼容性更好的“同頻共幀”動(dòng)效呢 ?
實(shí)際上,幾乎沒有引擎能承擔(dān)此任務(wù),那有沒有辦法實(shí)現(xiàn)呢?
原理
我們很難讓每個(gè)View同時(shí)執(zhí)行和繪制同樣的畫面,另一個(gè)問題是,如果設(shè)計(jì)多個(gè)View繪制Bitmap,那么還可能造成資源加載的內(nèi)存OOM的問題。另外一方面如果使用LazyAnimationDrawable、VapX、AlphaPlayer等 ,如果同時(shí)執(zhí)行,那么解碼線程需要?jiǎng)?chuàng)建多個(gè),顯然性能問題也是重中之重。
有沒有更加簡單方法呢 ?
實(shí)際上是有的,那就是投影。
我們無論使用CompositeDrawable、LazyAnimationDrawable、AnimationDrawable還是VectorDrawable,我們可以保證在使用個(gè)實(shí)例的情況下,將畫面繪制到不同View上即可。
不過:本篇以AnimationDrawable 為例子實(shí)現(xiàn),其實(shí)其他Drawable動(dòng)畫類似。
實(shí)現(xiàn)
但是這種難度也是很高的,如果我們使用一個(gè)View 管理器,然后構(gòu)建一個(gè)播放器,顯然還要處理View各種狀態(tài),顯然避免不了耦合問題。這里我們回到開頭說過的drawable方案,當(dāng)然,一個(gè)drawable顯然無法設(shè)置給多個(gè)View,這點(diǎn)顯然是我們需要處理的難點(diǎn),此外,每個(gè)View的大小也不一致,如何處理這種問題呢。
Drawable加殼
我們參考Glide中com.bumptech.glide.request.target.FixedSizeDrawable 實(shí)現(xiàn),其原理是通過FixedSizeDrawable代理真實(shí)的drawble繪制,然后利用Matrix實(shí)現(xiàn)Canvas縮放,即可適配不同大小的View。
FixedSizeDrawable(State state, Drawable wrapped) { this.state = Preconditions.checkNotNull(state); this.wrapped = Preconditions.checkNotNull(wrapped); // We will do our own scaling. wrapped.setBounds(0, 0, wrapped.getIntrinsicWidth(), wrapped.getIntrinsicHeight()); matrix = new Matrix(); wrappedRect = new RectF(0, 0, wrapped.getIntrinsicWidth(), wrapped.getIntrinsicHeight()); bounds = new RectF(); }
Matrix 的作用
matrix.setRectToRect(wrappedRect, drawableBounds, Matrix.ScaleToFit.CENTER); canvas.concat(matrix); //Canvas Matrix 轉(zhuǎn)換
當(dāng)然,必要時(shí)支持下alpha和colorFilter,下面是完整實(shí)現(xiàn)。
public static class AnimationDrawableWrapper extends Drawable { private final AnimationDrawable animationDrawable; //動(dòng)畫drawable private final Matrix matrix = new Matrix(); private final RectF wrappedRect; private final RectF drawableBounds; private int alpha = 255; private ColorFilter colorFilter; public AnimationDrawableWrapper(AnimationDrawable drawable) { this.animationDrawable = drawable; this.wrappedRect = new RectF(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); this.drawableBounds = new RectF(); } @Override public void draw(Canvas canvas) { Drawable current = animationDrawable.getCurrent(); if (current == null) { return; } current.setAlpha(this.alpha); current.setColorFilter(colorFilter); Rect drawableRect = current.getBounds(); wrappedRect.set(drawableRect); drawableBounds.set(getBounds()); // 變化坐標(biāo) matrix.setRectToRect(wrappedRect, drawableBounds, Matrix.ScaleToFit.CENTER); int save = canvas.save(); canvas.concat(matrix); current.draw(canvas); canvas.restoreToCount(save); current.setAlpha(255);//還原 current.setColorFilter(null); //還原 } @Override public void setAlpha(int alpha) { this.alpha = alpha; } @Override public void setColorFilter(@Nullable ColorFilter colorFilter) { this.colorFilter = colorFilter; } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } }
View更新
我們知道AnimationDrawable每一幀都是不一樣的,那怎么將每一幀都能繪制在View上呢,了解過Drawable更新機(jī)制的開發(fā)者都知道,每一個(gè)View都實(shí)現(xiàn)了Drawable.Callback,當(dāng)給View設(shè)置drawable時(shí),Drawable.Callback也會(huì)設(shè)置給drawable。
Drawable刷新View時(shí)需要調(diào)用invalidate,顯然是通過Drawable.Callback實(shí)現(xiàn),當(dāng)然,Drawable自身就實(shí)現(xiàn)了更新方法Drawable#invalidateSelf,我們只需要調(diào)用改方法刷新View即可觸發(fā)View#onDraw,從而觸發(fā)drawable#draw方法。
public void invalidateSelf() { final Callback callback = getCallback(); if (callback != null) { callback.invalidateDrawable(this); } }
更新AnimationDrawable
顯然,任何動(dòng)畫都具備時(shí)間屬性,因此更新Drawable是必要的,View本身是可以通過Drawable.Callback機(jī)制更新Drawable的。通過scheduleDrawable和unscheduleDrawable 定時(shí)處理Runnable和取消Runnable。
public interface Callback { void invalidateDrawable(@NonNull Drawable who); void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when); void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what); }
而AnimationDrawable實(shí)現(xiàn)了Runnable接口
@Override public void run() { nextFrame(false); }
然而,如果使用的RecyclerView,那么還可能會(huì)出現(xiàn)View 從頁面移除的問題,因此依靠View顯然是不行的,這里我們引入Handler或者Choreograper。
this.choreographer = Choreographer.getInstance();
但是,我們什么時(shí)候調(diào)用呢?顯然還得利用Drawable.Callback機(jī)制
給animationDrawable設(shè)置Drawable.Callback
this.drawable.setCallback(callback);
更新邏輯實(shí)現(xiàn)
@Override public void invalidateDrawable(@NonNull Drawable who) { //更新所有wrapper for (int i = 0; i < drawableList.size(); i++) { WeakReference<AnimationDrawableWrapper> reference = drawableList.get(i); AnimationDrawableWrapper wrapper = reference.get(); if (wrapper == null) { return; } wrapper.invalidateSelf(); } } @Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { this.scheduleTask = what; this.choreographer.postFrameCallbackDelayed(this, when - SystemClock.uptimeMillis()); } @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { this.scheduleTask = null; this.choreographer.removeFrameCallback(this); }
既然使用Choreographer,那doFrame需要實(shí)現(xiàn)的
@Override public void doFrame(long frameTimeNanos) { if(this.scheduleTask != null) { this.scheduleTask.run(); } }
好了,以上就是核心邏輯,到此我們就實(shí)現(xiàn)了核心邏輯
完整代碼
public class MirrorFrameAnimation implements Drawable.Callback, Choreographer.FrameCallback { private final Drawable drawable; private final int drawableWidth; private final int drawableHeight; private List<WeakReference<AnimationDrawableWrapper>> drawableList = new ArrayList<>(); private Choreographer choreographer; private Runnable scheduleTask; public MirrorFrameAnimation(Resources resources, int resId, int drawableWidth, int drawableHeight) { //設(shè)置寬高,防止AnimationDrawable大小不穩(wěn)定問題 this.drawableWidth = drawableWidth; this.drawableHeight = drawableHeight; this.drawable = resources.getDrawable(resId); this.drawable.setBounds(0, 0, drawableHeight, drawableHeight); this.drawable.setCallback(this); this.choreographer = Choreographer.getInstance(); } public void start() { choreographer.removeFrameCallback(this); if (drawable instanceof AnimationDrawable) { ((AnimationDrawable) drawable).start(); } } public void stop() { choreographer.removeFrameCallback(this); if (drawable instanceof AnimationDrawable) { ((AnimationDrawable) drawable).stop(); } } /** * @return The number of frames in the animation */ public int getNumberOfFrames() { if (drawable instanceof AnimationDrawable) { return ((AnimationDrawable) drawable).getNumberOfFrames(); } return 1; } /** * @return The Drawable at the specified frame index */ public Drawable getFrame(int index) { if (drawable instanceof AnimationDrawable) { return ((AnimationDrawable) drawable).getFrame(index); } return drawable; } /** * @return The duration in milliseconds of the frame at the * specified index */ public int getDuration(int index) { if (drawable instanceof AnimationDrawable) { return ((AnimationDrawable) drawable).getDuration(index); } return -1; } /** * @return True of the animation will play once, false otherwise */ public boolean isOneShot() { if (drawable instanceof AnimationDrawable) { return ((AnimationDrawable) drawable).isOneShot(); } return true; } /** * Sets whether the animation should play once or repeat. * * @param oneShot Pass true if the animation should only play once */ public void setOneShot(boolean oneShot) { if (drawable instanceof AnimationDrawable) { ((AnimationDrawable) drawable).setOneShot(oneShot); } } public void syncDrawable(View view) { if (!(drawable instanceof AnimationDrawable)) { if(view instanceof ImageView) { ((ImageView) view).setImageDrawable(drawable); }else{ view.setBackground(drawable); } return; } AnimationDrawableWrapper wrapper = new AnimationDrawableWrapper((AnimationDrawable) drawable); drawableList.add(new WeakReference<>(wrapper)); if(view instanceof ImageView) { ((ImageView) view).setImageDrawable(wrapper); }else{ view.setBackground(wrapper); } } @Override public void invalidateDrawable(@NonNull Drawable who) { for (int i = 0; i < drawableList.size(); i++) { WeakReference<AnimationDrawableWrapper> reference = drawableList.get(i); AnimationDrawableWrapper wrapper = reference.get(); if (wrapper == null) { return; } wrapper.invalidateSelf(); } } @Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { this.scheduleTask = what; this.choreographer.postFrameCallbackDelayed(this, when - SystemClock.uptimeMillis()); } @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { this.scheduleTask = null; this.choreographer.removeFrameCallback(this); } @Override public void doFrame(long frameTimeNanos) { if(this.scheduleTask != null) { this.scheduleTask.run(); } } }
使用方法
int dp2px = (int) dp2px(100); MirrorFrameAnimation mirrorFrameAnimation = new MirrorFrameAnimation(getResources(),R.drawable.loading_animation,dp2px,dp2px); mirrorFrameAnimation.syncDrawable(imageView1); mirrorFrameAnimation.syncDrawable(imageView2); mirrorFrameAnimation.syncDrawable(imageView3); mirrorFrameAnimation.syncDrawable(imageView4); mirrorFrameAnimation.syncDrawable(imageView5); mirrorFrameAnimation.syncDrawable(imageView6); mStart.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mirrorFrameAnimation.start(); } }); mStop.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mirrorFrameAnimation.stop(); } });
適用范圍
圖像同步執(zhí)行需求
本篇我們實(shí)現(xiàn)了“同頻共幀動(dòng)效”,實(shí)際上這也是一種對稱動(dòng)畫的優(yōu)化方法。
我們常常會(huì)出現(xiàn)屏幕邊緣方向同時(shí)展示相同動(dòng)畫的問題,由于每個(gè)動(dòng)畫啟動(dòng)存在一定的延時(shí),以及控制邏輯不穩(wěn)定,往往會(huì)出現(xiàn)一邊動(dòng)畫播放結(jié)束,另一邊動(dòng)畫還在展示的情況。
總結(jié)
動(dòng)效一直是Android設(shè)備的上需要花大力氣優(yōu)化的,如果是圖像同步執(zhí)行、對稱動(dòng)效,本篇方案顯然可以幫助我們減少線程和內(nèi)存的消耗。
以上就是Android實(shí)現(xiàn)同頻共幀動(dòng)畫效果的詳細(xì)內(nèi)容,更多關(guān)于Android同頻共幀動(dòng)畫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺析Android中g(shù)etWidth()和getMeasuredWidth()的區(qū)別
這篇文章主要介紹了淺析Android中g(shù)etWidth()和getMeasuredWidth()的區(qū)別 ,getMeasuredWidth()獲取的是view原始的大小,getWidth()獲取的是這個(gè)view最終顯示的大小,具體區(qū)別介紹大家參考下本文2018-04-04android中打開相機(jī)、打開相冊進(jìn)行圖片的獲取示例
本篇文章主要介紹了android中打開相機(jī)、打開相冊進(jìn)行圖片的獲取示例,非常具有實(shí)用價(jià)值,需要的朋友可以參考下。2017-01-01Android判斷服務(wù)是否運(yùn)行及定位問題實(shí)例分析
這篇文章主要介紹了Android判斷服務(wù)是否運(yùn)行及定位問題,以實(shí)例形式較為詳細(xì)的分析了Android判斷服務(wù)運(yùn)行狀態(tài)及獲取經(jīng)緯度的相關(guān)實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09在Visual Studio上構(gòu)建C++的GUI框架wxWidgets的開發(fā)環(huán)境
這篇文章主要介紹了Visual Studio上構(gòu)件C++的GUI框架wxWidgets開發(fā)環(huán)境的方法,wxWidgets是一個(gè)跨多個(gè)系統(tǒng)平臺(tái)的圖形化界面開發(fā)框架,并且可用語言不限于C++,需要的朋友可以參考下2016-04-04Android實(shí)現(xiàn)將View保存成Bitmap的方法
這篇文章主要介紹了Android實(shí)現(xiàn)將View保存成Bitmap的方法,涉及Android畫布Canvas、位圖bitmap及View的相關(guān)使用技巧,需要的朋友可以參考下2016-06-06Android自定義View實(shí)現(xiàn)漸變色進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)漸變色進(jìn)度條,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05Android開發(fā) OpenGL ES繪制3D 圖形實(shí)例詳解
這篇文章主要介紹了Android開發(fā) OpenGL ES繪制3D 圖形實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2016-09-09