Properties of animation in our daily use frequency is more, also more analytical online source, but many students say looking at the source when the fog, to see finally actually does not understand to the source structure principle and essence, so we release the entanglement in the source code, I take you from the Angle of the reference source, How to design a simplified version of the attribute animation framework, in order to better understand the principles and ideas of the attribute animation source code design.

What is a property animation?

First of all, if we’re going to write a property animation, we have to understand what is a property animation? What does it do?

The nature of animation

Android tween animation and frame-by-frame animation are animations of images or views by definition prior to the original Version 3.0 of Android. After version 3.0, Google Dad introduced property animation. A property is a property of an object. An animation changes the property state of a View (or an object) at a certain speed within a certain period of time, including but not limited to the position, size, transparency of the View, etc.

What’s the difference between property animation and tween animation?

Prior to 3.0, tween animation allowed us to move, scale, rotate, and fade views, but this was limited to inheriting View objects. Some students may be a bit strange, in addition to the View on the display of animation, what other scenes need animation? For example, in our custom View control, if we draw a Point in the onDraw() method, we can animate the Point object, or I just need to change the transparency of a View’s background color, and the tween animation is just a sight to see.

Secondly, during the animation process, the tanner animation can only change the state displayed by the View, without modifying the real properties of the View. For example, we moved a Button from the top of the screen to the bottom of the screen. However, if you click the area displayed by the Button after the animation is completed, you will find that it cannot be clicked. The button’s coordinate properties have not been modified.

So in summary, property animation means that you can animate properties of an object, not just a View. This is the core idea of attribute animation.

Use of property animation

As the old saying goes, know what it is and why. Well, before we know why, we know why. See how the property animation is called.

Let’s take the simplest example of scaling a TextView to 1.5 times its size.

TextView tvTip = findViewById(R.id.tv_tip);
ObjectAnimator objectAnimator = ObjectAnimator
                .ofFloat(tvTip, "scaleX".1f.1.5 f);
objectAnimator.start();
Copy the code

This is the simplest way to invoke property animation, and we can see the definition of property animation completely here: scaleX is scaled from 1x to 1.5x for the tvTip property.

Design a property animation framework

Below we will imitate the Android source property animation implementation, to achieve a simple version of the property animation, let us better understand the principle of the operation of the property animation.

What factors should we consider when designing an animation frame?
  1. First of all, considering the simplicity and ease of use of API calls, the simpler and more direct the better
  2. Each View can have many animations, but only one animation is running at the same time
  3. Because animation execution takes time to complete, animation execution cannot rely on its own for loop code, which can cause significant resource consumption
  4. How do I make animation work?
Start our design journey

With these questions in mind, we started to design our own property animation framework.

Architecture design

First, we need to consider what elements an animation task contains.

  1. The object itself, for the most part, is a View
  2. Animation duration
  3. The start and end values of the animation
  4. The speed effect of animation running is also known as interpolator

Second, we need to understand a few important concepts:

  1. Key frames

    Before the animation runs, we break down an animation task into several key frames. This is similar to how we need to plan the journey from Nanjing to Shanghai. In the middle, we need to plan the approximate time to go through Changzhou, Wuxi and Suzhou, and preliminarily estimate how long it will take to reach Shanghai.

    Animation is the same, it takes time to complete, and before we start, we need to split the animation into different states of time nodes, with the goal, we can have direction, this is the key frame.

  2. Interpolator: TimeInterpolator

    The time interpolator is used to calculate the percentage of the current attribute change according to the different time nodes. Examples are several commonly used interpolators: LinearInterpolator (linear interpolation), AccelerateDecelerateInterpolator (acceleration deceleration interpolation), DecelerateInterpolator (slow interpolator).

    Taking nanjing to Shanghai as an example, we plan to drive from Nanjing to Shanghai in 4 hours. If the linear interpolator is used, we will arrive from Nanjing to Shanghai at a constant speed. If you use the deceleration interpolator, you will drive at high speed after starting from Nanjing, and the closer you are to Shanghai, the slower you will drive.

  3. Evaluator: TypeEvaluator

    After all, we animate the object by changing its property value, which is a specific value. Therefore, the function of the estimator is to calculate the final attribute value of the specific change according to the percentage of the node change at the current time calculated by the interpolator.

We modeled the principle of attribute animation in source code and built our own attribute animation framework:

  1. First, before we initialize an animation taskMyObjectAnimatorWill generate a property setting assistantMyFloatPropertyValuesHolderFor managing the property values and keyframes we need to set. whileMyFloatPropertyValuesHolderThe keyframe management class is generatedMyKeyframeSetAnd generate several keyframes before startingMyFloatKeyframe.
  2. When the animation task starts, in the system, the animation will be triggered by listening to VSync signal in the property animation, which we will use hereVSYNCManagerSimulate the VSync signal and add a listener when the animation starts.
  3. When the VSync signal is heard in the animation task, we will calculate the current percentage through the number of executions and interpolator and pass it inMyFloatPropertyValuesHolderManage classes through keyframesMyKeyframeSetCalculates the value of the property to be set at this point in time and sets.
  4. When the animation is complete, the animation task classMyFloatPropertyValuesHolderResets the state of the current execution and clears the listening for VSync signals depending on whether the animation is repeated.

This is a complete set of execution process, some students may see this, still confused, what is the process, it does not matter, let’s Show your code, with the code to clarify the idea.

Code implementation
  1. The first step is to initialize an animation task, MyObjectAnimator, which is the same as the initialization of a native property animation.

    /** * Initialize an animation task that we wrote ourselves */
    MyObjectAnimator objectAnimator = MyObjectAnimator
                    .ofFloat(tv_tip, "scaleX".1f.2f);
    objectAnimator.setDuration(500);
    objectAnimator.setRepeat(false);
    objectAnimator.setTimeInterpolator(new LineInterpolator());
    objectAnimator.start();
    Copy the code
  2. Create a new MyObjectAnimator class and initialize MyObjectAnimator in myObjectAnimator.offloat () and set the interpolator, duration, and repeat flags.

    /** ** Animation task */
    public class MyObjectAnimator {
        /** * The current operation object */
        private WeakReference<Object> target;
    
        /** * whether to repeat */
        private boolean repeat = false;
    
        /** * Run time */
        private long mDuration = 300;
    
        /** * differentiator */
        private TimeInterpolator timeInterpolator;
    
        /** * animation assistant, used to set the property value */
        private MyFloatPropertyValuesHolder myFloatPropertyValuesHolder;
    
        / * < p > * * * * constructor to initialize the assistant MyFloatPropertyValuesHolder animation properties, is used to set parameters, and will be decomposed into N animation key frames, * *@paramTarget Animation object *@paramPropertyName specifies the propertyName to be modified. The propertyName in the View must have the corresponding setter and getter *@paramValues Node parameter */ of the keyframe
        public MyObjectAnimator(Object target, String propertyName, float. values) {
            this.target = new WeakReference<>(target);
            myFloatPropertyValuesHolder = new MyFloatPropertyValuesHolder(propertyName, values);
        }
    
        /** * Initialize the animation task **@param target
         * @param propertyName
         * @param values
         * @return* /
        public static MyObjectAnimator ofFloat(Object target, String propertyName, float. values) {
            MyObjectAnimator anim = new MyObjectAnimator(target, propertyName, values);
            return anim;
        }
    
        /** * Sets the time interpolator **@param timeInterpolator
         */
        public void setTimeInterpolator(TimeInterpolator timeInterpolator) {
            this.timeInterpolator = timeInterpolator;
        }
        /** * Sets whether to repeat *@param repeat
         */
        public void setRepeat(boolean repeat) {
            this.repeat = repeat;
        }
    
        /** * Set duration *@param duration
         */
        public void setDuration(long duration) {
            this.mDuration = duration;
        }
      
        /** * start animation */
        public void start(a) {
          	// Step 6 to implement}}Copy the code
  3. Below our new assistant class MyFloatPropertyValuesHolder animation attributes, and complete the construction method, construction method, can according to need to modify the property name to generate the corresponding Setter method, so the animation in our property, The property name passed in must have a corresponding Setter method in the owning object class.

    /** * Animation property "assistant" ** used to set the current object, reflection Settings */
    public class MyFloatPropertyValuesHolder {
        /** * Attribute name */
        String mPropertyName;
    
        /** * Float Class */
        Class mValueType;
    
        /** * Keyframe management */
        MyKeyframeSet myKeyframeSet;
    
      	/** * Sets the Setter method for the property, which is generated by the reflection method Object.class.getMethod()
     		Method mSetter = null;
      
        /** * constructor * initializes the keyframe management class **@paramPropertyName propertyName *@paramValues Node attributes */ of the key frame
        public MyFloatPropertyValuesHolder(String propertyName, float. values) {
            this.mPropertyName = propertyName;
            mValueType = float.class;
            myKeyframeSet = MyKeyframeSet.ofFloat(values);
          
           	setupSetter();
        }
      
       /** * Generates the corresponding Setter method */ by reflecting the object.class.getMethod () method
        public void setupSetter(a) {
            // Get the Setter for the property
            char firstLetter = Character.toUpperCase(mPropertyName.charAt(0));
            String theRest = mPropertyName.substring(1);
            String methodName = "set" + firstLetter + theRest;
    
            try {
                mSetter = View.class.getMethod(methodName, float.class);
    
            } catch(NoSuchMethodException e) { e.printStackTrace(); }}}Copy the code
  4. Create a new keyframe management class, MyKeyframeSet, and implement myKeyFrameset.offloat (Values). This is where the key frames are initialized by iterating through the node parameters passed in and stored in the List.

    /** * Keyframe management */
    public class MyKeyframeSet {
        /** * the first keyframe */
        MyFloatKeyframe mFirstKeyframe;
    
        /** * frame queue, mFirstKeyframe is actually the 0th element */
        List<MyFloatKeyframe> myFloatKeyframes;
    
        /** * Type estimator ** is equivalent to differential, used in the middle of the key frame, for different speed processing. * /
        TypeEvaluator mTypeEvaluator;
    
        /** * Generates the keyframe array ** from the passed node parameters@param values
         * @return* /
        public static MyKeyframeSet ofFloat(float. values) {
            // How many keyframe nodes are there
            int frameCount = values.length;
            // Iterate over the keyframe argument and generate the keyframe
            MyFloatKeyframe[] myFloatKeyframes = new MyFloatKeyframe[frameCount];
            myFloatKeyframes[0] = new MyFloatKeyframe(0, values[0]);
            // Iterate over the number of key frame nodes and calculate the percentage of key frames to initialize corresponding key frames
            for (int i = 1; i < frameCount; ++i) {
                myFloatKeyframes[i] = new MyFloatKeyframe((float) i / (frameCount - 1), values[i]);
            }
            return new MyKeyframeSet(myFloatKeyframes);
        }
    
        /** * Initializes the array of keyframes passed in, as well as the estimator@param keyframes
          */
         public MyKeyframeSet(MyFloatKeyframe... keyframes) {
             myFloatKeyframes = Arrays.asList(keyframes);
             mFirstKeyframe = keyframes[0];
             / / here directly using the Android native Float type valuation, Android. Animation. FloatEvaluator
             mTypeEvaluator = newFloatEvaluator(); }}Copy the code

    Create a new keyframe entity class: MyFloatKeyframe, which is used to store the state of the keyframe at a certain time

    /** * keyframe * saves the specific state of a moment * ps. The initialization of the animation task has been completed */
    public class MyFloatKeyframe {
        /** * The percentage of the current frame, ranging from 0 to 1 */
        float mFraction;
        /** * For each keyframe, specify the parameter */
        float mValue;
    		
        Class mValueType;
        public MyFloatKeyframe(float mFraction, float mValue) {
            this.mFraction = mFraction;
            this.mValue = mValue;
            this.mValueType = float.class;
        }
    
        public float getFraction(a) {
            return mFraction;
        }
    
        public float getValue(a) {
            returnmValue; }}Copy the code
  5. Create a linear interpolator class LineInterpolator that implements the TimeInterpolator interface.

    /** * linear interpolator * because it is linear motion, so the incoming percentage and output percentage */
    public class LineInterpolator implements TimeInterpolator {
        @Override
        public float getInterpolation(float input) {
            returninput; }}Copy the code

    TimeInterpolator interface:

    /** * Time differentiator ** implements this interface to modify execution percentage to modify runtime state */
    public interface TimeInterpolator {
        float getInterpolation(float input);
    }
    Copy the code

    At this point, we have completed the initialization of the property animation. After initializing the animation task class MyObjectAnimator, By initializing the animation attributes assistant MyFloatPropertyValuesHolder initialized again MyKeyframeSet keyframes management class, and through the incoming key frame parameters generated array stored in the MyKeyframeSet the key frames.

    Myobjectanimator.offloat (tv_tip, “scaleY”, 1f, 2f); Let’s talk about what happens after objectAnimator.start() is executed.

  6. Now we return to the start() method in the MyObjectAnimator class

    /** * start animation */
     public void start(a) {
         // Register listener
         VSYNCManager.getInstance().addCallbacks(this);
     }
    Copy the code

    You’ll notice that there’s only one listener set up here, so what’s going on in that listener?

    As we mentioned earlier, in the native properties method, the animation motion is listened to by the VSync mechanism class. However, in our third-party APP, we cannot listen to the VSync signal. Therefore, we write a VSYNCManager to simulate the signal, which is emitted every 16ms. You can search Android VSync signal by yourself) :

    ** Threads are used in code to simulate VSync signals because the VSync signal cannot be heard in a third party. * /
    public class VSYNCManager {
        private static final VSYNCManager mInstance = new VSYNCManager();
    
        private List<AnimationFrameCallback> callbacks = new ArrayList<>();
    
        public static VSYNCManager getInstance(a) {
            return mInstance;
        }
    
        /** * constructor to start a thread that simulates the VSYNC signal */
        private VSYNCManager(a) {
            new Thread(runnable).start();
        }
    
        /** * Add listener *@param callback
         */
        public void addCallbacks(AnimationFrameCallback callback) {
            if (callback == null) {
                return;
            }
    
            callbacks.add(callback);
        }
    
    
        /** * remove listener *@param callback
         */
        public void removeCallbacks(AnimationFrameCallback callback) {
            if (callback == null) {
                return;
            }
    
            callbacks.remove(callback);
        }
    
        /** * Analog VSYNC signal, signal every 16ms */
        private Runnable runnable = new Runnable() {
            @Override
            public void run(a) {
                while (true) {
                    try {
                        // Draw once at 60Hz (16ms)
                        Thread.sleep(16);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    // Simulate VSync signal
                    for(AnimationFrameCallback callback:callbacks) { callback.doAnimationFrame(System.currentTimeMillis()); }}}};/** ** signal listening method, when there is a signal passed, the callback */
        public interface AnimationFrameCallback {
            boolean doAnimationFrame(long currentTime); }}Copy the code
  7. Since we called vsyncManager.getInstance ().addCallbacks(this) in the start() method of MyObjectAnimation, it should be easy to think, Will MyObjectAnimation class implements VSYNCManager AnimationFrameCallback interface, in doAnimationFrame (long currentTime) listening to simulate VSync signal method.

  8. Implement doAnimationFrame(Long currentTime) in MyObjectAnimation. This is the essence of getting the animation moving.

    /** * Every 16ms will receive the signal callback, and the corresponding property set value **@param currentTime
     * @return* /
    @Override
    public boolean doAnimationFrame(long currentTime) {
        // Get VSync signal, start animating properties
    
        // Get the total number of times that should be executed
        float total = mDuration / 16;
    
        // calculate the current execution percentage (index++) /total
        float fraction = (index++) / total;
        if(timeInterpolator ! =null) {
            fraction = timeInterpolator.getInterpolation(fraction);
        }
    
        // Whether to repeat
        if (index >= total) {
            index = 0;
    
            if(! repeat) {// Do not repeat, remove listener
                VSYNCManager.getInstance().removeCallbacks(this);
                return true; }}// Set the corresponding property value through the animation property assistant to complete the animation operation
        myFloatPropertyValuesHolder.setAnimatedValue(target.get(), fraction);
        return false;
    }
    Copy the code

    We will divide the total time by the signal interval time of 16ms to get the total number of executions. After passing the current number of executions and the total number of executions, we get the default percentage of the current execution. If the interpolator is set, the interpolator will calculate the percentage of running at the current preset speed. Pass this percentage and the object to be set into the animation properties assistant for final setting.

  9. Adding in the MyFloatPropertyValuesHolder setAnimatedValue (Object target, float fraction) methods:

    public void setAnimatedValue(Object target, float fraction) {
        // Calculate the value to be modified from the current value and the execution percentage
        Object value = myKeyframeSet.getValue(fraction);
    
        try {
            mSetter.invoke(target, value);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch(InvocationTargetException e) { e.printStackTrace(); }}Copy the code

    We can see that in this method, we call the myKeyFrameset.getValue (fraction) method of the keyframe management class, which is the most important method in the entire property animation.

    In this method, the estimator is used to calculate. If the current running percentage is in the middle of the two frames, the corresponding parameters of the upper and lower key frames of the current percentage and the current running percentage are passed into the estimator to obtain the value corresponding to the current percentage. Taking Nanjing to Shanghai as an example, we planned to make three key stops in Changzhou, Wuxi and Suzhou between Nanjing and Shanghai. When we arrived at Zhenjiang (the city between Nanjing and Changzhou), we needed to calculate how many kilometers we had driven. Then we need to input the percentage of our current driving and the relative distance (0km) of Nanjing and Changzhou into the estimator, and then we can get our current driving distance.

    /** * Calculates the specific attribute value * that needs to be set eventually from the percentage passed in@param fraction
     * @return* /
    public Object getValue(float fraction) {
        // The position between key frames is calculated according to the execution time
    
        // Get the first frame
        MyFloatKeyframe prevKeyframe = mFirstKeyframe;
    
        // Iterate over all keyframes
        for (int i = 1; i < myFloatKeyframes.size(); ++i) {
            / / the next frame
            MyFloatKeyframe nextKeyframe = myFloatKeyframes.get(i);
    
            // [Key] Calculation of animation state between each key frame (formula see method)
            // Use the estimator to calculate. If the current running percentage is in the middle of the two frames, the corresponding parameters of the upper and lower key frames of the current percentage and the current running percentage are passed into the estimator to get the value corresponding to the current percentage
            if (fraction < nextKeyframe.getFraction()) {
                return mTypeEvaluator.evaluate(fraction, prevKeyframe.getValue(), nextKeyframe.getValue());
            }
    
            prevKeyframe = nextKeyframe;
        }
        return null;
    }
    Copy the code

    After we calculate the specific property value corresponding to the current running percentage by getValue(float Fraction) method, invoke is called by reflection through the Setter method initialized previously, and the specific property is set successfully, thus completing a frame run in the animation.

    When our signal is transmitted every 16ms, a frame of animation will be carried out, that is, the setting of the corresponding attribute value. When the total number of runs is reached, we reset the number of runs and remove the listener from the VSync signal if the animation is not repeated.

conclusion

Through the restoration of a simple attribute animation, we can deeply understand the idea behind attribute animation and operation principle, when understand the principle behind it, I believe that when looking at the source code will not be lost, but also for the source code understanding will be more profound.

Source code address: github.com/TYKevin/Cus…

Finally, if you like my article, please scan the code to pay attention to the public number, when there is a new article, you can timely receive oh.