[[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 - //Set animation start & end values
- //ofInt() has two functions:
- //1. Get the instance
- //2. Smooth transition between incoming parameters
- // As follows, 0 smoothly transitions to 3
- ValueAnimator animator = ValueAnimator.ofInt(0,3);
- //As follows, multiple parameters are passed in, and the effect is 0->5,5->3,3->10
- //ValueAnimator animator = ValueAnimator.ofInt(0,5,3,10);
- //Set the basic properties of the animation
- animator.setDuration(5000); //playing duration
- animator.setStartDelay(300); //Delay playback
- animator.setRepeatCount(0); //Number of replays
- animator.setRepeatMode(ValueAnimator.RESTART);
- //Replay mode
- //ValueAnimator.START: positive sequence
- //ValueAnimator.REVERSE: reverse order
- //Set the update listener
- //This method is executed once the value changes
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- //Get the changed value
- int currentValue = ( int ) animation.getAnimatedValue();
- // Output the changed value
- Log.d( "test" , "onAnimationUpdate: " + currentValue);
- //The changed value is assigned to the attribute value of the object
- view .setproperty(currentValue);
- //Refresh the view
- view .requestLayout();
- }
- });
- //Start the animation
- 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 - // ValueAnimator uses the <animator> tag
- <animator xmlns:android= "http://schemas.android.com/apk/res/android"
- android:duration= "1000"
- android:valueFrom= "1"
- android:valueTo= "0"
- android:valueType= "floatType"
- android:repeatCount= "1"
- android:repeatMode= "reverse" />
- />
Java code starts the animation - Animator animator = AnimatorInflater.loadAnimator(context, R.animator.set_animation);
- // Load XML animation
- animator.setTarget( view );
- // Set up the animation object
- animator.start();
2. Detailed explanation of the principle 1. Create animation- ObjectAnimator.ofFloat() start;
- /**
- * Construct an instance of ObjectAnimator whose return value is float
- *
- * @param target The object to be animated.
- * @param propertyName property name, the object must have a setXXX() method and be public .
- * @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.
- */
- public static ObjectAnimator ofFloat(Object target, String propertyName, float ... values ) {
- ObjectAnimator anim = new ObjectAnimator(target, propertyName);
- anim.setFloatValues( values );
- return anim;
- }
- Create an instance of ObjectAnimator and set values for it;
- Then, continue to look at the construction of ObjectAnimator;
Constructing ObjectAnimator - private ObjectAnimator(Object target, String propertyName) {
- setTarget(target);
- setPropertyName(propertyName);
- }
The setTarget() method and setPropertyName() were called respectively; 2. setTarget()- public void setTarget(@Nullable Object target) {
- final Object oldTarget = getTarget();
- if (oldTarget != target) {
- if (isStarted()) {
- cancel();
- }
- mTarget = target == null ? null : new WeakReference<Object>(target);
- // New target should cause re-initialization prior to starting
- mInitialized = false ;
- }
- }
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()- public void setPropertyName(@NonNull String propertyName) {
- // mValues could be null if this is being constructed piecemeal. Just record the
- // propertyName to be used later when setValues() is called if so.
- if (mValues != null ) {
- PropertyValuesHolder valuesHolder = mValues[0];
- String oldName = valuesHolder.getPropertyName();
- valuesHolder.setPropertyName(propertyName);
- mValuesMap.remove(oldName);
- mValuesMap.put(propertyName, valuesHolder);
- }
- mPropertyName = propertyName;
- // New property/ values /target should cause re-initialization prior to starting
- mInitialized = false ;
- }
- 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()- @Override
- public void setFloatValues( float ... values ) {
- if (mValues == null || mValues.length == 0) {
- // There is no value yet
- if (mProperty != null ) {
- setValues(PropertyValuesHolder.ofFloat(mProperty, values ));
- } else {
- setValues(PropertyValuesHolder.ofFloat(mPropertyName, values ));
- }
- } else {
- // If there is already a value, call the parent class's setFloatValues()
- super.setFloatValues( values );
- }
- }
The parent class, ValueAnimator, has the following method setFloatValues(): 5. ValueAnimator#setFloatValues()- public void setFloatValues( float ... values ) {
- if ( values == null || values .length == 0) {
- return ;
- }
- if (mValues == null || mValues.length == 0) {
- setValues(PropertyValuesHolder.ofFloat( "" , values ));
- } else {
- PropertyValuesHolder valuesHolder = mValues[0];
- valuesHolder.setFloatValues( values );
- }
- // New property/ values /target should cause re-initialization prior to starting
- mInitialized = false ;
- }
- 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()- public static PropertyValuesHolder ofFloat(String propertyName, float ... values ) {
- return new FloatPropertyValuesHolder(propertyName, values );
- }
- Construct FloatPropertyValuesHolder;
- FloatPropertyValuesHolder
- public FloatPropertyValuesHolder(String propertyName, float ... values ) {
- super(propertyName);
- setFloatValues( values );
- }
- 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;
- PropertyValuesHolder#setFloatValues()
- public void setFloatValues( float ... values ) {
- mValueType = float .class;
- mKeyframes = KeyframeSet.ofFloat( values );
- }
- 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()- public static KeyframeSet ofFloat( float ... values ) {
- boolean badValue = false ;
- int numKeyframes = values .length;
- // At least 2 frames are required
- FloatKeyframe keyframes[] = new FloatKeyframe[Math. max (numKeyframes,2)];
- // Then construct each frame. Each frame has two important parameters: fraction and value
- if (numKeyframes == 1) {
- keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
- keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values [0]);
- if ( Float .isNaN( values [0])) {
- badValue = true ;
- }
- } else {
- keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values [0]);
- for ( int i = 1; i < numKeyframes; ++i) {
- keyframes[i] =
- (FloatKeyframe) Keyframe.ofFloat(( float ) i / (numKeyframes - 1), values [i]);
- if ( Float .isNaN( values [i])) {
- badValue = true ;
- }
- }
- }
- if (badValue) {
- Log.w( "Animator" , "Bad value (NaN) in float animator" );
- }
- //Finally, gather all the key frames into one set
- return new FloatKeyframeSet(keyframes);
- }
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()- @Override
- @NonNull
- public ObjectAnimator setDuration(long duration) {
- super.setDuration(duration);
- return this;
- }
Called the setDuration() of the parent class ValueAnimator; - ValueAnimator#setDuration()
- @Override
- public ValueAnimator setDuration(long duration) {
- if (duration < 0) {
- throw new IllegalArgumentException( "Animators cannot have negative duration: " +
- duration);
- }
- mDuration = duration;
- return this;
- }
setDuration() simply stores the value of duration, nothing more, so let's continue analyzing setInterpolator(); 9. setInterpolator()- @Override
- public void setInterpolator(TimeInterpolator value) {
- if (value != null ) {
- mInterpolator = value;
- } else {
- mInterpolator = new LinearInterpolator();
- }
- }
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;- /**
- * The interpolator defines the frequency of the animation change, which can be linear or non-linear, such as accelerating or decelerating motion;
- */
- public interface TimeInterpolator {
- /**
- * 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]
- */
- float getInterpolation( float input);
- }
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; - LinearInterpolator#getInterpolation()
- public float getInterpolation( float input) {
- return input;
- }
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 - @Override
- public void start() {
- AnimationHandler.getInstance().autoCancelBasedOn(this);
- 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();
- }
- 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;
- private void start(boolean playBackwards) {
- .....
- mReversing = playBackwards;
- // Reset pulse to "true"
- mSelfPulse = !mSuppressSelfPulseRequested;
- .....
- // Add pulse callback
- 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);
- }
- }
- }
One of them is addAnimationCallback(), which mainly adds a callback interface AnimationHandler.AnimationFrameCallback to AnimationHander, as shown in the following code; - /**
- * 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));
- }
- }
- ValueAnimator implements AnimationFrameCallback, so what is added here is the instance of ValueAnimator;
- Finally it is added to the mAnimationCallbacks queue;
12. startAnimation()- private void startAnimation() {
- ......
- mAnimationEndRequested = false ;
- initAnimation();
- mRunning = true ;
- if (mSeekFraction >= 0) {
- mOverallFraction = mSeekFraction;
- } else {
- mOverallFraction = 0f;
- }
- if (mListeners != null ) {
- // The animation starts through the animation listener
- notifyStartListeners();
- }
- }
The key call is initAnimation() - void initAnimation() {
- if (!mInitialized) {
- int numValues = mValues.length;
- for ( int i = 0; i < numValues; ++i) {
- mValues[i].init();
- }
- mInitialized = true ;
- }
- }
mValues is the PropertyValuesHolder array, the purpose here is to initialize the PropertyValuesHolder; - 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
- mKeyframes.setEvaluator(mEvaluator);
- }
- }
- 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()- public void setCurrentPlayTime(long playTime) {
- float fraction = mDuration > 0 ? ( float ) playTime / mDuration : 1;
- setCurrentFraction(fraction);
- }
- 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- public void setCurrentFraction( float fraction) {
- // Call initAnimation() again. It has been initialized before, so it is useless here.
- initAnimation();
- // Calibrate fraction to [0, mRepeatCount + 1]
- fraction = clampFraction(fraction);
- mStartTimeCommitted = true ; // do not allow start time to be compensated for jank
- if (isPulsingInternal()) {
- // Random time?
- long seekTime = (long) (getScaledDuration() * fraction);
- // Get the current running time of the animation
- long currentTime = AnimationUtils.currentAnimationTimeMillis();
- // Only modify the start time when the animation is running. Seek fraction will ensure
- // non-running animations skip to the correct start time .
- // Get the start time
- mStartTime = currentTime - seekTime;
- } else {
- // If the animation loop hasn't started, or during start delay, the startTime will be
- // adjusted once the delay has passed based on seek fraction.
- mSeekFraction = fraction;
- }
- mOverallFraction = fraction;
- final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
- // Execute the animation. Note that the subclass's animateValue() method will be called first.
- animateValue(currentIterationFraction);
- }
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()- void animateValue( float fraction) {
- final Object target = getTarget();
- if (mTarget != null && target == null ) {
- // We lost the target reference, cancel and clean up. Note: we allow null target if the
- /// target has never been set .
- cancel();
- return ;
- }
- // Call the parent class's animateValue(). This is critical. The time interpolation and estimator calculations are performed in the parent class's animateValue() method.
- super.animateValue(fraction);
- int numValues = mValues.length;
- for ( int i = 0; i < numValues; ++i) {
- // Here mValues is PropertyValuesHolder[], that is, the property value of the target is changed in PropertyValuesHolder.
- mValues[i].setAnimatedValue(target);
- }
- }
Parent class ValueAnimator#animateValue() - void animateValue( float fraction) {
- // Get time interpolation
- fraction = mInterpolator.getInterpolation(fraction);
- mCurrentFraction = fraction;
- int numValues = mValues.length;
- // Send the time interpolation to the estimator to calculate the values
- for ( int i = 0; i < numValues; ++i) {
- mValues[i].calculateValue(fraction);
- }
- // Send notification
- if (mUpdateListeners != null ) {
- int numListeners = mUpdateListeners. size ();
- for ( int i = 0; i < numListeners; ++i) {
- mUpdateListeners.get(i).onAnimationUpdate(this);
- }
- }
- }
animateValue(): Calculates time interpolation and estimator, calls PropertyValuesHolder to change properties; - void setAnimatedValue(Object target) {
- if (mProperty != null ) {
- mProperty.set (target, getAnimatedValue());
- }
- if (mSetter != null ) {
- try {
- mTmpValueArray[0] = getAnimatedValue();
- // Modify the property value through reflection call
- mSetter.invoke(target, mTmpValueArray);
- } catch (InvocationTargetException e) {
- Log.e( "PropertyValuesHolder" , e.toString());
- } catch (IllegalAccessException e) {
- Log.e( "PropertyValuesHolder" , e.toString());
- }
- }
- }
- 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; - public final boolean doAnimationFrame(long frameTime) {
- .....
- boolean finished = animateBasedOnTime(currentTime);
- if (finished) {
- endAnimation();
- }
- return finished;
- }
This code is also very long. Let's only look at the key call to animateBasedOnTime() - boolean animateBasedOnTime(long currentTime) {
- boolean done = false ;
- if (mRunning) {
- .....
- float currentIterationFraction = getCurrentIterationFraction(
- mOverallFraction, mReversing);
- animateValue(currentIterationFraction);
- }
- return done;
- }
- 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;
- private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
- @Override
- public void doFrame(long frameTimeNanos) {
- doAnimationFrame(getProvider().getFrameTime());
- if (mAnimationCallbacks. size () > 0) {
- getProvider().postFrameCallback(this);
- }
- }
- };
- 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; |