前言:卓越的人有一大优点就是在不利和艰难的遭遇里百折不挠。 ——贝多芬
一、概述
我们在上一篇文章中讲解了补间动画(TweenAnimation)的执行原理,我们这次来探讨一下属性动画的原理,属性动画与补间动画不同,属性动画是作用于控件属性的,正因为属性动画能够针对控件某一属性来做动画,所以他能够单独改变该控件属性的值。通常属性动画就ValueAniamtor和ObjectAniamator两个类比较常用,因为ObjectAniamator继承自ValueAniamtor,ValueAniamtor是整个属性动画的核心类,动画的驱动就在此类中实现,我们先从ValueAniamtor开始分析。
我们先来看看ValueAniamtor的使用:创建ValueAniamtor实例,添加AnimatorUpdateListener监听,可以得到动画执行的每一帧的值,然后根据返回值对View做相关的操作。
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 400);
valueAnimator.setDuration(2000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
mTextView.layout(value, value, value + mTextView.getWidth(), value + mTextView.getHeight());
Log.e(TAG, "value:" + value);
}
});
valueAnimator.start();
效果如下:
二、原理分析
1、ValueAnimator
我们先来看看ValueAnimator.ofInt(0, 400)中的代码,同时传入了动画的取值范围
public static ValueAnimator ofInt(int... values) {
ValueAnimator anim = new ValueAnimator();
anim.setIntValues(values);
return anim;
}
这里创建了ValueAnimator实例,返回这个实例,点击设置值的方法setIntValues()进去看看
public void setIntValues(int... values) {
if (values == null || values.length == 0) {
return;
}
if (mValues == null || mValues.length == 0) {
setValues(PropertyValuesHolder.ofInt("", values));
} else {
//这里是为了处理重复的设置
PropertyValuesHolder valuesHolder = mValues[0];
valuesHolder.setIntValues(values);
}
// New property/values/target should cause re-initialization prior to starting
mInitialized = false;
}
ofInt()提供的values最终被包装成一个PropertyValuesHolder,在ValueAnimator中操作的都是PropertyValuesHolder对象,PropertyValuesHolder以某种那个方式保存了values的值,
setValues(PropertyValuesHolder.ofInt("", values));
这里PropertyValuesHolder.ofInt(" ", values)传的参数字符串参数为" ",因为ValueAnimator并不提供目标对象,自然无法提供属性名称。
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;
}
setValues()其实是把PropertyValuesHolder对象保存起来,那么我们来看看PropertyValuesHolder.ofInt("", values)是怎样保存提供的values的
public static PropertyValuesHolder ofInt(String propertyName, int... values) {
return new IntPropertyValuesHolder(propertyName, values);
}
我们继续点进去看看IntPropertyValuesHolder,IntPropertyValuesHolder继承自PropertyValuesHolde,用于封装int类型的值,
public IntPropertyValuesHolder(String propertyName, int... values) {
super(propertyName);
setIntValues(values);
}
@Override
public void setIntValues(int... values) {
super.setIntValues(values);
mIntKeyframes = (Keyframes.IntKeyframes) mKeyframes;
}
这里调用了父类的setIntValues(),并且产生了一个mKeyframes对象,我们点进去父类的方法super.setIntValues(values)看看
public void setIntValues(int... values) {
mValueType = int.class;
mKeyframes = KeyframeSet.ofInt(values);
}
mValueType的作用是确定ObjectAnimator执行set方法时的参数类型,而values被封装成一个KeyframeSet,我们来看看KeyframeSet.ofInt(values)的实现,
public static KeyframeSet ofInt(int... values) {
int numKeyframes = values.length;
IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2)];
if (numKeyframes == 1) {
keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f);
keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]);
} else {
keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f, values[0]);
for (int i = 1; i < numKeyframes; ++i) {//将值拆分
keyframes[i] =
(IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]);
}
}
return new IntKeyframeSet(keyframes);
}
values中的每一个值都被封装成一个IntKeyframe ,IntKeyframeSet则保存一组IntKeyframe的集合。 keyframes[i] =(IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]);在创建关键帧的时候,第一个参数表示关键帧在整个区域之间的位置,第二个参数表示值是多少。i表示第几帧,numKeyframes 表示帧的数量总数。
@Override
public Object getValue(float fraction) {
return getIntValue(fraction);
}
动画在处理当前帧工作时会计算当前帧的动画进度,然后根据0-1的进度区间影射进度值,影射之后的数值就通过mKeyframes的getValue()里面取到的,mKeyframes 是一个KeyframeSet 对象,在创建属性动画时也顺带被创建,比如:KeyframeSet.ofInt(400),在这个方法里面400就是作为一个关键帧,什么叫做关键帧呢?很明显动画需要知道从哪里开始哪里结束,所以至少需要两个关键帧,这里只传入400,那么默认开始是0,400就是结束的关键帧,所以内部自己创建两个关键帧。这些数字在哪里使用到呢?都在下面这个方法里面使用了
@Override
public Object getValue(float fraction) {
return getIntValue(fraction);
}
@Override
public int getIntValue(float fraction) {
if (fraction <= 0f) {
final IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(0);
final IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(1);
int prevValue = prevKeyframe.getIntValue();
int nextValue = nextKeyframe.getIntValue();
float prevFraction = prevKeyframe.getFraction();
float nextFraction = nextKeyframe.getFraction();
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
if (interpolator != null) {
fraction = interpolator.getInterpolation(fraction);
}
float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
return mEvaluator == null ?
prevValue + (int)(intervalFraction * (nextValue - prevValue)) :
((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
intValue();
} else if (fraction >= 1f) {
final IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(mNumKeyframes - 2);
final IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(mNumKeyframes - 1);
int prevValue = prevKeyframe.getIntValue();
int nextValue = nextKeyframe.getIntValue();
float prevFraction = prevKeyframe.getFraction();
float nextFraction = nextKeyframe.getFraction();
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
if (interpolator != null) {
fraction = interpolator.getInterpolation(fraction);
}
float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
return mEvaluator == null ?
prevValue + (int)(intervalFraction * (nextValue - prevValue)) :
((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).intValue();
}
IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(0);
·······
// shouldn't get here
return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).intValue();
}
ValueAnimator.ofInt(400),当关键帧只有两帧,内部其实只创建了两个关键帧,一个起点0,一个终点400,上面显示了两帧和多于两帧的处理,那么,在这种只有两帧的情况下,将 0-1 的动画进度值转换成我们需要的 0-400 区间内的值;当关键帧超过两帧时,分三种情况来处理:第一帧的处理;最后一帧的处理;中间帧的处理;什么时候关键帧会超过两帧呢?其实也就是我们这么使用的时候:ValueAnimator.ofInt(0, 400, 0, -400, 0)
,类似这种用法的时候关键帧就不止两个了,这时候数量就是根据参数的个数来决定的了。
那么第一帧怎么处理呢,那在处理第一帧的工作时,只需要将第二帧当成是最后一帧,那么第一帧和第二帧这样也就可以看成是只有两帧的场景了吧。但是参数 fraction 动画进度是以实际第一帧到最后一帧计算出来的,所以需要先对它进行转换,换算出它在第一帧到第二帧之间的进度,接下去的逻辑也就跟处理两帧时的逻辑是一样的了。
同样的道理,在处理最后一帧时,只需要取出倒数第一帧跟倒数第二帧的信息,然后将进度换算到这两针之间的进度,接下去的处理逻辑也就是一样的了。
但处理中间帧的逻辑就不一样了,因为根据 0-1 的动画进度,我们可以很容易区分是处于第一帧还是最后一帧,无非一个就是 0,一个是 1。但是,当动画进度值在 0-1 之间时,我们并没有办法直接看出这个进度值是落在中间的哪两个关键帧之间,如果有办法计算出当前的动画进度处于哪两个关键帧之间,那么接下去的逻辑也就是一样的了,所以关键就是在于找出当前进度处于哪两个关键帧之间:
@Override
public int getIntValue(float fraction) {
if (fraction <= 0f) {
·········
IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(0);
for (int i = 1; i < mNumKeyframes; ++i) {
IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(i);
if (fraction < nextKeyframe.getFraction()) {
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
float intervalFraction = (fraction - prevKeyframe.getFraction()) /
(nextKeyframe.getFraction() - prevKeyframe.getFraction());
int prevValue = prevKeyframe.getIntValue();
int nextValue = nextKeyframe.getIntValue();
// Apply interpolator on the proportional duration.
if (interpolator != null) {
intervalFraction = interpolator.getInterpolation(intervalFraction);
}
return mEvaluator == null ?
prevValue + (int)(intervalFraction * (nextValue - prevValue)) :
((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
intValue();
}
prevKeyframe = nextKeyframe;
}
// shouldn't get here
return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).intValue();
}
系统的找法也很简单,从第一帧开始,按顺序遍历每一帧,然后去判断当前的动画进度跟这一帧保存的位置信息来找出当前进度是否就是落在某两个关键帧之间。因为每个关键帧保存的信息除了有它对应的值之外,还有一个是它在第一帧到最后一帧之间的哪个位置,至于这个位置的取值是什么,这就是由在创建这一系列关键帧时来控制的了。
还记得是在哪里创建了这一系列的关键帧的吧,回去 KeyframeSet 的ofInt()里看看:
public static KeyframeSet ofInt(int... values) {
int numKeyframes = values.length;
IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2)];
if (numKeyframes == 1) {
keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f);
keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]);
} else {
keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f, values[0]);
for (int i = 1; i < numKeyframes; ++i) {//将值拆分
keyframes[i] =
(IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]);
}
}
return new IntKeyframeSet(keyframes);
}
在创建每个关键帧时,传入了两个参数,第一个参数就是表示这个关键帧在整个区域之间的位置,第二参数就是它表示的值是多少。看上面的代码, i 表示的是第几帧,numKeyframes 表示的是关键帧的总数量,所以 i/(numKeyframes - 1) 也就是表示这一系列关键帧是按等比例来分配的。
比如说, ValueAnimator.ofInt(0, 50, 100, 200)
,这总共有四个关键帧,那么按等比例分配,第一帧就是在起点位置 0,第二帧在 1/3 位置,第三帧在 2/3 的位置,最后一帧就是在 1 的位置。
这里可以看到,ValueAnimator初始化时,同时把传入的每一个数值封装成IntKeyframe,并且保存在IntKeyframeSet中,IntKeyframeSet则是在PropertyValueHolder中初始化,初始化完成通过valueAnimator.setDuration(2000)等设置相关的属性,然后通过 valueAnimator.start()启动动画。
@Override
public void start() {
start(false);
}
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;
mSelfPulse = !mSuppressSelfPulseRequested;
// Special case: reversing from seek-to-0 should act as if not seeked at all.
if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
if (mRepeatCount == INFINITE) {
// Calculate the fraction of the current iteration.
float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
mSeekFraction = 1 - fraction;
} else {
mSeekFraction = 1 + mRepeatCount - mSeekFraction;
}
}
mStarted = true;
mPaused = false;
mRunning = false;
mAnimationEndRequested = false;
// Resets mLastFrameTime when start() is called, so that if the animation was running,
// calling start() would put the animation in the
// started-but-not-yet-reached-the-first-frame phase.
mLastFrameTime = -1;
mFirstFrameTime = -1;
mStartTime = -1;
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);
}
}
}
Looper.myLooper()的判断是确保在UI线程中执行,属性动画逐帧更新都是在handler来实现的,addAnimationCallback(0)点击进去
private void addAnimationCallback(long delay) {
if (!mSelfPulse) {
return;
}
getAnimationHandler().addAnimationFrameCallback(this, delay);
}
属性动画的更新都是靠handler不断发消息来实现的,我们来看一下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);
}
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}
if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}
这里是用来驱动动画的handler,mAnimationCallbacks是ArrayLiat,用于保存接口对象;SystemClock.uptimeMillis() + delay表示延迟动画执行的时间,第一次执行动画的时候,也就是mAnimationCallbacks.size() == 0,跟进去看看postFrameCallback(),
private AnimationFrameCallbackProvider getProvider() {
if (mProvider == null) {
mProvider = new MyFrameCallbackProvider();
}
return mProvider;
}
进入MyFrameCallbackProvider类:
public void postFrameCallback(FrameCallback callback) {
postFrameCallbackDelayed(callback, 0);
}
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
if (DEBUG_FRAMES) {
Log.d(TAG, "PostCallback: type=" + callbackType
+ ", action=" + action + ", token=" + token
+ ", delayMillis=" + delayMillis);
}
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
走到这里我们发现Choreographer的scheduleFrameLocked(now),里面是有关屏幕刷新的相关的,Animation的动画原理就是ViewRootImpl生成一个doTraversal()的Runnable()放到Choreographer队列里面的,这些队列都是在接收屏幕刷新信号时执行的,但是在接收信号前会先监听下一个刷新信号的事件。
那么在屏幕刷新信号后,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);
}
}
};
这里主要是两件事,一是去处理动画相关工作,找到动画真正执行的地方,二是向底层注册监听下一个屏幕刷新信号的消息;我们知道动画时一个持续的过程,每一帧都应该处理一个动画进度,直致动画结束。结合这段代码,我们可以知道:当第一个属性动画调用了start()后,mAnimationCallbacks这时大小为0,通过addAnimationFrameCallback()间接向底层注册屏幕刷新信号事件,然后将动画加入列表,当屏幕信号刷新时,mAnimationCallbacks的doFrame()被回调,里面去处理当前的动画,根据当列表是否为0而继续注册屏幕刷新信号事件,直到mAnimationCallbacks.size == 0为止。
private void doAnimationFrame(long frameTime) {
long currentTime = SystemClock.uptimeMillis();
final int size = mAnimationCallbacks.size();
for (int i = 0; i < size; i++) {
final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
if (callback == null) {
continue;
}
if (isCallbackDue(callback, currentTime)) {
callback.doAnimationFrame(frameTime);
if (mCommitCallbacks.contains(callback)) {
getProvider().postCommitCallback(new Runnable() {
@Override
public void run() {
commitAnimationFrame(callback, getProvider().getFrameTime());
}
});
}
}
}
cleanUpList();
}
这里循环遍历取出每一个ValueAnimator,isCallbackDue(callback, currentTime)判读动画是否有设置延迟开始,如果动画开始时间到了,则调用callback.doAnimationFrame(frameTime),然后cleanUpList()则是清除已经完成的动画,已经结束的动画需要被移除,等所有动画完成,列表大小就位0,就结束了注册下一个屏幕刷新信号事件
public final boolean doAnimationFrame(long frameTime) {
if (mStartTime < 0) {
mStartTime = mReversing? frameTime
: frameTime + (long) (mStartDelay * resolveDurationScale());
}
if (mPaused) {
mPauseTime = frameTime;
removeAnimationCallback();
return false;
} else if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
mStartTime += (frameTime - mPauseTime);
}
}
if (!mRunning) {
if (mStartTime > frameTime && mSeekFraction == -1) {
return false;
} else {
mRunning = true;
startAnimation();
}
}
if (mLastFrameTime < 0) {
if (mSeekFraction >= 0) {
long seekTime = (long) (getScaledDuration() * mSeekFraction);
mStartTime = frameTime - seekTime;
mSeekFraction = -1;
}
mStartTimeCommitted = false; // allow start time to be compensated for jank
}
mLastFrameTime = frameTime;
final long currentTime = Math.max(frameTime, mStartTime);
boolean finished = animateBasedOnTime(currentTime);
if (finished) {
endAnimation();
}
return finished;
}
这里主要做了几件事:1.mStartTime处理第一帧的动画的一些工作;2.根据动画时间来计算动画进度,主要逻辑在animateBasedOnTime(currentTime)里面,意义类似于Animation的getTransformation()方法;3.动画结束调用endAnimation()移除mAnimationCallbacks。
我们回到AnimationHandler的doAnimationFrame(long frameTime),在做了上面的操作后接着会做其他的处理commitAnimationFrame()
private void commitAnimationFrame(AnimationFrameCallback callback, long frameTime) {
if (!mDelayedCallbackStartTime.containsKey(callback) &&
mCommitCallbacks.contains(callback)) {
callback.commitAnimationFrame(frameTime);
mCommitCallbacks.remove(callback);
}
}
因为Aniamtor实现了AnimationFrameCallback接口,这里等于回调了Animator的方法,然后从列表中移除
public void commitAnimationFrame(long frameTime) {
if (!mStartTimeCommitted) {
mStartTimeCommitted = true;
long adjustment = frameTime - mLastFrameTime;
if (adjustment > 0) {
mStartTime += adjustment;
if (DEBUG) {
Log.d(TAG, "Adjusted start time by " + adjustment + " ms: " + toString());
}
}
}
}
这里就是修改第一帧的时间mStartTime,为什么要修改第一帧的时间呢?因为当动画太过于复杂的时候,等不到下一个刷新信号来到前,动画会根据之前记录的第一帧来计算动画进度,会出现丢了好几帧的情况,所以需要对动画的第一帧进行修正。
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
final long scaledDuration = getScaledDuration();
final float fraction = scaledDuration > 0 ?
(float)(currentTime - mStartTime) / scaledDuration : 1f;
final float lastFraction = mOverallFraction;
final boolean newIteration = (int) fraction > (int) lastFraction;
final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
(mRepeatCount != INFINITE);
if (scaledDuration == 0) {
// 0 duration animator, ignore the repeat count and skip to the end
done = true;
} else if (newIteration && !lastIterationFinished) {
// Time to repeat
if (mListeners != null) {
int numListeners = mListeners.size();
for (int i = 0; i < numListeners; ++i) {
mListeners.get(i).onAnimationRepeat(this);
}
}
} else if (lastIterationFinished) {
done = true;
}
mOverallFraction = clampFraction(fraction);
float currentIterationFraction = getCurrentIterationFraction(
mOverallFraction, mReversing);
animateValue(currentIterationFraction);
}
return done;
}
回到这里,这里计算动画帧的计算逻辑,根据当前时间以及动画第一帧时间和动画持续时长了计算动画进度,确保动画进度在0-1之间,动画逻辑计算完毕后应用到动画效果上面去,animateValue(currentIterationFraction)类似Animation动画中的applyTransformation();
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);
}
}
}
这里主要做了三件事,1.根据插值器来计算当前动画真正的进度;2.根据插值器得到的进度值来影射我们需要的进度值,因为经过插值器计算的进度值也是在0-1之间而已,我们需要的是实际具体的进度值数值,比如实际范围是0-300,根据进度值占比来换算具体的数值;3.通知动画进度回调。(有关进度值相关的资料可以参考Android动画篇(四)—— 属性动画ValueAnimator的高级进阶)
这部分工作主要是实现了mValues[i].calculateValue(fraction),mValues就是PropertyValuesHolder,
void calculateValue(float fraction) {
Object value = mKeyframes.getValue(fraction);
mAnimatedValue = mConverter == null ? value : mConverter.convert(value);
}
很明显value = mKeyframes.getValue(fraction)就是上面PropertyValuesHolder包装的数据,整个动画运行的原理就结束了。
总结:通过设置ValueAnimator.ofInt(0,100,300)来设置区域值,通过PropertyValuesHolder保存值,关键帧也是从里面得到的,start()启动动画,通过handler来不断发送消息,不断回调动画时间来计算动画实际进度,同时注册下一个屏幕刷新事件来刷新动画效果。
至此,本文结束!