目录
  • 同频共帧
  • 现状 & 痛点
    • 现状
    • 痛点
  • 原理
    • 实现
      • Drawable加壳
      • View更新
      • 更新AnimationDrawable
      • 完整代码
      • 使用方法
      • 适用范围
    • 总结

      同频共帧

      我们听过“同频共振”,其原理是多个物体物体以同样的频率振动,但是本篇实现的效果是“同频共帧”,含义是:动画以同样的频率和同样的帧展示在多个不同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: 目前支持功能最多的动效引擎,普通动画性能也非常不错,相比其他引擎快很多。该引擎使用自研渲染引擎和解码器实现,但是对于预合成动效(超长动效和复杂动效可能会用到),由于其使用的是软解,在低配设备上比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同频共帧动画的资料请关注其它相关文章!

      声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。