Android中的基础动画 属性动画(Property Animation)

导读

  • 移动开发知识体系总章(Java基础、Android、Flutter)
  • Android 动画的分类及介绍
  • Android中的视图动画(View Animation)(View动画、补间动画)
  • Android中的逐帧动画(Drawable Animation)
  • Android中的基础动画 属性动画(Property Animation)

Android中的基础动画 属性动画(Property Animation)

通过前面的文章Android中的视图动画(View Animation)(View动画、补间动画)我们知道视图动画只是改变了View的视觉效果,而实际并未变更,而属性动画可谓是视图动画的加强版,并且具有更好的特性,因为属性动画不仅改变了视觉效果,而且实际也跟随变动了,并保留了视图动画如监听等功能。

举个不恰当的例子:
彭空空做梦赚了一卡车的人民币,实际收入呢,0元;
马云大佬做梦赚了一卡车的人民币,实际收入一卡车的人民币。

这里我写了两段简单的代码,一个是属性动画,一个是补间动画,效果均是让Button平移的效果,并对两个Button设置了点击事件,以下是关键代码:

private void showTweenAnim() {
        Animation translateAnimation = new TranslateAnimation(0, 800, 0, 0);
        translateAnimation.setDuration(3000);
        translateAnimation.setRepeatCount(-1);
        button2.startAnimation(translateAnimation);
    }
private void showObjectAnimatorOfFloat() {
        ObjectAnimator animator = ObjectAnimator.ofFloat(button1, "translationX", 0, 500);
        animator.setDuration(3000);
        animator.setRepeatCount(-1);
        animator.start();
    }
属性动画和补间动画

这是以上代码的动画效果和点击事件的响应,可以看到Button从左边移动到右边,属性动画一直可以响应点击事件,而补间动画只有在原来的位置才响应事件。

再看两个动画的代码,对比发现,除了构造(静态方法)基本上一致,而我们知道补间动画要实现平移、选择、缩放、透明度的动画,分别需要TranslateAnimation(平移动画)、RotateAnimation(旋转动画)、ScaleAnimation(缩放动画)、AlphaAnimation(透明度动画)这些类,而上面的代码中,属性动画使用了ObjectAnimator的ofFloat()方法,后面传入关键参数“translationX”,就实现了平移动画,看来属性动画内部进行了扩展性的封装,这里就不去具体研究是如何封装的了。

下面具体来看看ObjectAnimator.ofFloat()方法:

 public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
        ObjectAnimator anim = new ObjectAnimator(target, propertyName);
        anim.setIntValues(values);
        return anim;
    }
  • new ObjectAnimator(target, propertyName)
    ofInt(Object target, String propertyName, int... values)方法接收三个参数。先把前两个参数传递给了ObjectAnimator构造方法:
    private ObjectAnimator(Object target, String propertyName) {
            setTarget(target);
            setPropertyName(propertyName);
        }
    
    构造方法里先是执行了setTarget(target)方法,根据命名可以看出是进行绑定的操作的,内部进行了一个oldTarget的比对,并且内部使用到了弱引用,这里贴出代码来:
      @Override
        public void setTarget(@Nullable Object target) {
            final Object oldTarget = getTarget();
            if (oldTarget != target) {
                if (isStarted()) {
                    cancel();
                }
                mTarget = target == null ? null : new WeakReference(target);
                // New target should cause re-initialization prior to starting
                mInitialized = false;
            }
        }
     
       

    引用类型是java中比较重要的概念,可查看Java引用类型

    • setPropertyName(propertyName)方法:
        public void setPropertyName(@NonNull String propertyName) {
            // mValues could be null if this is being constructed piecemeal. Just record the
            // propertyName to be used later when setValues() is called if so.
            if (mValues != null) {
                PropertyValuesHolder valuesHolder = mValues[0];
                String oldName = valuesHolder.getPropertyName();
                valuesHolder.setPropertyName(propertyName);
                mValuesMap.remove(oldName);
                mValuesMap.put(propertyName, valuesHolder);
            }
            mPropertyName = propertyName;
            // New property/values/target should cause re-initialization prior to starting
            mInitialized = false;
        }
      
      这里这里先对mValues进行了非空判断,如果不为空,就会把mValues的第一个参数取出来作为PropertyValuesHolder,并绑定propertyName,再存储到mValuesMap,进行了存储,那么PropertyValuesHolder是什么呢:
       /**
        * This class holds information about a property and the values that that property
        * should take on during an animation. PropertyValuesHolder objects can be used to create
        * animations with ValueAnimator or ObjectAnimator that operate on several different properties
        * in parallel.
        */
       public class PropertyValuesHolder implements Cloneable {...}
    

    通过注释,我们得知PropertyValuesHolder是用来封装属性相关的变量:


    PropertyValuesHolder的相关变量
    • setFloatValues(float... values)方法:
       @Override
        public void setFloatValues(float... values) {
            if (mValues == null || mValues.length == 0) {
                // No values yet - this animator is being constructed piecemeal. Init the values with
                // whatever the current propertyName is
                if (mProperty != null) {
                    setValues(PropertyValuesHolder.ofFloat(mProperty, values));
                } else {
                    setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
                }
            } else {
                super.setFloatValues(values);
            }
        }
      
      以及父类ValueAnimator的setFloatValues(float... values)方法:
        public void setFloatValues(float... values) {
            if (values == null || values.length == 0) {
                return;
            }
            if (mValues == null || mValues.length == 0) {
                setValues(PropertyValuesHolder.ofFloat("", values));
            } else {
                PropertyValuesHolder valuesHolder = mValues[0];
                valuesHolder.setFloatValues(values);
            }
            // New property/values/target should cause re-initialization prior to starting
            mInitialized = false;
        }
      
      关注两个方法:
      • PropertyValuesHolder.ofFloat("", values)
        可以看到,子类和父类的setFloatValues(float... values)方法,最终都会执行setValues方法,这里先追踪PropertyValuesHolder.ofFloat(),由于跳转较多,这里贴出最终的核心代码:(Keyframes接口的实现类KeyframeSet中的一段核心代码)
           public static KeyframeSet ofFloat(float... values) {
                  boolean badValue = false;
                  int numKeyframes = values.length;
                  FloatKeyframe keyframes[] = new               FloatKeyframe[Math.max(numKeyframes,2)];
                if (numKeyframes == 1) {
                      keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
                      keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
                      if (Float.isNaN(values[0])) {
                          badValue = true;
                      }
                  } else {
                      keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
                      for (int i = 1; i < numKeyframes; ++i) {
                          keyframes[i] =
                                  (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
                      if (Float.isNaN(values[i])) {
                          badValue = true;
                      }
                  }
              }
              if (badValue) {
                  Log.w("Animator", "Bad value (NaN) in float animator");
              }
              return new FloatKeyframeSet(keyframes);
          }
        
        这段代码是要把传递过来的values进行FloatKeyframe转换为2个帧,这就是前面说到的开始帧(开始状态)、和结束帧(结束状态)。
      • setValues(PropertyValuesHolder... values)
           public void setValues(PropertyValuesHolder... values) {
             int numValues = values.length;
             mValues = values;
             mValuesMap = new HashMap(numValues);
             for (int i = 0; i < numValues; ++i) {
                 PropertyValuesHolder valuesHolder = values[i];
                 mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);
             }
             // New property/values/target should cause re-initialization prior to starting
             mInitialized = false;
         }
        
        最终是把values的所有参数取出来,作为PropertyValuesHolder存储到了mValuesMap中。

    到这里,准备工作就做完了,即为target准备propertyName的动画,把传递过来的values转换为系统识别的开始帧、结束帧。


    setDuration()、setRepeatCount()、就不展开了,主要看看start()方法:

        @Override
        public void start() {
            AnimationHandler.getInstance().autoCancelBasedOn(this);
            if (DBG) {...}
            super.start();
        }
    

    新出现一个以Handler命名的类AnimationHandler,由于后面是getInstance()方法,我们大胆猜测这是一个单例,单列模式是编程中常见的设计模式,可查看单例的相关知识。跟着后面的autoCancelBasedOn()顾明思意应该就是用于动画取消,保证即将执行的动画的唯一性,这里也不展开了,先看看AnimationHandler的定义:

      /**
       * This custom, static handler handles the timing pulse that is shared by all active
       * ValueAnimators. This approach ensures that the setting of animation values will happen on the
       * same thread that animations start on, and that all animations will share the same times for
       * calculating their values, which makes synchronizing animations possible.
       *
       * The handler uses the Choreographer by default for doing periodic callbacks. A custom
       * AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
       * may be independent of UI frame update. This could be useful in testing.
       *
       * @hide
       */
      public class AnimationHandler {}
    

    大概意思是说AnimationHandler主要是用于处理所有活动的属性动画共享的“时间脉冲”,这个时间脉冲即从开始到结束每个时间段的“值”,AnimationHandler保证了一个动画的完整播放都是发生在同一个线程,该处理程序默认情况下使用Choreographer进行定期回调。 可以在处理程序上设置自定义AnimationFrameCallbackProvider,以提供可能独立于UI框架更新的定时脉冲。由于该类非常重要,所以后面还会涉及到该类下的其他方法。

    知道了AnimationHandler具有重要的管理的作用后,继续追踪父类ValueAnimator的start()方法:

       private void start(boolean playBackwards) {
            if (Looper.myLooper() == null) {
                throw new AndroidRuntimeException("Animators may only be run on Looper threads");
            }
            ...
            addAnimationCallback(0);
            if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
                // If there's no start delay, init the animation and notify start listeners right away
                // to be consistent with the previous behavior. Otherwise, postpone this until the first
                // frame after the start delay.
                startAnimation();
                if (mSeekFraction == -1) {
                    // No seek, start at play time 0. Note that the reason we are not using fraction 0
                    // is because for animations with 0 duration, we want to be consistent with pre-N
                    // behavior: skip to the final value immediately.
                    setCurrentPlayTime(0);
                } else {
                    setCurrentFraction(mSeekFraction);
                }
            }
        }
    

    在start()方法中关注这些:

    • 对Looper的非空判断
      Looper是Android中非常重要的类,可查看有关Looper的相关文章,

    • addAnimationCallback(0)

         private void addAnimationCallback(long delay) {
              ...
              getAnimationHandler().addAnimationFrameCallback(this, delay);
          }
      

      这里拿到了具有管理功能的AnimationHandler单例对象,并且前面说到AnimationHandler是很重要的类,那么这里深入看看这里添加的回调:

           /**
           *  Register to get a callback on the next frame after the delay.
           */
          public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
              if (mAnimationCallbacks.size() == 0) {
                  getProvider().postFrameCallback(mFrameCallback);
              }
              ...
          }
      

      方法注释说,注册以获取延迟后下一帧的回调,然后方法中执行了postFrameCallback(mFrameCallback)方法,这里重点关注mFrameCallback:

          private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
              @Override
              public void doFrame(long frameTimeNanos) {
                  doAnimationFrame(getProvider().getFrameTime());
                  if (mAnimationCallbacks.size() > 0) {
                      getProvider().postFrameCallback(this);
                  }
              }
          };
      

      这里有出现了前面提到的类:Choreographer

        /**
         * Coordinates the timing of animations, input and drawing.
         *...
         */
        public final class Choreographer {
            /**
           * Implement this interface to receive a callback when a new display frame is
           * being rendered.  The callback is invoked on the {@link Looper} thread to
           * which the {@link Choreographer} is attached.
           */
          public interface FrameCallback {
              /**
               * Called when a new display frame is being rendered.
               * ...
               */
              public void doFrame(long frameTimeNanos);
          }
        }
      

      由于注释都很长,这里贴出关键注释和其含义:

      • class Choreographer :*协调动画,输入和绘图的时间。
      • interface FrameCallback :实现此接口以在呈现新的显示框架时接收回调。
      • void doFrame :在渲染新的显示框架时调用。

      Choreographer翻译过来是“编舞”的意思,舞蹈其实就是一个动作一个动作的组合,而动画也是一帧一帧的组合,而通过上面的注释来看,即在动画中每次渲染的时候Choreographer都会执行FrameCallback接口的doFrame,似乎确实有“编舞”之意。既然如此,我们来验证一下是不是每次渲染时都要调用该回调:

    1、以Debug模式运行
    2、在start()方法上打上断点
    3、在动画中添加addUpdateListener监听,并打上断点
    4、在AnimationHandler类下的mFrameCallback中打上断点
    5、点击执行动画
    6、通过点击resume跳到下一个断点,


    调试1.png

    调试2.png

    这里需要注意 doAnimationFrame 的断点,必须要在后面打上,而不是一开始打上
    通过debug我们会发现,doAnimationFrame之后就会调用addUpdateListener,然后重复如此,一直到动画结束,并且越简单的动画,重复次数越少,反之则重复次数越多,这里可以通过setDuration(30)和setDuration(3000)进行对比。
    整个流程就是:通过getAnimationHandler().addAnimationFrameCallback(this, delay)进行回调绑定,这个回调就是父类ValueAnimator类实现的AnimationHandler类中的AnimationFrameCallback回调,AnimationFrameCallback中的方法doAnimationFrame()在Choreographer类的FrameCallback回调中的方法doFrame()中被执行。

    到这里的结论就是Choreographer通过调用doAnimationFrame()来驱动动画执行每一个关键帧。

    • startAnimation():

        private void startAnimation() {
            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                        System.identityHashCode(this));
            }
      
            mAnimationEndRequested = false;
            initAnimation();
            mRunning = true;
            if (mSeekFraction >= 0) {
                mOverallFraction = mSeekFraction;
            } else {
                mOverallFraction = 0f;
            }
            if (mListeners != null) {
                notifyStartListeners();
            }
        }
      
         void initAnimation() {
              if (!mInitialized) {
                  int numValues = mValues.length;
                  for (int i = 0; i < numValues; ++i) {
                      mValues[i].init();
                  }
                  mInitialized = true;
              }
          }
      

      在前文中我们知道mValues其实就是PropertyValuesHolder,也就是说 initAnimation的目的是初始化PropertyValuesHolder:

          void init() {
              if (mEvaluator == null) {
                  // We already handle int and float automatically, but not their Object
                  // equivalents
                  mEvaluator = (mValueType == Integer.class) ? sIntEvaluator :
                          (mValueType == Float.class) ? sFloatEvaluator :
                          null;
              }
              if (mEvaluator != null) {
                  // KeyframeSet knows how to evaluate the common types - only give it a custom
                  // evaluator if one has been set on this class
                  mKeyframes.setEvaluator(mEvaluator);
              }
          }
      

      这里mEvaluator进行了三目运算,由于前面我们执行的是ObjectAnimator.ofFloat(),所以mEvaluator就是sFloatEvaluator,这里就涉及到了估值器

          private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator();
      
          public class FloatEvaluator implements TypeEvaluator {
              public Float evaluate(float fraction, Number startValue, Number endValue) {
                  float startFloat = startValue.floatValue();
                  return startFloat + fraction * (endValue.floatValue() - startFloat);
              }
          }
      

      init()方法的目的是初始化PropertyValuesHolder,初始化的时候,会确定具体的估值器,这个float类型的估值器只有一个evaluate()方法,返回线性插入起始值和结束值的结果,就是根据时间的变化规律计算得到每一步的运算结果。关于估值器和插值器的相关文章

      这里需要先说一下mKeyframes怎么来的呢,正是前面提到的setIntValues方法中执行的KeyframeSet.ofInt(values)。

         public void setIntValues(int... values) {
              mValueType = int.class;
              mKeyframes = KeyframeSet.ofInt(values);
          }
      
    • setCurrentPlayTime():

          public void setCurrentPlayTime(long playTime) {
              float fraction = mDuration > 0 ? (float) playTime / mDuration : 1;
              setCurrentFraction(fraction);
          }
      
         public void setCurrentFraction(float fraction) {
              initAnimation();
              fraction = clampFraction(fraction);
              mStartTimeCommitted = true; // do not allow start time to be compensated for jank
              if (isPulsingInternal()) {
                  long seekTime = (long) (getScaledDuration() * fraction);
                  long currentTime = AnimationUtils.currentAnimationTimeMillis();
                  // Only modify the start time when the animation is running. Seek fraction will ensure
                  // non-running animations skip to the correct start time.
                  mStartTime = currentTime - seekTime;
              } else {
                  // If the animation loop hasn't started, or during start delay, the startTime will be
                  // adjusted once the delay has passed based on seek fraction.
                  mSeekFraction = fraction;
              }
              mOverallFraction = fraction;
              final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
              animateValue(currentIterationFraction);
          }
      

      setCurrentFraction()方法基本都是时间的计算,最后执行了animateValue()方法,这里先贴上ObjectAnimator类的该方法:

        void animateValue(float fraction) {
              final Object target = getTarget();
              ...
              super.animateValue(fraction);
              int numValues = mValues.length;
              for (int i = 0; i < numValues; ++i) {
                  mValues[i].setAnimatedValue(target);
              }
          }
      

    在ObjectAnimator类的animateValue()方法中,需要注意

    • super.animateValue(fraction);即执行父类的animateValue(),

         void animateValue(float fraction) {
            fraction = mInterpolator.getInterpolation(fraction);
            mCurrentFraction = fraction;
            int numValues = mValues.length;
            for (int i = 0; i < numValues; ++i) {
                mValues[i].calculateValue(fraction);
            }
            if (mUpdateListeners != null) {
                int numListeners = mUpdateListeners.size();
                for (int i = 0; i < numListeners; ++i) {
                    mUpdateListeners.get(i).onAnimationUpdate(this);
                }
            }
        }
      

      mInterpolator.getInterpolation(fraction);是获取时间插值器,
      mValues[i].calculateValue(fraction);是将时间插值送给估值器,计算出 values

    • mValues[i].setAnimatedValue(target);

        void setAnimatedValue(Object target) {
            if (mProperty != null) {
                mProperty.set(target, getAnimatedValue());
            }
            if (mSetter != null) {
                try {
                    mTmpValueArray[0] = getAnimatedValue();
                    mSetter.invoke(target, mTmpValueArray);
                } catch (InvocationTargetException e) {
                    Log.e("PropertyValuesHolder", e.toString());
                } catch (IllegalAccessException e) {
                    Log.e("PropertyValuesHolder", e.toString());
                }
            }
        }
      

      注意,这个setAnimatedValue()方法是PropertyValuesHolder类的,可以看到mSetter.invoke(target, mTmpValueArray)这行代码通过反射进行了属性值的修改。
      也就是说setCurrentPlayTime()方法的目的1,是获取时间插值器值和估值器,2,改变target的属性

    至此,动画的第一帧就执行完毕了。


    我们通过ObjectAnimator.ofFloat()方法,查看了跟踪查看了整个属性动画的机制。这里贴出动画机制的相关方法,由于的这个图片压缩的太狠,最后只有这张图勉强能看清(建议右键,在新页面打开图片):


    ValueAnimator.png

    我们得出一些结论:

    • 属性动画和我们生活中的动画一样,都是由一帧一帧构成的。
    • 属性动画需要优先计算出开始帧和结束帧。
    • 属性动画通过start调用执行动画,背后会进行一系列的工作。
      • 属性动画依靠监听 Choreographer使得其可以不断地调用 doAnimationFrame() 来驱动动画执行每一个关键帧。
      • 每一次的 doAnimationFrame() 调用都会去计算时间插值,而通过时间插值器计算得到 fraction 又会传给估值器,使得估值器可以计算出属性的当前值。
      • PropertyValuesHolder作为属性动画的变量封装和管理和以及通过反射修改目标属性的值。

    当然,从简单的角度来说动画机制就是如此这般了,但是这只是粗颗粒而言,上文中海油很多细节并没有展开,比如如何保证动画唯一性、动画的相关时间是如何计算的、比如插值器和估值器是怎么工作的、Choreographer又是如何不断调用的等等问题,后续我会根据时间情况慢慢梳理出来。

    你可能感兴趣的:(Android中的基础动画 属性动画(Property Animation))