导读
- 移动开发知识体系总章(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构造方法:
构造方法里先是执行了setTarget(target)方法,根据命名可以看出是进行绑定的操作的,内部进行了一个oldTarget的比对,并且内部使用到了弱引用,这里贴出代码来:private ObjectAnimator(Object target, String propertyName) { setTarget(target); setPropertyName(propertyName); }
@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)方法:
这里这里先对mValues进行了非空判断,如果不为空,就会把mValues的第一个参数取出来作为PropertyValuesHolder,并绑定propertyName,再存储到mValuesMap,进行了存储,那么PropertyValuesHolder是什么呢: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; }
/**
* 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是用来封装属性相关的变量:
- setFloatValues(float... values)方法:
以及父类ValueAnimator的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); } }
关注两个方法: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中的一段核心代码)
这段代码是要把传递过来的values进行FloatKeyframe转换为2个帧,这就是前面说到的开始帧(开始状态)、和结束帧(结束状态)。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); }
- setValues(PropertyValuesHolder... values)
最终是把values的所有参数取出来,作为PropertyValuesHolder存储到了mValuesMap中。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; }
- PropertyValuesHolder.ofFloat("", values)
到这里,准备工作就做完了,即为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跳到下一个断点,
这里需要注意 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()方法,查看了跟踪查看了整个属性动画的机制。这里贴出动画机制的相关方法,由于简书的这个图片压缩的太狠,最后只有这张图勉强能看清(建议右键,在新页面打开图片):
我们得出一些结论:
- 属性动画和我们生活中的动画一样,都是由一帧一帧构成的。
- 属性动画需要优先计算出开始帧和结束帧。
- 属性动画通过start调用执行动画,背后会进行一系列的工作。
- 属性动画依靠监听 Choreographer使得其可以不断地调用 doAnimationFrame() 来驱动动画执行每一个关键帧。
- 每一次的 doAnimationFrame() 调用都会去计算时间插值,而通过时间插值器计算得到 fraction 又会传给估值器,使得估值器可以计算出属性的当前值。
- PropertyValuesHolder作为属性动画的变量封装和管理和以及通过反射修改目标属性的值。
当然,从简单的角度来说动画机制就是如此这般了,但是这只是粗颗粒而言,上文中海油很多细节并没有展开,比如如何保证动画唯一性、动画的相关时间是如何计算的、比如插值器和估值器是怎么工作的、Choreographer又是如何不断调用的等等问题,后续我会根据时间情况慢慢梳理出来。