同频共帧
咱们听过“同频共振”,其原理是多个物体物体以相同的频率振荡,可是本篇完成的作用是“同频共帧”,意义是:动画以相同的频率和相同的帧展现在多个不同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: 目前支撑功用最多的动效引擎,普通动画功能也非常不错,运用C++和自研引擎完成,可是对于预组成动效(超长动效和杂乱动效或许会用到),因为其运用的是软解,在低配设备上比VapPlayer和AlphaPlayer卡的多。
VapPlayer/AlphaPlayer : 这两种其都是经过alpha 遮罩完成,大部分情况下运用的是设备硬解码器,不过,VapPlayer短少硬解码器筛选机制,偶然有时会拿到软解码器,别的其自身存在音画同步问题,至于AlphaPlayer把播映交给系统和三方播映器,避免了此类问题。可是,假如你的app是音视频类app,他们都有一起的问题,便是终端设备上,硬解码器的实例数量是受限制的,往往低配设备上,运用这两种解码器就会形成绿屏、起播失败、解码器卡住等问题。
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设备的上需求花大力气优化的,假如是图画同步履行、对称动效,本篇计划明显能够帮助咱们削减线程和内存的消耗。