This article assumes that you already have some familiarity with property animation, or at least use it. Let’s start with the simplest use of property animation.
ObjectAnimator
.ofInt(target,propName,values[])
.setInterpolator(LinearInterpolator)
.setEvaluator(IntEvaluator)
.setDuration(500)
.start();Copy the code
Interpolator(interpolator) setInterpolator(interpolator) , setEvaluator (…). , setDuration (…). How it is used in the source code. In addition, we’ll focus on how Android property animations are implemented step by step (down to frame by frame). Finally, the code used in this article is Android 4.2.2.
The above code generates an animation of the View property, which according to ofInt() is an animation of the View property of type Int. Let the other functions pass first, starting with ObjectAnimator’s start () function.
public void start() { //... Omit unnecessary code super.start(); }Copy the code
We know from the code that it calls the parent’s start() method, valueanimator.start (). This method calls the start(Boolean playBackwards) method inside its own class.
/** * this method starts playing animation. The start() method uses a Boolean value playBackwards to determine whether animations need to be played back. This value is usually false, but it can also be true when called from the reverse() method. One thing to note is that this method must be called from the UI main thread. */ private void start(boolean playBackwards) { if (Looper.myLooper() == null) { throw new AndroidRuntimeException("Animators may only be run on Looper threads"); } mPlayingBackwards = playBackwards; mCurrentIteration = 0; mPlayingState = STOPPED; mStarted = true; mStartedDelay = false; AnimationHandler animationHandler = getOrCreateAnimationHandler(); animationHandler.mPendingAnimations.add(this); If (mStartDelay == 0) {setCurrentPlayTime(0); mPlayingState = STOPPED; mRunning = true; notifyStartListeners(); } animationHandler.start(); }Copy the code
Interpretation of several values in the code:
- MPlayingStated represents the current state of the animation. Used to find out when to start the animation (if state == STOPPED). It is also used when the animator is called cancel() or end() to stop it at the last frame of the animation. Possible values are STOPPED, RUNNING, SEEKED.
- MStarted is an additional play-state value in the Animator that indicates whether the animation should be delayed.
- MStartedDelay indicates whether the animation has already started in startDelay.
- AnimationHandler AnimationHandler is a ValueAnimator inner class that implements the Runnable interface.
From the above code, we know that a ValueAnimator has its own states (STOPPED, RUNNING, SEEKED), and whether or not a ValueAnimator is delayed affects its execution. The code ends with a call to animationHandler.start(), which appears to be where the animation starts. Don’t worry, we haven’t initialized ValueAnimator yet, check setCurrentPlayTime(0).
public void setCurrentPlayTime(long playTime) { initAnimation(); long currentTime = AnimationUtils.currentAnimationTimeMillis(); if (mPlayingState ! = RUNNING) { mSeekTime = playTime; mPlayingState = SEEKED; } mStartTime = currentTime - playTime; doAnimationFrame(currentTime); }Copy the code
This function sets its initial value before the animation starts. This function sets the animation progress to a specified point in time. PlayTime should be between 0 and the total time of the animation, including when the animation executes repeatedly. If the animation has not started yet, it will not start until this time is set. If the animation is already running, setCurrentTime() sets the current progress to this value and continues from this point.
Let’s look at initAnimation()
void initAnimation() {
if (!mInitialized) {
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].init();
}
mInitialized = true;
}
}Copy the code
This function seems to be related to initializing the animation. This function is called before the first frame of the animation is processed. If startDelay is not 0, this function will be called after the delay ends. It completes the final initialization of the animation.
So what are mValues? Remember how we introduced ObjectAnimator at the beginning of this article? OfInt (T target, Property Property, int… The values) method is not introduced. The official documentation explains this method: Construct and return an ObjectAnimator object between values of type int. When values has only one value, that value is the end value of the animator. If there are two values, these two values are used as the starting and ending values. If there are more than two values, these are the starting values, the intermediate values for the animator to run, and the ending values. These values are evenly distributed over the duration of the animator. , >
Interrupt objectAnimator.start () and return to the beginning objectAnimator.ofint (…)
Next let’s dive into ofInt(…) Look inside.
public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setIntValues(values);
return anim;
}Copy the code
An explanation of this function:
- Target is the object that will be animated
- PropertyName is the name of the property on which the object property will be animated
- Values A group of values. Over time, the animation changes based on this set of values.
Look again at the function im.setintValues
public void setIntValues(int... 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.ofInt(mProperty, values)); } else { setValues(PropertyValuesHolder.ofInt(mPropertyName, values)); } } else { super.setIntValues(values); }}Copy the code
The beginning, mProperty certainly has not been initialized, we go in setValues (PropertyValuesHolder. OfInt (mPropertyName, values) and have a look. There’s the PropertyValuesHolder class involved. PropertyValuesHolder This class has information about the property and the values that need to be used during animation. The PropertyValuesHolder object can be used with ObjectAnimator or ValueAnimator to create animators that operate in parallel without PropertyValuesHolder.
So PropertyValuesHolder. OfInt () is used for? It constructs and returns an PropertyValuesHolder object of a specific type (in this case, IntPropertyValuesHolder type) from the properties and values passed in.
public static PropertyValuesHolder ofInt(String propertyName, int... values) {
return new IntPropertyValuesHolder(propertyName, values);
}Copy the code
Inside the IntPropertyValuesHolder
public IntPropertyValuesHolder(String propertyName, int... values) {
super(propertyName);
setIntValues(values);
}
@Override
public void setIntValues(int... values) {
super.setIntValues(values);
mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet;
}Copy the code
Jump to setIntValues of the parent class (PropertyValuesHolder)
public void setIntValues(int... values) {
mValueType = int.class;
mKeyframeSet = KeyframeSet.ofInt(values);
}Copy the code
This function is similar to the PropertyValueHolder constructor, which sets the values needed during the animation. If there is only one value, then that value is assumed to be the end value of the animator, and the initial value of the animation is automatically inferred through the object’s getter method. Of course, if all values are empty, the same values will be filled in automatically at the beginning of the animation. This mechanism only works if the PropertyValuesHolder object is used with the ObjectAnimator, and there is a getter method that automatically deduces from the propertyName. Otherwise PropertyValuesHolder has no way of determining what those values are. Next we see the keyFrameset.ofint (values) method. KeyframeSet This class holds a collection of Keyframes that valueAnimators use to calculate values for a given set of keyframes. The access to this class is package visible, because the details of how this class implements how keyFrames are stored and used do not need to be known externally.
Next, let’s look at the keyFrameset.ofint (values) method.
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);
}Copy the code
OfInt (target,propName,values[]), the int array we passed in at the beginning of this article using the system-provided animation initializer, is in concrete use here. Forget about using keyframe.ofint (…) . From this we can see that the Android SDK uses the length of the int[] passed in to determine the value of each frame in the animator. Objectanimator.ofint (…) objectAnimator.ofint (…) objectAnimator.ofint (…) In this paper. The IntKeyframeSet constructor is used to initialize these keyframes in the last sentence of keyFrameset. ofInt.
public IntKeyframeSet(IntKeyframe... keyframes) {
super(keyframes);
}Copy the code
IntKeyframeSet constructor and call the parent class KeyframeSet constructor to achieve.
public KeyframeSet(Keyframe... keyframes) {
mNumKeyframes = keyframes.length;
mKeyframes = new ArrayList<Keyframe>();
mKeyframes.addAll(Arrays.asList(keyframes));
mFirstKeyframe = mKeyframes.get(0);
mLastKeyframe = mKeyframes.get(mNumKeyframes - 1);
mInterpolator = mLastKeyframe.getInterpolator();
}Copy the code
The first and last frames of the newly initialized Keyframe array are given priority as fields in the KeyframeSet, probably for later calculation of the start and end of the animation.
ObjectValue, PropertyValueHolder, KeyframeSet
We went around a lot, and I don’t know if I confused you, but just to summarize. We’re not going to go through the order of the calls, it’s too long. I’m going to jump right into the relationship between ObjectValue, PropertyValueHolder, KeyframeSet. ObjectAnimator holds PropertyValuesHolder, which stores properties about the specific object (usually a View control) to be animated and values needed during animation. The PropertyValueHolder in turn uses KeyframeSet to hold the values of the keyframes of the animator from start to finish. This gives us an idea of the data structures the Animator uses to store and use during execution. PropertyValueHolder, KeyframeSet, and PropertyValueHolder are two classes whose API design is very clever. Each class uses a specific type of implementation as an internal implementation of a large abstract class that provides external apis (e.g., PropertyValuesHolder.ofInt(…) The implementation of.
Back to the analysis of the ObjectAnimator.start() process
The objectAnimator.start () process will be analyzed in the following way. From the above analysis we know that is a ValueAnimator. InitAnimation mValue in () is PropertyValuesHolder types of things. In initAnimation() mValues[I].init() initializes their Evaluator Evaluator
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 mKeyframeSet.setEvaluator(mEvaluator); }}Copy the code
MEvaluator, of course, also can use ObjectAnimator. SetEvaluator (…). The incoming; When null, the SDK initializes a specific type of Evaluator for us based on mValueType. So our initialization is done. Next, jump out of initAnimation() and return to setCurrentPlayTime(…)
public void setCurrentPlayTime(long playTime) { initAnimation(); long currentTime = AnimationUtils.currentAnimationTimeMillis(); if (mPlayingState ! = RUNNING) { mSeekTime = playTime; mPlayingState = SEEKED; } mStartTime = currentTime - playTime; doAnimationFrame(currentTime); }Copy the code
Explanation of the three states of animator STOPPED, RUNNING, SEEKED
-
static final int STOPPED = 0; // It hasn't started yetCopy the code
-
static final int RUNNING = 1; // It is playing normallyCopy the code
-
static final int SEEKED = 2; // Seeked to some time valueCopy the code
Explanation of mSeekedTime and mStartTime
- MSeekedTime Set when setCurrentPlayTime() is called. If it is negative, the animator has not been able to locate a value.
- MStartTime is first used when the animation.animateFrame() method is called. This time is used to determine the run time (and the run score value) on the second call to animateFrame()
setCurrentPlayTime(…) All code up to doAnimationFrame(currentTime) in doAnimationFrame is actually an Animator initialization. It seems doAnimator (…). That’s the function that actually handles animation frames. This function is mainly used to process a frame in the animator and adjust the start time of the animator if necessary.
Final Boolean doAnimationFrame(Long frameTime) {// Change the start time and state of the animator if (mPlayingState == STOPPED) {mPlayingState = RUNNING; if (mSeekTime < 0) { mStartTime = frameTime; } else { mStartTime = frameTime - mSeekTime; // Now that we're playing, reset the seek time mSeekTime = -1; } } // The frame time might be before the start time during the first frame of // an animation. The "current time" must always be on or after the start // time to avoid animating frames at negative time intervals. In practice, this // is very rare and only happens when seeking backwards. final long currentTime = Math.max(frameTime, mStartTime); return animationFrame(currentTime); }Copy the code
It looks like this function just adjusted some parameters, the real handler is still in the animationFrame(…). In the. Let’s follow him inside.
boolean animationFrame(long currentTime) { boolean done = false; switch (mPlayingState) { case RUNNING: case SEEKED: float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f; if (fraction >= 1f) { if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) { // Time to repeat if (mListeners ! = null) { int numListeners = mListeners.size(); for (int i = 0; i < numListeners; ++i) { mListeners.get(i).onAnimationRepeat(this); } } if (mRepeatMode == REVERSE) { mPlayingBackwards = mPlayingBackwards ? false : true; } mCurrentIteration += (int)fraction; fraction = fraction % 1f; mStartTime += mDuration; } else { done = true; The fraction = math.h min (fraction, 1.0 f); } } if (mPlayingBackwards) { fraction = 1f - fraction; } animateValue(fraction); break; } return done; }Copy the code
This inner function processes a simple animation frame for the given animation. The currentTime parameter is sent by the timer pulse via handler (or by the program during initialization, just like in our analysis). It is used to calculate the time that the animation has been running. And the score value that has been run. The return value of this function identifies whether the animation should stop (if the runtime exceeds the total time the animation should run, including if the number of repetitions is exceeded).
The fraction in this function can be interpreted simply as the current position of the animator’s progress bar. If (fraction >= 1f) {if (fraction >= 1f) {if (fraction >= 1f) {if (fraction >= 1f) {if (fraction >= 1f)}} In order not to make this article too complicated, we will not analyze it here. The simplest animator executes only once. AnimateValue (Fraction) should then be executed.
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); }}}Copy the code
On each animator frame, this function is called with the parameter passed in: fraction. Interpolate Score This function converts an already run score to interpolaterd score, which is then converted to a value that can be used in an animation (by evaluator. This function is usually called at animation Update, but it may also be called at end(). To set the final value of the property.
Interpolaterd is interpolated with evaluator.
- Interpolator: Used to define the rate at which an animator changes. It allows basic animation effects (gradients, stretches, pans, rotations) to speed up, slow down, repeat, etc. In the source code, the function of interpolation is to calculate its play time score according to a certain point in time, see the official documentation for details.
- Evaluator: Evaluator inherits all of the TypeEvaluator interface and has only one evaluate() method. It returns the value of the property you want to animate at the current point in time.
We can think of animation as the playing of a movie. Movies have progress bars. Interpolator controls the movie’s playing frequency, which is how many times fast forward or Interpolator. Evaluator then calculates which frame in the movie is currently playing based on values given by Interpolator, that is, where the progress bar will be located.
This function has three steps:
- Interpolator calculates the running time of an animation
- CalculateValue (Fraction) (that is, the PropertyValuesHolder object array) computes the value of the current animation
- Call the animation’s onAnimationUpdate(…) Notifies the animation of updates
//PropertyValuesHolder.calculateValue(...) void calculateValue(float fraction) { mAnimatedValue = mKeyframeSet.getValue(fraction); } //mKeyframeSet.getValue public Object getValue(float fraction) { // Special-case optimization for the common case of only two keyframes if (mNumKeyframes == 2) { if (mInterpolator ! = null) { fraction = mInterpolator.getInterpolation(fraction); } return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(), mLastKeyframe.getValue()); } if (fraction <= 0f) { final Keyframe nextKeyframe = mKeyframes.get(1); final TimeInterpolator interpolator = nextKeyframe.getInterpolator(); if (interpolator ! = null) { fraction = interpolator.getInterpolation(fraction); } final float prevFraction = mFirstKeyframe.getFraction(); float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction); return mEvaluator.evaluate(intervalFraction, mFirstKeyframe.getValue(), nextKeyframe.getValue()); } else if (fraction >= 1f) { final Keyframe prevKeyframe = mKeyframes.get(mNumKeyframes - 2); final TimeInterpolator interpolator = mLastKeyframe.getInterpolator(); if (interpolator ! = null) { fraction = interpolator.getInterpolation(fraction); } final float prevFraction = prevKeyframe.getFraction(); float intervalFraction = (fraction - prevFraction) / (mLastKeyframe.getFraction() - prevFraction); return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), mLastKeyframe.getValue()); } Keyframe prevKeyframe = mFirstKeyframe; for (int i = 1; i < mNumKeyframes; ++i) { Keyframe nextKeyframe = mKeyframes.get(i); if (fraction < nextKeyframe.getFraction()) { final TimeInterpolator interpolator = nextKeyframe.getInterpolator(); if (interpolator ! = null) { fraction = interpolator.getInterpolation(fraction); } final float prevFraction = prevKeyframe.getFraction(); float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction); return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), nextKeyframe.getValue()); } prevKeyframe = nextKeyframe; } // shouldn't reach here return mLastKeyframe.getValue(); }Copy the code
Let’s focus on if (mNumKeyframes == 2) first, because this is the most common case. Remember we used objectAnimator.ofint (…) Int [] array passed in? We usually pass in the start and end values of the animation, which is mNumKeyframes ==2 here. Evaluate (intevaluator.evaluate (…)) Code).
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}Copy the code
mEvaluator.evaluate(…) After calculation, we will return to the ValueAnimator. AnimateValue (…). Again, back to the ValueAnimator. SetCurrentPlayTime (…). . Finally back to Valueanimator. start(Boolean playBackwards). SetCurrentPlayTime (…) This function, to summarize, is mainly used to initialize the values of an animation. This initialization is much more complex than we think. It mainly uses PropertyValuesHolder, Evaluator, Interpolator to initialize values. PropertyValueHolder in turn stores the required values via KeyframeSet.
Valueanimator.start (Boolean playBackwards)
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mPlayingBackwards = playBackwards;
mCurrentIteration = 0;
mPlayingState = STOPPED;
mStarted = true;
mStartedDelay = false;
AnimationHandler animationHandler = getOrCreateAnimationHandler();
animationHandler.mPendingAnimations.add(this);
if (mStartDelay == 0) {
// This sets the initial value of the animation, prior to actually starting it running
setCurrentPlayTime(0);
mPlayingState = STOPPED;
mRunning = true;
notifyStartListeners();
}
animationHandler.start();
}Copy the code
Immediately after setCurrentPlayTime(0), the animation is notified by notifyStartListeners() of the startup message. This is finally done through animationHandler.start(). AnimationHandler is an object of type animationHandler that implements the Runable interface.
//AnimationHandler.start() public void start() { scheduleAnimation(); } //AnimationHandler.scheduleAnimation() private void scheduleAnimation() { if (! mAnimationScheduled) { mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null); mAnimationScheduled = true; } } // Called by the Choreographer. @Override public void run() { mAnimationScheduled = false; doAnimationFrame(mChoreographer.getFrameTime()); }Copy the code
Mhandler.start () is finally going through mChoreographer. Send it to the UI system. This process is more complicated and will not be covered here. We just need to know that after a frame in an animation is sent to the UI system in this way, the UI system will call animationHandler.run () after executing a frame. So this process is equivalent to actually, AnimationHandler. Start () to start the first animation AnimationHandler executed – UI system. The run () – > UI system after the execution, the callback function to perform AnimationHandler. The run (). Animationhandler.run () will call itself multiple times (driven by the UI system, of course) until the animation ends.
It’s a complicated process, so if you’re interested, check out my next blog post.