Preface
In daily development, everyone cannot do without animation, and attribute animation is more powerful. We must not only know how to use it, but also know its principles. Only in this way can you be easily used. So, today, let’s understand the principles of attribute animation from the simplest point of view.
ObjectAnimator .ofInt(mView,"width",100,500) .setDuration(1000) .start();
ObjectAnimator#ofInt
Taking this as an example, the code is as follows.
public static ObjectAnimator ofInt(Object target, String propertyName, int... values) { ObjectAnimator anim = new ObjectAnimator(target, propertyName); (values); return anim; }
In this method, first, a ObjectAnimator object will be new, and then the value will be set through the setIntValues method, and then returned. In the ObjectAnimator construction method, the current animation object will be set through the setTarget method, and the current property name will be set through the setPropertyName. Let's focus on the setIntValues method.
public void setIntValues(int... values) { if (mValues == null || == 0) { // No values yet - this animator is being constructed piecemeal. Init the values with // whatever the current propertyName is if (mProperty != null) { setValues((mProperty, values)); } else { setValues((mPropertyName, values)); } } else { (values); } }
First of all, we will determine whether mValues is null. We are null here, and mProperty is also null, so we will call it.setValues((mPropertyName, values));
method. See firstMethod, PropertyValuesHolder class is the holds property and value. In this method, an IntPropertyValuesHolder object will be constructed and returned.
public static PropertyValuesHolder ofInt(String propertyName, int... values) { return new IntPropertyValuesHolder(propertyName, values); }
The construction method of IntPropertyValuesHolder is as follows:
public IntPropertyValuesHolder(String propertyName, int... values) { super(propertyName); setIntValues(values); }
Here, first, the constructor of his class will be called, and then the setIntValues method will be called. In the constructor of his parent class, the propertyName is just set. The content of setIntValues is as follows:
public void setIntValues(int... values) { (values); mIntKeyframes = () mKeyframes; }
In the setIntValues method of the parent class, mValueType is initialized and mKeyframes is (values). Where KeyframeSet is a keyframe collection. Then assign mKeyframes to mIntKeyframes.
KeyframeSet
This class records keyframes. Let's take a look at his ofInt method.
public static KeyframeSet ofInt(int... values) { int numKeyframes = ; IntKeyframe keyframes[] = new IntKeyframe[(numKeyframes,2)]; if (numKeyframes == 1) { keyframes[0] = (IntKeyframe) (0f); keyframes[1] = (IntKeyframe) (1f, values[0]); } else { keyframes[0] = (IntKeyframe) (0f, values[0]); for (int i = 1; i < numKeyframes; ++i) { keyframes[i] = (IntKeyframe) ((float) i / (numKeyframes - 1), values[i]); } } return new IntKeyframeSet(keyframes); }
What about here? The keyframe is calculated based on the values passed in, and finally the IntKeyframeSet is returned.
Go back to the ObjectAnimator, the setValues here uses the parent class ValueAnimator
ValueAnimator#setValues
public void setValues(PropertyValuesHolder... values) { int numValues = ; mValues = values; mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues); for (int i = 0; i < numValues; ++i) { PropertyValuesHolder valuesHolder = values[i]; ((), valuesHolder); } // New property/values/target should cause re-initialization prior to starting mInitialized = false; }
The operation here is simple, which is to put PropertyValuesHolder into mValuesMap.
ObjectAnimator#start
This method is where the animation begins.
public void start() { // See if any of the current active/pending animators need to be canceled AnimationHandler handler = (); if (handler != null) { int numAnims = (); for (int i = numAnims - 1; i >= 0; i--) { if ((i) instanceof ObjectAnimator) { ObjectAnimator anim = (ObjectAnimator) (i); if ( && hasSameTargetAndProperties(anim)) { (); } } } numAnims = (); for (int i = numAnims - 1; i >= 0; i--) { if ((i) instanceof ObjectAnimator) { ObjectAnimator anim = (ObjectAnimator) (i); if ( && hasSameTargetAndProperties(anim)) { (); } } } numAnims = (); for (int i = numAnims - 1; i >= 0; i--) { if ((i) instanceof ObjectAnimator) { ObjectAnimator anim = (ObjectAnimator) (i); if ( && hasSameTargetAndProperties(anim)) { (); } } } } if (DBG) { (LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration()); for (int i = 0; i < ; ++i) { PropertyValuesHolder pvh = mValues[i]; (LOG_TAG, " Values[" + i + "]: " + () + ", " + (0) + ", " + (1)); } } (); }
First of all, the AnimationHandler object will be obtained. If it is not empty, it will be determined that it is an animation in mAnimations, mPendingAnimations, and mDelayedAnimms and will be cancelled. Finally, call the start method of the parent class.
ValueAnimator#start
private void start(boolean playBackwards) { if (() == null) { throw new AndroidRuntimeException("Animators may only be run on Looper threads"); } mReversing = playBackwards; mPlayingBackwards = playBackwards; if (playBackwards && mSeekFraction != -1) { if (mSeekFraction == 0 && mCurrentIteration == 0) { // special case: reversing from seek-to-0 should act as if not seeked at all mSeekFraction = 0; } else if (mRepeatCount == INFINITE) { mSeekFraction = 1 - (mSeekFraction % 1); } else { mSeekFraction = 1 + mRepeatCount - (mCurrentIteration + mSeekFraction); } mCurrentIteration = (int) mSeekFraction; mSeekFraction = mSeekFraction % 1; } if (mCurrentIteration > 0 && mRepeatMode == REVERSE && (mCurrentIteration < (mRepeatCount + 1) || mRepeatCount == INFINITE)) { // if we were seeked to some other iteration in a reversing animator, // figure out the correct direction to start playing based on the iteration if (playBackwards) { mPlayingBackwards = (mCurrentIteration % 2) == 0; } else { mPlayingBackwards = (mCurrentIteration % 2) != 0; } } int prevPlayingState = mPlayingState; mPlayingState = STOPPED; mStarted = true; mStartedDelay = false; mPaused = false; updateScaledDuration(); // in case the scale factor has changed since creation time AnimationHandler animationHandler = getOrCreateAnimationHandler(); (this); if (mStartDelay == 0) { // This sets the initial value of the animation, prior to actually starting it running if (prevPlayingState != SEEKED) { setCurrentPlayTime(0); } mPlayingState = STOPPED; mRunning = true; notifyStartListeners(); } (); }
- Initialize some values first
- updateScaledDuration zoom time, default is 1.0f
- Get or create an AnimationHandler, add the animation to the mPendingAnimations list,
- If there is no delay, notify the listener
existIn this case, the scheduleAnimation method will be called. In this case, a callback will be used to use mChoreographerpost, and the mAnimate run method will be executed. mChoreographerpost involves VSYNC, so I won't introduce it here.
mAnimate#run
doAnimationFrame(());
Here we will use doAnimationFrame to set animation frames. Let's look at the code of this method.
void doAnimationFrame(long frameTime) { mLastFrameTime = frameTime; // mPendingAnimations holds any animations that have requested to be started // We're going to clear mPendingAnimations, but starting animation may // cause more to be added to the pending list (for example, if one animation // starting triggers another starting). So we loop until mPendingAnimations // is empty. while (() > 0) { ArrayList<ValueAnimator> pendingCopy = (ArrayList<ValueAnimator>) (); (); int count = (); for (int i = 0; i < count; ++i) { ValueAnimator anim = (i); // If the animation has a startDelay, place it on the delayed list if ( == 0) { (this); } else { (anim); } } } // Next, process animations currently sitting on the delayed queue, adding // them to the active animations if they are ready int numDelayedAnims = (); for (int i = 0; i < numDelayedAnims; ++i) { ValueAnimator anim = (i); if ((frameTime)) { (anim); } } int numReadyAnims = (); if (numReadyAnims > 0) { for (int i = 0; i < numReadyAnims; ++i) { ValueAnimator anim = (i); (this); = true; (anim); } (); } // Now process all active animations. The return value from animationFrame() // tells the handler whether it should now be ended int numAnims = (); for (int i = 0; i < numAnims; ++i) { ((i)); } for (int i = 0; i < numAnims; ++i) { ValueAnimator anim = (i); if ((anim) && (frameTime)) { (anim); } } (); if (() > 0) { for (int i = 0; i < (); ++i) { (i).endAnimation(this); } (); } // Schedule final commit for the frame. (Choreographer.CALLBACK_COMMIT, mCommit, null); // If there are still active or delayed animations, schedule a future call to // onAnimate to process the next frame of the animations. if (!() || !()) { scheduleAnimation(); } }
The method is long and the logic is as follows:
- Take out the animation from mPendingAnimations and select startAnimation or add it to the mDelayedAnimms list in advance.
- If the animation in the mDelayedAnims list is ready, add it to the mReadyAnims list
- Take out the animation to be executed from the mAnimations list and add it to the mTmpAnimations list
- Execute animation frames through doAnimationFrame method
- Continue to scheduleAnimation
From the above we can see that the key to executing animation is the doAnimationFrame method. In this method, the animationFrame method is called.
ValueAniator#animationFrame
boolean animationFrame(long currentTime) { boolean done = false; switch (mPlayingState) { case RUNNING: case SEEKED: float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f; if (mDuration == 0 && mRepeatCount != INFINITE) { // Skip to the end mCurrentIteration = mRepeatCount; if (!mReversing) { mPlayingBackwards = false; } } if (fraction >= 1f) { if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) { // Time to repeat if (mListeners != null) { int numListeners = (); for (int i = 0; i < numListeners; ++i) { (i).onAnimationRepeat(this); } } if (mRepeatMode == REVERSE) { mPlayingBackwards = !mPlayingBackwards; } mCurrentIteration += (int) fraction; fraction = fraction % 1f; mStartTime += mDuration; // Note: We do not need to update the value of mStartTimeCommitted here // since we just added a duration offset. } else { done = true; fraction = (fraction, 1.0f); } } if (mPlayingBackwards) { fraction = 1f - fraction; } animateValue(fraction); break; } return done; }
- Calculate fraction
- Call the animateValue method
According to the dynamic dispatch principle of virtual machine execution engine, the animateValue method of ObjectAnimator will be called here.
ObjectAnimator#animateValue
void animateValue(float fraction) { final Object target = getTarget(); if (mTarget != null && target == null) { // We lost the target reference, cancel and clean up. cancel(); return; } (fraction); int numValues = ; for (int i = 0; i < numValues; ++i) { mValues[i].setAnimatedValue(target); } }
There are two main things here.
- Call the animateValue method of the parent class
- Set properties via setAnimatedValue
The methods of its parent class are as follows:
void animateValue(float fraction) { fraction = (fraction); mCurrentFraction = fraction; int numValues = ; for (int i = 0; i < numValues; ++i) { mValues[i].calculateValue(fraction); } if (mUpdateListeners != null) { int numListeners = (); for (int i = 0; i < numListeners; ++i) { (i).onAnimationUpdate(this); } } }
In this method, the current fraction will be obtained through Interpolator and the current value should be calculated through calculateValue. Here, the calculationValue of IntPropertyValuesHolder will be called.
void calculateValue(float fraction) { mIntAnimatedValue = (fraction); }
We know that mIntKeyframes corresponds to IntKeyframeSet. In the getIntValue of this class, the current corresponding value will be calculated through the TypeEvaluator. I won't say much.
Finally, go back to animateValue. After calculating the value, setAnimatedValue will be called to set the value. Let's take a look at his implementation.
IntPropertyValuesHolder#setAnimatedValue
void setAnimatedValue(Object target) { if (mIntProperty != null) { (target, mIntAnimatedValue); return; } if (mProperty != null) { (target, mIntAnimatedValue); return; } if (mJniSetter != 0) { nCallIntMethod(target, mJniSetter, mIntAnimatedValue); return; } if (mSetter != null) { try { mTmpValueArray[0] = mIntAnimatedValue; (target, mTmpValueArray); } catch (InvocationTargetException e) { ("PropertyValuesHolder", ()); } catch (IllegalAccessException e) { ("PropertyValuesHolder", ()); } } }
Well, you can see the traces of modifying attributes here. There are four situations
- mIntProperty is not null
- mProperty is not null
- mJniSetter is not null
- mSetter is not null
First, we construct the object through the String propertyName, int… values parameter, mIntProperty is null, and mProperty is also null. So how did the other two come from? What seems to be missing?
Also, in the doAnimationFrame, do you call startAnimation directly? That's right, that's it.
startAnimation
In this method, the initAnimation method is called. Or according to the dynamic dispatch rules, the initAnimation method of ObjectAnimator is called here. Here we call the setupSetterAndGetter method of PropertyValuesHolder, and initialize mSetter, etc. here. I won’t say much here. Let’s read the code for yourself.
OK, the above is all about the attribute animation pairs in Android. I hope the content of this article will be of some help to all Android developers. If you have any questions, you can leave a message to communicate. Thank you for your support.