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<Object>(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<String, PropertyValuesHolder>(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<Number> {
            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又是如何不断调用的等等问题,后续我会根据时间情况慢慢梳理出来。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343