同频共帧

咱们听过“同频共振”,其原理是多个物体物体以相同的频率振荡,可是本篇完成的作用是“同频共帧”,意义是:动画以相同的频率和相同的帧展现在多个不同View上。

特点:

  • 动画作用
  • 相同的频率
  • 相同的帧 (严格意义上是小于1个vsync信号的帧)
  • 多个不同View一起展现

Android 同频共帧动画完成

之前的文章中咱们完成了许多动效,但简直都是根据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设备的上需求花大力气优化的,假如是图画同步履行、对称动效,本篇计划明显能够帮助咱们削减线程和内存的消耗。