Analysis of the principle of attribute animation mechanism

Analysis of the principle of attribute animation mechanism

[[437959]]

This article is reprinted from the WeChat public account "Android Development Programming", the author is Android Development Programming. Please contact the Android Development Programming public account for reprinting this article.

Preface

The use of animation is common knowledge in Android development

However, there are many types of animations and they are complex to use. Whenever custom animations are needed to achieve complex animation effects, many developers seem helpless.

Today we will analyze the principle of attribute animation from the source code

1. Simple application of animation

ValueAnimator

The core class of property animation. Principle: control the change of value, and then manually assign it to the property of the object to achieve animation;

For different control values, Android provides us with three construction methods to instantiate ValueAnimator objects:

ValueAnimator.ofInt(int... values) -- integer values

ValueAnimator.ofFloat(float... values) -- floating point values

ValueAnimator.ofObject(TypeEvaluator evaluator, Object... values) -- Custom object type

1. Java method

  1. //Set animation start & end values
  2. //ofInt() has two functions:
  3. //1. Get the instance
  4. //2. Smooth transition between incoming parameters
  5. // As follows, 0 smoothly transitions to 3
  6. ValueAnimator animator = ValueAnimator.ofInt(0,3);
  7. //As follows, multiple parameters are passed in, and the effect is 0->5,5->3,3->10
  8. //ValueAnimator animator = ValueAnimator.ofInt(0,5,3,10);
  9. //Set the basic properties of the animation
  10. animator.setDuration(5000); //playing duration
  11. animator.setStartDelay(300); //Delay playback
  12. animator.setRepeatCount(0); //Number of replays
  13. animator.setRepeatMode(ValueAnimator.RESTART);
  14. //Replay mode
  15. //ValueAnimator.START: positive sequence
  16. //ValueAnimator.REVERSE: reverse order
  17. //Set the update listener
  18. //This method is executed once the value changes
  19. animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  20. @Override
  21. public void onAnimationUpdate(ValueAnimator animation) {
  22. //Get the changed value
  23. int currentValue = ( int ) animation.getAnimatedValue();
  24. // Output the changed value
  25. Log.d( "test" , "onAnimationUpdate: " + currentValue);
  26. //The changed value is assigned to the attribute value of the object
  27. view .setproperty(currentValue);
  28. //Refresh the view
  29. view .requestLayout();
  30. }
  31. });
  32. //Start the animation
  33. animator.start();

2. XML method

Common XML files in the path res/animator/, such as set_animator.xml

Set the animation parameters in the above file

  1. // ValueAnimator uses the <animator> tag
  2. <animator xmlns:android= "http://schemas.android.com/apk/res/android"  
  3. android:duration= "1000"  
  4. android:valueFrom= "1"  
  5. android:valueTo= "0"  
  6. android:valueType= "floatType"  
  7. android:repeatCount= "1"  
  8. android:repeatMode= "reverse" />
  9. />

Java code starts the animation

  1. Animator animator = AnimatorInflater.loadAnimator(context, R.animator.set_animation);
  2. // Load XML animation
  3. animator.setTarget( view );
  4. // Set up the animation object
  5. animator.start();

2. Detailed explanation of the principle

1. Create animation

  1. ObjectAnimator.ofFloat() start;
  2. /**
  3. * Construct an instance of ObjectAnimator whose return value is float
  4. *
  5. * @param target The object to be animated.
  6. * @param propertyName property name, the object must have a setXXX() method and be public .
  7. * @param values ​​, the value of the attribute change, can be set to 1 or more. When there is only 1, the starting value is the attribute value itself. When there are 2 values, the first is the starting value and the second is the ending value. When there are more than 2 values, the definition of the first and last values ​​is the same as when there are 2, and the middle value is the value that needs to be passed.
  8. */
  9. public   static ObjectAnimator ofFloat(Object target, String propertyName, float ... values ) {
  10. ObjectAnimator anim = new ObjectAnimator(target, propertyName);
  11. anim.setFloatValues( values ​​);
  12. return anim;
  13. }
  • Create an instance of ObjectAnimator and set values ​​for it;
  • Then, continue to look at the construction of ObjectAnimator;

Constructing ObjectAnimator

  1. private ObjectAnimator(Object target, String propertyName) {
  2. setTarget(target);
  3. setPropertyName(propertyName);
  4. }

The setTarget() method and setPropertyName() were called respectively;

2. setTarget()

  1. public void setTarget(@Nullable Object target) {
  2. final Object oldTarget = getTarget();
  3. if (oldTarget != target) {
  4. if (isStarted()) {
  5. cancel();
  6. }
  7. mTarget = target == null ? null : new WeakReference<Object>(target);
  8. // New target should cause re-initialization prior   to starting
  9. mInitialized = false ;
  10. }
  11. }

The old animation object (which may also be null) is inconsistent with the newly set animation object;

If the old animation is in the started state, cancel the animation first, and then record the animation object as a weak reference object;

3. setPropertyName()

  1. public void setPropertyName(@NonNull String propertyName) {
  2. // mValues ​​could be null if this is being constructed piecemeal. Just record the
  3. // propertyName to be used later when setValues() is called if so.
  4. if (mValues ​​!= null ) {
  5. PropertyValuesHolder valuesHolder = mValues[0];
  6. String oldName = valuesHolder.getPropertyName();
  7. valuesHolder.setPropertyName(propertyName);
  8. mValuesMap.remove(oldName);
  9. mValuesMap.put(propertyName, valuesHolder);
  10. }
  11. mPropertyName = propertyName;
  12. // New property/ values ​​/target should cause re-initialization prior   to starting
  13. mInitialized = false ;
  14. }
  • Record the name of propertyName;
  • If the propertyName already exists, its corresponding PropertyValuesHolder will be replaced. Here, a HashMap is used to save propertyName and PropertyValuesHolder.
  • if propertyName is "translationX";
  • Next, let’s look at the setFloatValues() method;

4. setFloatValues()

  1. @Override
  2. public void setFloatValues( float ... values ​​) {
  3. if (mValues ​​== null || mValues.length == 0) {
  4. // There is no value yet
  5. if (mProperty != null ) {
  6. setValues(PropertyValuesHolder.ofFloat(mProperty, values ​​));
  7. } else {
  8. setValues(PropertyValuesHolder.ofFloat(mPropertyName, values ​​));
  9. }
  10. } else {
  11. // If there is already a value, call the parent class's setFloatValues()
  12. super.setFloatValues( values ​​);
  13. }
  14. }

The parent class, ValueAnimator, has the following method setFloatValues():

5. ValueAnimator#setFloatValues()

  1. public void setFloatValues( float ... values ​​) {
  2. if ( values ​​== null || values ​​.length == 0) {
  3. return ;
  4. }
  5. if (mValues ​​== null || mValues.length == 0) {
  6. setValues(PropertyValuesHolder.ofFloat( "" , values ​​));
  7. } else {
  8. PropertyValuesHolder valuesHolder = mValues[0];
  9. valuesHolder.setFloatValues( values ​​);
  10. }
  11. // New property/ values ​​/target should cause re-initialization prior   to starting
  12. mInitialized = false ;
  13. }
  • Regardless of whether the parent class's setFloatValues() is called;
  • Finally, the values ​​are constructed into PropertyValuesHolder one by one, and finally stored in the HashMap mentioned above;
  • Of course, if the hashMap here has not been initialized, it will be initialized first;
  • The most important thing is to construct the PropertyValuesHolder object;
  • Then let's continue to look at the PropertyValuesHolder#ofFloat() method;

6. PropertyValuesHolder#ofFloat()

  1.   public   static PropertyValuesHolder ofFloat(String propertyName, float ... values ) {
  2. return new FloatPropertyValuesHolder(propertyName, values ​​);
  3. }
  4. Construct FloatPropertyValuesHolder;
  5. FloatPropertyValuesHolder
  6. public FloatPropertyValuesHolder(String propertyName, float ... values ) {
  7. super(propertyName);
  8. setFloatValues( values ​​);
  9. }
  • The FloatPropertyValuesHolder constructor is relatively simple, calling the parent class's constructor and passing the propertyName;
  • The key is the further call of the setFloatValues() method;
  • It further calls the parent class's setFloatValues() method, in which the animation keyframes are initialized;
  1. PropertyValuesHolder#setFloatValues()
  2. public void setFloatValues( float ... values ​​) {
  3. mValueType = float .class;
  4. mKeyframes = KeyframeSet.ofFloat( values ​​);
  5. }
  • The KeyframeSet#ofFloat() method is further called to complete the construction of the keyframe;
  • KeyframeSet is the implementation class of the Keyframe interface;

7. KeyframeSet#ofFloat()

  1. public   static KeyframeSet ofFloat( float ... values ​​) {
  2. boolean badValue = false ;
  3. int numKeyframes = values ​​.length;
  4. // At least 2 frames are required
  5. FloatKeyframe keyframes[] = new FloatKeyframe[Math. max (numKeyframes,2)];
  6. // Then construct each frame. Each frame has two important parameters: fraction and value
  7. if (numKeyframes == 1) {
  8. keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
  9. keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values ​​[0]);
  10. if ( Float .isNaN( values ​​[0])) {
  11. badValue = true ;
  12. }
  13. } else {
  14. keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values ​​[0]);
  15. for ( int i = 1; i < numKeyframes; ++i) {
  16. keyframes[i] =
  17. (FloatKeyframe) Keyframe.ofFloat(( float ) i / (numKeyframes - 1), values ​​[i]);
  18. if ( Float .isNaN( values ​​[i])) {
  19. badValue = true ;
  20. }
  21. }
  22. }
  23. if (badValue) {
  24. Log.w( "Animator" , "Bad value (NaN) in float animator" );
  25. }
  26. //Finally, gather all the key frames into one set
  27. return new FloatKeyframeSet(keyframes);
  28. }

Its main contents are:

  • Construct the key frames of the animation, and there must be at least 2 key frames in the animation;
  • There are two important parameters in the key frame. Fraction can be regarded as the serial number of the key frame, and value is the value of the key frame, which may be the starting value or a value in the middle.
  • Finally, the key frames are collected into a key frame set and returned to the PropertyValuesHolder;

8. setDuration()

  1. @Override
  2. @NonNull
  3. public ObjectAnimator setDuration(long duration) {
  4. super.setDuration(duration);
  5. return this;
  6. }

Called the setDuration() of the parent class ValueAnimator;

  1. ValueAnimator#setDuration()
  2. @Override
  3. public ValueAnimator setDuration(long duration) {
  4. if (duration < 0) {
  5. throw new IllegalArgumentException( "Animators cannot have negative duration: " +
  6. duration);
  7. }
  8. mDuration = duration;
  9. return this;
  10. }

setDuration() simply stores the value of duration, nothing more, so let's continue analyzing setInterpolator();

9. setInterpolator()

  1. @Override
  2. public void setInterpolator(TimeInterpolator value) {
  3. if (value != null ) {
  4. mInterpolator = value;
  5. } else {
  6. mInterpolator = new LinearInterpolator();
  7. }
  8. }

If null is passed, the default is LinearInterpolator, which is a linear interpolator;

Our hypothetical scenario here also sets up LinearInterpolator, which is the simplest interpolator, and its function is to complete uniform motion;

10. Rough analysis of LinearInterpolator;

  1. /**
  2. * The interpolator defines the frequency of the animation change, which can be linear or non-linear, such as accelerating or decelerating motion;
  3. */
  4. public interface TimeInterpolator {
  5. /**
  6. * The input passed here represents the ratio of the current time to the total time, and returns the current change frequency based on this time ratio. Its output and input values ​​are both between [0,1]
  7. */
  8. float getInterpolation( float input);
  9. }

The key definition of the interpolator is to implement the getInterpolation() method, which calculates the change frequency of the current animation based on the time percentage of the current animation.

Then let's take a look at LinearInterpolator's getInterpolation() implementation;

  1. LinearInterpolator#getInterpolation()
  2. public   float getInterpolation( float input) {
  3. return input;
  4. }

Yes, it returns to the original value, because the change of time must always be uniform;

11. Start

Start the animation from the start() method

  1. @Override
  2. public void start() {
  3. AnimationHandler.getInstance().autoCancelBasedOn(this);
  4. if (DBG) {
  5. Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
  6. for ( int i = 0; i < mValues.length; ++i) {
  7. PropertyValuesHolder pvh = mValues[i];
  8. Log.d(LOG_TAG, " Values[" + i + "]: " +
  9. pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
  10. pvh.mKeyframes.getValue(1));
  11. }
  12. }
  13. super.start();
  14. }
  • First make sure the animation has been canceled; the important line of code in this method is to call start() of the parent class ValueAnimator;
  • The parent class's external start() method is very simple, and its main implementation is in another overloaded private start() method;
  1. private void start(boolean playBackwards) {
  2. .....
  3. mReversing = playBackwards;
  4. // Reset pulse to "true"  
  5. mSelfPulse = !mSuppressSelfPulseRequested;
  6. .....
  7. // Add pulse callback
  8. addAnimationCallback(0);
  9. if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
  10. // If there's no start delay, init the animation and notify start listeners right away
  11. // to be consistent with the previous behavior. Otherwise, postpone this until the first  
  12. // frame after the start delay.
  13. startAnimation();
  14. if (mSeekFraction == -1) {
  15. // No seek, start at play time 0. Note that the reason we are not using fraction 0
  16. // is because for animations with 0 duration, we want to be consistent with pre-N
  17. // behavior: skip to the final value immediately.
  18. setCurrentPlayTime(0);
  19. } else {
  20. setCurrentFraction(mSeekFraction);
  21. }
  22. }
  23. }

One of them is addAnimationCallback(), which mainly adds a callback interface AnimationHandler.AnimationFrameCallback to AnimationHander, as shown in the following code;

  1. /**
  2. * Register to get a callback on the next frame after the delay.
  3. */
  4. public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
  5. if (mAnimationCallbacks. size () == 0) {
  6. getProvider().postFrameCallback(mFrameCallback);
  7. }
  8. if (!mAnimationCallbacks. contains (callback)) {
  9. mAnimationCallbacks.add (callback) ;
  10. }
  11. if (delay > 0) {
  12. mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
  13. }
  14. }
  • ValueAnimator implements AnimationFrameCallback, so what is added here is the instance of ValueAnimator;
  • Finally it is added to the mAnimationCallbacks queue;

12. startAnimation()

  1. private void startAnimation() {
  2. ......
  3. mAnimationEndRequested = false ;
  4. initAnimation();
  5. mRunning = true ;
  6. if (mSeekFraction >= 0) {
  7. mOverallFraction = mSeekFraction;
  8. } else {
  9. mOverallFraction = 0f;
  10. }
  11. if (mListeners != null ) {
  12. // The animation starts through the animation listener
  13. notifyStartListeners();
  14. }
  15. }

The key call is initAnimation()

  1. void initAnimation() {
  2. if (!mInitialized) {
  3. int numValues ​​= mValues.length;
  4. for ( int i = 0; i < numValues; ++i) {
  5. mValues[i].init();
  6. }
  7. mInitialized = true ;
  8. }
  9. }

mValues ​​is the PropertyValuesHolder array, the purpose here is to initialize the PropertyValuesHolder;

  1. void init() {
  2. if (mEvaluator == null ) {
  3. // We already handle int   and   float automatically, but not their Object
  4. // equivalents
  5. mEvaluator = (mValueType == Integer .class) ? sIntEvaluator :
  6. (mValueType == Float .class) ? sFloatEvaluator:
  7. null ;
  8. }
  9. if (mEvaluator != null ) {
  10. // KeyframeSet knows how to evaluate the common types - only give it a custom
  11. // evaluator if one has been set   on this class
  12. mKeyframes.setEvaluator(mEvaluator);
  13. }
  14. }
  • The main purpose of the init() method is to set the estimator for the keyframe;
  • The ObjectAnimator#ofFloat() method was called earlier, so the default one here is FloatEvaluator;
  • Next, setCurrentPlayTime() will be called to start the animation;

13. setCurrentPlayTime()

  1. public void setCurrentPlayTime(long playTime) {
  2. float fraction = mDuration > 0 ? ( float ) playTime / mDuration : 1;
  3. setCurrentFraction(fraction);
  4. }
  • Initially, setCurrentPlayTime(0) is called, that is, playTime is 0, and mDuration is set by ourselves through setDuration();
  • So the fraction obtained here is also 0;
  • Take a closer look at the setCurrentFraction() method;

14. setCurrentFraction

  1. public void setCurrentFraction( float fraction) {
  2. // Call initAnimation() again. It has been initialized before, so it is useless here.
  3. initAnimation();
  4. // Calibrate fraction to [0, mRepeatCount + 1]
  5. fraction = clampFraction(fraction);
  6. mStartTimeCommitted = true ; // do not allow start time   to be compensated for jank
  7. if (isPulsingInternal()) {
  8. // Random time?
  9. long seekTime = (long) (getScaledDuration() * fraction);
  10. // Get the current running time of the animation
  11. long currentTime = AnimationUtils.currentAnimationTimeMillis();
  12. // Only   modify the start time   when the animation is running. Seek fraction will ensure
  13. // non-running animations skip to the correct start time .
  14. // Get the start time
  15. mStartTime = currentTime - seekTime;
  16. } else {
  17. // If the animation loop hasn't started, or during start delay, the startTime will be
  18. // adjusted once the delay has passed based on seek fraction.
  19. mSeekFraction = fraction;
  20. }
  21. mOverallFraction = fraction;
  22. final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
  23. // Execute the animation. Note that the subclass's animateValue() method will be called first.
  24. animateValue(currentIterationFraction);
  25. }

The previous part is some time calculation to get the current real currentIterationFraction, and finally the animation will be executed by calling animateValue();

15. ObjectAnimator#animateValue()

  1. void animateValue( float fraction) {
  2. final Object target = getTarget();
  3. if (mTarget != null && target == null ) {
  4. // We lost the target reference, cancel and clean up. Note: we allow null target if the
  5. /// target has never been set .
  6. cancel();
  7. return ;
  8. }
  9. // Call the parent class's animateValue(). This is critical. The time interpolation and estimator calculations are performed in the parent class's animateValue() method.
  10. super.animateValue(fraction);
  11. int numValues ​​= mValues.length;
  12. for ( int i = 0; i < numValues; ++i) {
  13. // Here mValues ​​is PropertyValuesHolder[], that is, the property value of the target is changed in PropertyValuesHolder.
  14. mValues[i].setAnimatedValue(target);
  15. }
  16. }

Parent class ValueAnimator#animateValue()

  1. void animateValue( float fraction) {
  2. // Get time interpolation
  3. fraction = mInterpolator.getInterpolation(fraction);
  4. mCurrentFraction = fraction;
  5. int numValues ​​= mValues.length;
  6. // Send the time interpolation to the estimator to calculate the values  
  7. for ( int i = 0; i < numValues; ++i) {
  8. mValues[i].calculateValue(fraction);
  9. }
  10. // Send notification
  11. if (mUpdateListeners != null ) {
  12. int numListeners = mUpdateListeners. size ();
  13. for ( int i = 0; i < numListeners; ++i) {
  14. mUpdateListeners.get(i).onAnimationUpdate(this);
  15. }
  16. }
  17. }

animateValue(): Calculates time interpolation and estimator, calls PropertyValuesHolder to change properties;

  1. void setAnimatedValue(Object target) {
  2. if (mProperty != null ) {
  3. mProperty.set (target, getAnimatedValue());
  4. }
  5. if (mSetter != null ) {
  6. try {
  7. mTmpValueArray[0] = getAnimatedValue();
  8. // Modify the property value through reflection call
  9. mSetter.invoke(target, mTmpValueArray);
  10. } catch (InvocationTargetException e) {
  11. Log.e( "PropertyValuesHolder" , e.toString());
  12. } catch (IllegalAccessException e) {
  13. Log.e( "PropertyValuesHolder" , e.toString());
  14. }
  15. }
  16. }
  • Here, the property is modified through the Setter method of the property;
  • At this point in the analysis, the execution of one key frame of the animation is completed;
  • How are the remaining frames driven? We have to go back to the start() method, where we first analyze the addAnimationFrameCallback() method;
  • This method is equivalent to registering AnimationHandler.AnimationFrameCallback with AnimationHandler;
  • One of the methods in this callback is doAnimationFrame();

In the implementation of ValueAnimator, it is as follows;

  1. public final boolean doAnimationFrame(long frameTime) {
  2. .....
  3. boolean finished = animateBasedOnTime(currentTime);
  4. if (finished) {
  5. endAnimation();
  6. }
  7. return finished;
  8. }

This code is also very long. Let's only look at the key call to animateBasedOnTime()

  1. boolean animateBasedOnTime(long currentTime) {
  2. boolean done = false ;
  3. if (mRunning) {
  4. .....
  5. float currentIterationFraction = getCurrentIterationFraction(
  6. mOverallFraction, mReversing);
  7. animateValue(currentIterationFraction);
  8. }
  9. return done;
  10. }
  • The purpose is still to calculate the currentIterationFraction;
  • Execute animation through animateValue() method;
  • You can see that as long as doAnimationFrame() is called continuously, a key frame of the animation will be generated;
  • If the key frames are continuous, then the animation we see will be produced in the end;
  • Let's analyze how doAnimationFrame() is called continuously;
  • This needs to go back to AnimationHandler, where there is a very important callback implementation - Choreographer.FrameCallback;
  1. private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
  2. @Override
  3. public void doFrame(long frameTimeNanos) {
  4. doAnimationFrame(getProvider().getFrameTime());
  5. if (mAnimationCallbacks. size () > 0) {
  6. getProvider().postFrameCallback(this);
  7. }
  8. }
  9. };
  • The redraw in Andorid is generated by Choreographer 60 vsyncs in 1 second to notify the view tree to redraw the view;
  • After vsync is generated, its listener callback interface Choreographer.FrameCallback will be called;
  • That is to say, as long as you register this interface with Choreographer, you will receive 60 callbacks every second;
  • Therefore, here we can continuously call doAnimationFrame() to drive the animation;

16. Process summary

  • Animation is composed of many keyframes;
  • The main component of property animation is PropertyValuesHolder, which encapsulates keyframes;
  • After the animation starts, it listens to Choreographer's vsync, so that it can continuously call doAnimationFrame() to drive the animation to execute each key frame;
  • Each doAnimationFrame() call will calculate the time interpolation, and the fraction calculated by the time interpolator will be passed to the estimator, so that the estimator can calculate the current value of the attribute;
  • Finally, the value of the target property is modified by reflection through the Setter method recorded by PropertyValuesHolder;
  • When the attribute values ​​change frame by frame, forming a continuous sequence, it is the animation we see;

Summarize

In the last month of 2021, let’s work hard together;

<<:  The iOS15.1 system channel is closed and cannot be downgraded!

>>:  Have you seen the latest Android phone cost-effectiveness rankings during Double 11? Meizu dominates the list

Recommend

Common iOS debugging methods: breakpoint debugging

Tricks Guide In the process of iOS project develo...

Acg sentence control

Source code introduction Keep your favorite peopl...

Five tips for attracting new users in education and training institutions!

Students are the lifeline for educational trainin...

Pinduoduo Product Analysis Report

In the market structure where Alibaba and JD.com ...

How to do a good job in event operation planning process?

The essence of an event is communication, but the...

Shanshanyu 2021 Portrait Basic System Class 1

Shanshanyu 2021 Portrait Basic System Class 1 Int...

The first great ape named by the Chinese, fewer than 150 of them are still alive

The Tianxing gibbon was officially named by Chine...

How to operate and promote industrial Internet products?

The operation of industrial Internet products is ...