同频共帧
我们听过“同频共振”,其原理是多个物体物体以同样的频率振动,但是本篇实现的效果是“同频共帧”,含义是:动画以同样的频率和同样的帧展示在多个不同view上。
特点:
- 动画效果
- 同样的频率
- 同样的帧 (严格意义上是小于1个vsync信号的帧)
- 多个不同view同时展示
之前的文章中我们实现了很多动效,但几乎都是基于view本身实现的,但是在android中,drawable最容易扩展的动效工具,通过drawable提供的接口,我们可以接入libpag、lottie、svg、apng、gif,lazyanimationdrawable、animationdrawable等动效,更加方便移植,同时drawable支持sethotspot和setstate接口,可以实现复杂度较低的交互效果。
这种动效其实在手机版qq上就有,如果你给自己的头像设置为一个动态图,那么在群聊连发多条消息,那么你就会发现,在同一个页面上你的头像动画是同步展示的。
现状 & 痛点
现状
我们以帧动画问题展开,要知道帧动画有难以容忍的内存占用问题、以及主线程解码问题,同时包体积问题也相当严重,为此市面上出现了很多方案。libpag、lottie、vapplayer、alphaplayer、apng、gif、svga、animationdrawable等。但你在开发时就会发现,每一种引擎都有自己独特的优势,也有自己独特的劣势,你往往想着用一种引擎统一所有动效实现,但往往现实不允许。
我们来说说几大引擎的优缺点:
libpag: 目前支持功能最多的动效引擎,普通动画性能也非常不错,相比其他引擎快很多。该引擎使用自研渲染引擎和解码器实现,但是对于预合成动效(超长动效和复杂动效可能会用到),由于其使用的是软解,在低配设备上比vapplayer和alphaplayer卡的多,另外lib so相比其他引擎也是大很多。
vapplayer/alphaplayer : 这两种其都是通过alpha 遮罩实现,大部分情况下使用的是设备硬解码器,不过,vapplayer缺乏硬解码器筛选机制,偶尔有时会拿到软解码器,另外其本身存在音画同步问题,至于alphaplayer把播放交给系统和三方播放器,避免了此类问题。但是,如果是音视频类app,他们都有共同的问题,终端设备上硬解码器的实例数量是受限制的,甚至有的设备解码器同一时刻只能使用一个,使用这两种解码器就会造成业务播放器绿屏、起播失败、解码器卡住等问题。不过解决办法是将特效和业务播放器资源类型隔离,如果业务播放器是使用h264,那么你可以动效使用h264\mpeg2等其他编码类型。
lottie: lottie目前是比较广为人知的动效引擎,使用也相当广泛。但其存在跨平台兼容性,缺少很多特效,其性能是不如libpag的,不过总体能覆盖到大部分场景。另一个开发中常常会遇到的问题是,ui设计人员对于lottie的compose layer理解存在问题,往往会出现将lottie动画做成和帧动画一样的动画,显然,compose layer的思想是多张图片合成,那就意味着图片本身应该有大有小,按一定轨迹运动和渐变,而不是一帧一帧简单播放。
apng、gif : 这类动画属于资源型动画,其本身存在很多缺点,比如占内存和耗cpu,不过简单的场景还是可以使用的。
svga:很多平台对这种动画抱有期待,特别是其矢量性质和低内存的特点,然而,其本身面临标准不统一的问题,造成跨平台的能力不足。
lazyanimationdrawable:几乎所有的动画对低配设备都不友好,帧动画比上不足比下有余,低配设备上,为了解决libpag、vapplayer、lottie对低配设备上音视频类app不友好的问题,使用animationdrawble显然是不行的,因此我们往往会实现了自己的animationdrawable,使其具备兜底的能力: 异步解码 + 展示一帧预加载下一帧的能力,其实也就是lazyanimationdrawable。
痛点
以上我们罗列了很多问题,看似和我们的主要目的毫无关系,其实我们可以想想,如果使用上述引擎,哪种方式可以实现兼容性更好的“同频共帧”动效呢 ?
实际上,几乎没有引擎能承担此任务,那有没有办法实现呢?
原理
我们很难让每个view同时执行和绘制同样的画面,另一个问题是,如果设计多个view绘制bitmap,那么还可能造成资源加载的内存oom的问题。另外一方面如果使用lazyanimationdrawable、vapx、alphaplayer等 ,如果同时执行,那么解码线程需要创建多个,显然性能问题也是重中之重。
有没有更加简单方法呢 ?
实际上是有的,那就是投影。
我们无论使用compositedrawable、lazyanimationdrawable、animationdrawable还是vectordrawable,我们可以保证在使用个实例的情况下,将画面绘制到不同view上即可。
不过:本篇以animationdrawable 为例子实现,其实其他drawable动画类似。
实现
但是这种难度也是很高的,如果我们使用一个view 管理器,然后构建一个播放器,显然还要处理view各种状态,显然避免不了耦合问题。这里我们回到开头说过的drawable方案,当然,一个drawable显然无法设置给多个view,这点显然是我们需要处理的难点,此外,每个view的大小也不一致,如何处理这种问题呢。
drawable加壳
我们参考glide中com.bumptech.glide.request.target.fixedsizedrawable 实现,其原理是通过fixedsizedrawable代理真实的drawble绘制,然后利用matrix实现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 转换
当然,必要时支持下alpha和colorfilter,下面是完整实现。
public static class animationdrawablewrapper extends drawable { private final animationdrawable animationdrawable; //动画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()); // 变化坐标 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更新机制的开发者都知道,每一个view都实现了drawable.callback,当给view设置drawable时,drawable.callback也会设置给drawable。
drawable刷新view时需要调用invalidate,显然是通过drawable.callback实现,当然,drawable自身就实现了更新方法drawable#invalidateself,我们只需要调用改方法刷新view即可触发view#ondraw,从而触发drawable#draw方法。
public void invalidateself() { final callback callback = getcallback(); if (callback != null) { callback.invalidatedrawable(this); } }
更新animationdrawable
显然,任何动画都具备时间属性,因此更新drawable是必要的,view本身是可以通过drawable.callback机制更新drawable的。通过scheduledrawable和unscheduledrawable 定时处理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实现了runnable接口
@override public void run() { nextframe(false); }
然而,如果使用的recyclerview,那么还可能会出现view 从页面移除的问题,因此依靠view显然是不行的,这里我们引入handler或者choreograper。
this.choreographer = choreographer.getinstance();
但是,我们什么时候调用呢?显然还得利用drawable.callback机制
给animationdrawable设置drawable.callback
this.drawable.setcallback(callback);
更新逻辑实现
@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需要实现的
@override public void doframe(long frametimenanos) { if(this.scheduletask != null) { this.scheduletask.run(); } }
好了,以上就是核心逻辑,到此我们就实现了核心逻辑
完整代码
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) { //设置宽高,防止animationdrawable大小不稳定问题 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(); } });
适用范围
图像同步执行需求
本篇我们实现了“同频共帧动效”,实际上这也是一种对称动画的优化方法。
我们常常会出现屏幕边缘方向同时展示相同动画的问题,由于每个动画启动存在一定的延时,以及控制逻辑不稳定,往往会出现一边动画播放结束,另一边动画还在展示的情况。
总结
动效一直是android设备的上需要花大力气优化的,如果是图像同步执行、对称动效,本篇方案显然可以帮助我们减少线程和内存的消耗。
以上就是android实现同频共帧动画效果的详细内容,更多关于android同频共帧动画的资料请关注代码网其它相关文章!
发表评论