English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Detailed Analysis of Property Animation in Android Source Code

Introduction

Animations are indispensable in daily development, and property animations are even more powerful. We not only need to know how to use them but also understand their principles. Only then can we use them with ease. So, today, let's start with the simplest and understand the principles of property animations.

ObjectAnimator
 .ofInt(mView,"width",100,500)
 .setDuration(1000)
 .start();

ObjectAnimator#ofInt

For example, the code is as follows.

public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
 ObjectAnimator anim = new ObjectAnimator(target, propertyName);
 anim.setIntValues(values);
 return anim;
}

In this method, we first create an ObjectAnimator object, then set the values using the setIntValues method, and then return. In the constructor of ObjectAnimator, the current animation object is set through the setTarget method, and the current property name is set through setPropertyName. We will focus on the setIntValues method.

public void setIntValues(int... values) {}}
 if (mValues == null || mValues.length == 0) {
 // No values yet - this animator is being constructed piecemeal. Initialize 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);
 }
}

Firstly, it will be judged whether mValues is null, here it is null, and mProperty is also null, so the following will be called
The method setValues(PropertyValuesHolder.ofInt(mPropertyName, values));. Let's take a look at the PropertyValuesHolder.ofInt method. The class PropertyValuesHolder holds properties and values. In this method, an IntPropertyValuesHolder object is constructed and returned.

public static PropertyValuesHolder ofInt(String propertyName, int... values) {
 return new IntPropertyValuesHolder(propertyName, values);
}

The constructor of IntPropertyValuesHolder is as follows:

public IntPropertyValuesHolder(String propertyName, int... values) {
 super(propertyName);
 setIntValues(values);
}

Here, the constructor of its category is first called, followed by the invocation of the setIntValues method. In the constructor of its superclass, only the propertyName is set. The content of setIntValues is as follows:

public void setIntValues(int... values) {}}
 super.setIntValues(values);
 mIntKeyframes = (Keyframes.IntKeyframes) mKeyframes;
}

In the parent class's setIntValues method, mValueType is initialized to int.class, mKeyframes to KeyframeSet.ofInt(values). Where KeyframeSet is the keyframe set. Then mKeyframes is assigned to mIntKeyframes.

KeyframeSet

This class is used to record keyframes. Let's take a look at its ofInt method.

public static KeyframeSet ofInt(int... values) {
 
 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);
}

Here? Calculate keyframes according to the传入的values, and finally return IntKeyframeSet.

Return to ObjectAnimator, where setValues uses the parent class ValueAnimator's

ValueAnimator#setValues

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/the target should cause re-initialization before starting
 mInitialized = false;
}

The operation here is simple, just put the PropertyValuesHolder into the mValuesMap.

ObjectAnimator#start

This method is the starting point of the animation.

public void start() {
 // See if any of the current active/pending animators need to be canceled
 AnimationHandler handler = sAnimationHandler.get();
 if (handler != null) {
 int numAnims = handler.mAnimations.size();
 for (int i = numAnims - 1; i >= 0; i--) {}}
 if (handler.mAnimations.get(i) instanceof ObjectAnimator) {
 ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i);
 if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
  anim.cancel();
 }
 }
 }
 numAnims = handler.mPendingAnimations.size();
 for (int i = numAnims - 1; i >= 0; i--) {}}
 if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
 ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
 if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
  anim.cancel();
 }
 }
 }
 numAnims = handler.mDelayedAnims.size();
 for (int i = numAnims - 1; i >= 0; i--) {}}
 if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
 ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
 if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
  anim.cancel();
 }
 }
 }
 }
 if (DBG) {
 Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
 for (int i = 0; i < mValues.length; ++i) {
 PropertyValuesHolder pvh = mValues[i];
 Log.d(LOG_TAG, " Values[" + i + "]: " +
 pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
 pvh.mKeyframes.getValue(1));
 }
 }
 super.start();
}

Firstly, it will obtain the AnimationHandler object. If it is not null, it will determine whether it is an animation in mAnimations, mPendingAnimations, or mDelayedAnims, and cancel it. Finally, it will call the parent class's start method.

ValueAnimator#start

private void start(boolean playBackwards) {
 if (Looper.myLooper() == 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();
 animationHandler.mPendingAnimations.add(this);
 if (mStartDelay == 0) {
 // This sets the initial value of the animation, before actually starting it running
 if (prevPlayingState != SEEKED) {
 setCurrentPlayTime(0);
 }
 mPlayingState = STOPPED;
 mRunning = true;
 notifyStartListeners();
 }
 animationHandler.start();
}
  • First initialize some values
  • updateScaledDuration to scale the time, the default is1.0f
  • Obtain or create an AnimationHandler, and add the animation to the mPendingAnimations list
  • If there is no delay, notify the listener
  • animationHandler.start

In animationHandler.start, the scheduleAnimation method will be called, in which, a callback will be posted to mChoreographer, and finally, the run method of mAnimate will be executed. mChoreographerpost involves VSYNC, which will not be introduced here.

mAnimate#run

doAnimationFrame(mChoreographer.getFrameTime());

Here, we will use doAnimationFrame to set animation frames, let's take a 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 (mPendingAnimations.size() > 0) {
 ArrayList<ValueAnimator> pendingCopy =
 (ArrayList<ValueAnimator>) mPendingAnimations.clone();
 mPendingAnimations.clear();
 int count = pendingCopy.size();
 for (int i = 0; i < count; ++i) {
 ValueAnimator anim = pendingCopy.get(i);
 // If the animation has a startDelay, place it on the delayed list
 if (anim.mStartDelay == 0) {
 anim.startAnimation(this);
 } else {
 mDelayedAnims.add(anim);
 }
 }
 }
 // Next, process animations currently sitting on the delayed queue, adding
 // them to the active animations if they are ready
 int numDelayedAnims = mDelayedAnims.size();
 for (int i = 0; i < numDelayedAnims; ++i) {
 ValueAnimator anim = mDelayedAnims.get(i);
 if (anim.delayedAnimationFrame(frameTime)) {
 mReadyAnims.add(anim);
 }
 }
 int numReadyAnims = mReadyAnims.size();
 if (numReadyAnims > 0) {
 for (int i = 0; i < numReadyAnims; ++i) {
 ValueAnimator anim = mReadyAnims.get(i);
 anim.startAnimation(this);
 anim.mRunning = true;
 mDelayedAnims.remove(anim);
 }
 mReadyAnims.clear();
 }
 // Now process all active animations. The return value from animationFrame()
 // tells the handler whether it should now be ended
 int numAnims = mAnimations.size();
 for (int i = 0; i < numAnims; ++i) {
 mTmpAnimations.add(mAnimations.get(i));
 }
 for (int i = 0; i < numAnims; ++i) {
 ValueAnimator anim = mTmpAnimations.get(i);
 if (mAnimations.contains(anim) && anim.doAnimationFrame(frameTime)) {
 mEndingAnims.add(anim);
 }
 }
 mTmpAnimations.clear();
 if (mEndingAnims.size() > 0) {
 for (int i = 0; i < mEndingAnims.size(); ++i) {
 mEndingAnims.get(i).endAnimation(this);
 }
 mEndingAnims.clear();
 }
 // Schedule final commit for the frame.
 mChoreographer.postCallback(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 (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) {
 scheduleAnimation();
 }
}

The method is long, and the logic is as follows:}}

  1. Take the animation from mPendingAnimations, and based on the previously selected startAnimation or add it to the mDelayedAnims list.
  2. If the animation in the mDelayedAnims list is ready, add it to the mReadyAnims list
  3. Take the animation to be executed from the mAnimations list and add it to the mTmpAnimations list
  4. Execute animation frames through the doAnimationFrame method
  5. Continue to execute scheduleAnimation

From the above, we can see that the key to executing the 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 = mListeners.size();
  for (int i = 0; i < numListeners; ++i) {
  mListeners.get(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 = Math.min(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 the virtual machine execution engine, it will call the animateValue method of ObjectAnimator 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;
 }
 super.animateValue(fraction);
 int numValues = mValues.length;
 for (int i = 0; i < numValues; ++i) {
 mValues[i].setAnimatedValue(target);
 }
}

This mainly does two things,

  1. Call the animateValue method of the superclass
  2. Set properties through setAnimatedValue

The method of its superclass is as follows:

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);
 }
 }
}

In this method, the current fraction is obtained through Interpolator, and calculateValue is used to calculate the current value, which will call IntPropertyValuesHolder's calculateValue

void calculateValue(float fraction) {
 mIntAnimatedValue = mIntKeyframes.getIntValue(fraction);
}

We know that mIntKeyframes corresponds to IntKeyframeSet. In the getIntValue method of this class, the current corresponding value is calculated through TypeEvaluator. No more words.

Finally, return to animateValue. After calculating the value, setAnimatedValue is called to set the value. Let's see its implementation.

IntPropertyValuesHolder#setAnimatedValue

void setAnimatedValue(Object target) {
 if (mIntProperty != null) {
 mIntProperty.setValue(target, mIntAnimatedValue);
 return;
 }
 if (mProperty != null) {
 mProperty.set(target, mIntAnimatedValue);
 return;
 }
 if (mJniSetter != 0) {
 nCallIntMethod(target, mJniSetter, mIntAnimatedValue);
 return;
 }
 if (mSetter != null) {
 try {
 mTmpValueArray[0] = mIntAnimatedValue;
 mSetter.invoke(target, mTmpValueArray);
 catch (InvocationTargetException e) {
 Log.e("PropertyValuesHolder", e.toString());
 catch (IllegalAccessException e) {
 Log.e("PropertyValuesHolder", e.toString());
 }
 }
}

Well, you can see the trace of modifying property values here. There are the following four cases

  1. mIntProperty is not null
  2. mProperty is not null
  3. mJniSetter is not null
  4. mSetter is not null

Firstly, we construct an object through the String propertyName, int... values parameters, where mIntProperty is null, and mProperty is also null. How did the other two come from? It seems that something is missing?

Why call startAnimation directly in doAnimationFrame?63;That's right, it's here.

startAnimation

In this method, the initAnimation method is called. According to the dynamic dispatch rules, the initAnimation method of ObjectAnimator is called here. Here, the setupSetterAndGetter method of PropertyValuesHolder is called, where mSetter is initialized, etc. There's no more to say, just look at the code yourself.

That's all about the property animation in Android. I hope the content of this article can bring some help to Android developers. If you have any questions, you can leave a message for communication. Thank you for your support of the Yell Tutorial.

Declaration: The content of this article is from the Internet, and the copyright belongs to the original author. The content is contributed and uploaded by Internet users spontaneously, and this website does not own the copyright, has not been edited by humans, and does not assume relevant legal liability. If you find any content suspected of copyright infringement, please send an email to: notice#oldtoolbag.com (Please replace # with @ when sending an email to report abuse, and provide relevant evidence. Once verified, this site will immediately delete the infringing content.)

You May Also Like