An overview of the

Attribute animation is a new set of animation apis provided in Android3.0, which has more flexibility than traditional tween animation. For example, if we want to use animation to update the width of a button, if we use tween animation, the final enlarged button will be deformed, but there is no such problem with property animation. The biggest difference is that tween animation will not really change the properties of view, while property animation will.

This article will analyze the property animation from the source point of view

ValueAnimation

The most commonly used classes for attribute animation are ValueAnimation and ObjectAnimator, where ValueAnimation is the parent of ObjectAnimator. It is the core of the property animation implementation, so let’s first look at the implementation of ValueAnimation

#### Inheritance relationship

public class ValueAnimator extends Animator {... }Copy the code

ValueAnimator is derived from Animator. Animator is an abstract class that defines the interface for animating properties, such as start, Cancel, setDuration, addListener, etc. ValueAnimator is typically constructed using its static of method. Here we look at the implementation of ofInt. Other of methods are implemented similarly

public static ValueAnimator ofInt(int. values) {
    ValueAnimator anim = new ValueAnimator();
    anim.setIntValues(values);
    return anim;
}
Copy the code

OfInt constructs a ValueAnimator instance and sets its initial value with setIntValues.

public void setIntValues(int. values) {
    if (values == null || values.length == 0) {
        return;
    }
    if (mValues == null || mValues.length == 0) {
        setValues(PropertyValuesHolder.ofInt("", values));
    } else {
        PropertyValuesHolder valuesHolder = mValues[0];
        valuesHolder.setIntValues(values);
    }
    // New property/values/target should cause re-initialization prior to starting
    mInitialized = false;
}
Copy the code

MValues is an PropertyValuesHolder array. PropertyValuesHolder, by its name, is the owner of properties and values. It maintains information about properties and values. Start with mValues null, continue with setValues, and construct the PropertyValuesHolder instance as an argument using the ofInt method of PropertyValuesHolder with an empty property name.

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

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

MValuesMap is a Map in which the property name is the key value and the value is the PropertyValuesHolder. The PropertyValuesHolder for each property is saved in mValuesMap.

@Override
public void start(a) {
    start(false);
}

private void start(boolean playBackwards) {
    if (Looper.myLooper() == null) {
        throw new AndroidRuntimeException("Animators may only be run on Looper threads");
    }
    mPlayingBackwards = playBackwards;
    mCurrentIteration = 0;
    mPlayingState = STOPPED;
    mStarted = true;
    mStartedDelay = false;
    mPaused = false;
    AnimationHandler animationHandler = getOrCreateAnimationHandler();
    animationHandler.mPendingAnimations.add(this);
    if (mStartDelay == 0) {
        // This sets the initial value of the animation, prior to actually starting it running
        setCurrentPlayTime(0);
        mPlayingState = STOPPED;
        mRunning = true;
        notifyStartListeners();
    }
    animationHandler.start();
}
Copy the code

ValueAnimation starts using the internal private start method by default, with playBackwards being false. Then create a through getOrCreateAnimationHandler AnimationHandler, then add the current animation to its internal waiting queue mPendingAnimations, The animationHandler member is an animationHandler, which is the inner class of ValueAnimation. If mStartDelay is not set, the first frame of the animation will be executed. The animation Listener is then notified by notifyStartListeners that the animation is started.

public void setCurrentPlayTime(long playTime) {
    initAnimation();// Initialize the animation
    long currentTime = AnimationUtils.currentAnimationTimeMillis();// The current time
    if(mPlayingState ! = RUNNING) { mSeekTime = playTime; mPlayingState = SEEKED; } mStartTime = currentTime - playTime;// Calculate the start time of animation execution
    doAnimationFrame(currentTime);// Perform the animation
}
Copy the code

SetCurrentPlayTime is an animation executed at the point in time specified by playTime, which is within the range [0, Duration]. In setCurrentPlayTime you need to initialize the animation, then set mSeekTime to 0, execute state to SEEKED, and then execute the animation via doAnimationFrame.

void initAnimation(a) {
    if(! mInitialized) {int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
            mValues[i].init();
        }
        mInitialized = true; }}Copy the code

The animation only needs to be initialized once, mainly to the PropertyValuesHolder, which we will see later. Initialization is controlled by mInitialized tags.

final boolean doAnimationFrame(long frameTime) {
    if (mPlayingState == STOPPED) {
        mPlayingState = RUNNING;// Set the playback state to RUNNING
        if (mSeekTime < 0) {
            mStartTime = frameTime;
        } else {
            mStartTime = frameTime - mSeekTime;
            // Now that we're playing, reset the seek time
            mSeekTime = -1; }}if (mPaused) {
        if (mPauseTime < 0) {
            mPauseTime = frameTime;
        }
        return false;
    } else if (mResumed) {
        mResumed = false;
        if (mPauseTime > 0) {
            // Offset by the duration that the animation was pausedmStartTime += (frameTime - mPauseTime); }}final long currentTime = Math.max(frameTime, mStartTime);
    return animationFrame(currentTime);// Return true if the animation ends
}
Copy the code

MSeekTime is 0, and mStartTime is the frameTime value, which is the current time value. If mPaused means to pause the animation, return false. If mPaused is true, the animation continues. At this point, mStartTime, the start time of the animation, is recalcitated according to mPauseTime. The maximum values of frameTime and mStartTime are then passed to the animationFrame for further animation.

boolean animationFrame(long currentTime) {
    boolean done = false;
    switch (mPlayingState) {
    case RUNNING:
    case SEEKED:
        float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;
        if (fraction >= 1f) {
            if(mCurrentIteration < mRepeatCount | | mRepeatCount = = INFINITE) {... }else{
                 done = true;
                fraction = Math.min(fraction, 1.0 f); }}... animateValue(fraction);break;
    }

    return done;
}
Copy the code

In the animationFrame, the fraction is calculated according to mDuration, which is the percentage of elapsed time that is objectively uniform. The animation value is then calculated using animateValue, which returns whether the animation is complete.

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); }}}Copy the code

Animation interpolation by fraction and the interpolation calculation, we know the interpolator is able to control the execution rate of animation, it based on fraction value calculation, the default interpolator is AccelerateDecelerateInterpolator ValueAnimation, We then calculate the animation value based on interpolation through the calculateValue of the PropertyValuesHolder, which is the value within the interval specified by the OF method, and notify the animation value update through onAnimatinoUpdate. At this point our animation executes on the first frame, so how does it execute continuously? This is done by AnimationHandler.

protected static ThreadLocal<AnimationHandler> sAnimationHandler =
            new ThreadLocal<AnimationHandler>();
Copy the code

AnimationHandler is described as follows

/**
* This custom, static handler handles the timing pulse that is shared by
* all active animations. This approach ensures that the setting of animation
* values will happen on the UI thread and that all animations will share
* the same times for calculating their values, which makes synchronizing
* animations possible.
*
* The handler uses the Choreographer for executing periodic callbacks.
*/
Copy the code

You can see that the AnimationHandler is shared by all active animations within the thread. It ensures that the animation value setting takes place in the UI thread, and that all animations will share the same value to calculate their own value so that the animation executes synchronously. This handler uses Choreographer to perform periodic callbacks. Choreographer can be thought of as an observer of Vsync signals, providing an interface for upper-layer applications to listen to Vsync signals and for applications to refresh their UI based on that signal.

The AnimationHandler’s start method is called at the end of the start method


protected static class AnimationHandler implements Runnable {...public void start(a) {
        scheduleAnimation();// Arrange the animation in the next frame
    }

    @Override
    public void run(a) {
        mAnimationScheduled = false;
        doAnimationFrame(mChoreographer.getFrameTime());
    }

    // Attribute animations are rendered by the choreographer
    private void scheduleAnimation(a) {
        if(! mAnimationScheduled) {// Actually post a Callback to the choreographer whose run method will be executed when the vertical signal arrives
            mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this.null);
            mAnimationScheduled = true; }}}Copy the code

Animationhandler is used by Choreographer to execute animations. Call scheduleAnimation in the start method to add the current Animationhandler to Choreographer’s callback queue. It is of type Choreographer.CALLBACK_ANIMATION. Choreographer will notify this callback when it receives a VSync signal that calls the run method of the callback, which in this case executes the doAnimationFrame method, Reset mAnimationScheduled at the same time. So the actual animation is done through the doAnimationFrame.

private void doAnimationFrame(long frameTime) {
    // There may be multiple waiting groups of animations, start them and add them to the active list
    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); }}}...// Here we add the live animation to the temporary animation and execute doAnimationFrame to draw a frame animation
    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);
        // The return value of doAnimationFrame tells if the animation is over
        if (mAnimations.contains(anim) && anim.doAnimationFrame(frameTime)) {
            mEndingAnims.add(anim);
        }
    }
    
    mTmpAnimations.clear();Why is mTmpAnimations needed here?
    if (mEndingAnims.size() > 0) {
        for (int i = 0; i < mEndingAnims.size(); ++i) {
            mEndingAnims.get(i).endAnimation(this);
        }
        mEndingAnims.clear();
    }

    // If there are active animations or delayed animations, wait for the next vertical signal to arrange the next drawing
    if(! mAnimations.isEmpty() || ! mDelayedAnims.isEmpty()) { scheduleAnimation();// Draw next frame}}// The animation to be executed is added to the list of activities
private void startAnimation(AnimationHandler handler) {
    initAnimation();
    handler.mAnimations.add(this);
    if (mStartDelay > 0&& mListeners ! =null) {
        // Listeners were already notified in start() if startDelay is 0; this is
        // just for delayed animationsnotifyStartListeners(); }}private void endAnimation(AnimationHandler handler) {
    handler.mAnimations.remove(this);
    handler.mPendingAnimations.remove(this);
    handler.mDelayedAnims.remove(this);
    mPlayingState = STOPPED;
    mPaused = false;
    if((mStarted || mRunning) && mListeners ! =null) {
        if(! mRunning) {// If it's not yet running, then start listeners weren't called. Call them now.
            notifyStartListeners();
        }
        ArrayList<AnimatorListener> tmpListeners =
                (ArrayList<AnimatorListener>) mListeners.clone();
        int numListeners = tmpListeners.size();
        for (int i = 0; i < numListeners; ++i) {
            tmpListeners.get(i).onAnimationEnd(this);
        }
    }
    mRunning = false;
    mStarted = false;
    mStartListenersCalled = false;
    mPlayingBackwards = false;
}
Copy the code

The waiting animation is first started in the doAnimationFrame via startAnimation, which actually adds the animation to the active animation list mAnimations and calls the callback to start executing the animation. Then doAnimationFrame is executed for the animation in the active list, which executes a frame of animation. EndAnimation is called for the finished animation, which removes the animation instance from the active list, wait list, and delay list, respectively. Finally, scheduleAnimation is called to schedule the execution of the next frame whenever there is a live animation. This allows the animation to be executed continuously.

PropertyValuesHolder

ValueAnimation relies on PropertyValuesHolder during the execution of ValueAnimation, but the PropertyName is null. The following parts are involved in the whole process:

  1. Instantiate PropertyValuesHolder with the of method in setValues
  2. Call its init method in the init animation method
  3. AnimateValue is computed in the calculateValue of the PropertyValuesHolder

So let’s see how each of these is implemented. Okay

Set the values

public static PropertyValuesHolder ofInt(String propertyName, int. values) {
    return new IntPropertyValuesHolder(propertyName, values);
}
Copy the code
static class IntPropertyValuesHolder extends PropertyValuesHolder {...public IntPropertyValuesHolder(String propertyName, int. values) {
        super(propertyName);
        setIntValues(values);
    }

    @Override
    public void setIntValues(int. values) {
        super.setIntValues(values);
        mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet;
    }
    ……
}
Copy the code
//PropertyValuesHolder.java
public void setIntValues(int. values) {
    mValueType = int.class;
    mKeyframeSet = KeyframeSet.ofInt(values);
}

//KeyframeSet.java
public static KeyframeSet ofInt(int. values) {
    int numKeyframes = values.length;
    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);
}
Copy the code

The values that we set up end up in the form of KeyFrameSet, which is essentially the value of the keyframe, which contains the values that we set, and the fraction for each value.

Initialize the

void init(a) {
    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 classmKeyframeSet.setEvaluator(mEvaluator); }}Copy the code

Init actually sets the valuer for PropertyValuesHolder. SIntEvaluator is actually an integer valuer used to evaluate the animation value

public class IntEvaluator implements TypeEvaluator<Integer> {
    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        int startInt = startValue;
        return (int)(startInt + fraction * (endValue - startInt)); }}Copy the code

Calculate animation value

void calculateValue(float fraction) {
    mAnimatedValue = mKeyframeSet.getValue(fraction);
}

public Object getValue(float fraction) {
    if (mNumKeyframes == 2) {// The number of keyframes is 2
        if(mInterpolator ! =null) {// The interpolator first computes the interpolation
            fraction = mInterpolator.getInterpolation(fraction);
        }
        return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(),
                mLastKeyframe.getValue());
    }
    if (fraction <= 0f) {// Keyframe the first frame
        final Keyframe nextKeyframe = mKeyframes.get(1);
        final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
        if(interpolator ! =null) {
            fraction = interpolator.getInterpolation(fraction);
        }
        final float prevFraction = mFirstKeyframe.getFraction();
        float intervalFraction = (fraction - prevFraction) /
            (nextKeyframe.getFraction() - prevFraction);
        return mEvaluator.evaluate(intervalFraction, mFirstKeyframe.getValue(),
                nextKeyframe.getValue());
    } else if (fraction >= 1f) {// Keyframe last frame
        final Keyframe prevKeyframe = mKeyframes.get(mNumKeyframes - 2);
        final TimeInterpolator interpolator = mLastKeyframe.getInterpolator();
        if(interpolator ! =null) {
            fraction = interpolator.getInterpolation(fraction);
        }
        final float prevFraction = prevKeyframe.getFraction();
        float intervalFraction = (fraction - prevFraction) /
            (mLastKeyframe.getFraction() - prevFraction);
        return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
                mLastKeyframe.getValue());
    }
    // Other keyframes
    Keyframe prevKeyframe = mFirstKeyframe;
    for (int i = 1; i < mNumKeyframes; ++i) {
        Keyframe nextKeyframe = mKeyframes.get(i);
        if (fraction < nextKeyframe.getFraction()) {
            final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
            if(interpolator ! =null) {
                fraction = interpolator.getInterpolation(fraction);
            }
            final float prevFraction = prevKeyframe.getFraction();
            float intervalFraction = (fraction - prevFraction) /
                (nextKeyframe.getFraction() - prevFraction);
            return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
                    nextKeyframe.getValue());
        }
        prevKeyframe = nextKeyframe;
    }
    // shouldn't reach here
    return mLastKeyframe.getValue();
}
Copy the code

The animation value is finally calculated by the KeyFrameSet. We construct the KeyFrameSet using the of method, where the fraction is used to calculate the final animation value. The fraction may be calculated by the interpolator. StartValue is the value of the first keyframe and endValue is the value of the last keyframe. When the number of keyframes is greater than 2, we need to do this, so I’m going to give you an example like ofInt(1,100,200), the number of keyframes is 3, The corresponding keyframes are [0f,1],[0.5f,100],[1f,200], where 0f,0.5f, and 1f are the corresponding fractions of the KeyFrame respectively. When calculating the animation value with fraction in getValue:

  1. Evaluate (intervalFraction, 0,100); evaluate(intervalFraction, 0,100);
  2. Evaluate (intervalFraction, 100,200) evaluate(intervalFraction, 100,200)

ObjectAnimation

ObjectAnimator is a subclass of ValueAnimation. The difference between ObjectAnimator and ValueAnimation is that ObjectAnimation contains a Target and PropertyName. You can apply an animation to a Property on that Target, not just a View. As long as the Target has setter and getter methods on the PropertyName. ObjectAnimation overrides the initAnimation and animateValue methods

@Override
void initAnimation(a) {
    if(! mInitialized) {// mValueType may change due to setter/getter setup; do this before calling super.init(),
        // which uses mValueType to set up the default type evaluator.
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
            mValues[i].setupSetterAndGetter(mTarget);
        }
        super.initAnimation(); }}Copy the code

ObjectAnimation calls the setupSetterAndGetter method of PropertyValuesHolder compared to ValueAnimation.

void setupSetterAndGetter(Object target) {
    ……
    Class targetClass = target.getClass();
    if (mSetter == null) {
        setupSetter(targetClass);// Get the mSetter method
    }
    for (Keyframe kf : mKeyframeSet.mKeyframes) {
        if(! kf.hasValue()) {if (mGetter == null) {
                setupGetter(targetClass);
                if (mGetter == null) {
                    // Already logged the error - just return to avoid NPE
                    return; }}try {
                kf.setValue(mGetter.invoke(target));
            } catch (InvocationTargetException e) {
                Log.e("PropertyValuesHolder", e.toString());
            } catch (IllegalAccessException e) {
                Log.e("PropertyValuesHolder", e.toString()); }}}}// Get the set method for target
void setupSetter(Class targetClass) {
    mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", mValueType);
}

// Get the get method for the target property
private void setupGetter(Class targetClass) {
    mGetter = setupSetterOrGetter(targetClass, sGetterPropertyMap, "get".null);
}

// PropertyValuesHolder.java
private Method setupSetterOrGetter(Class targetClass, HashMap
       
        > propertyMapMap, String prefix, Class valueType)
       ,> {
    Method setterOrGetter = null;
    try {
        // Have to lock property map prior to reading it, to guard against
        // another thread putting something in there after we've checked it
        // but before we've added an entry to it
        mPropertyMapLock.writeLock().lock();
        HashMap<String, Method> propertyMap = propertyMapMap.get(targetClass);
        if(propertyMap ! =null) {
            setterOrGetter = propertyMap.get(mPropertyName);// fetch from the cache
        }
        if (setterOrGetter == null) {// If not, fetch it from target
            setterOrGetter = getPropertyFunction(targetClass, prefix, valueType);
            if (propertyMap == null) {
                propertyMap = newHashMap<String, Method>(); propertyMapMap.put(targetClass, propertyMap); } propertyMap.put(mPropertyName, setterOrGetter); }}finally {
        mPropertyMapLock.writeLock().unlock();
    }
    return setterOrGetter;
}
Copy the code

The setupSetterOrGetter method will use getPropertyFunction to get the corresponding get or set method of the property, which will be cached in the propertyMapMap. The propertyMapMap uses the Target instance as the key. Take a Map<String,Method> as value. This Map is used to hold the Method corresponding to the property, where the key is the name of the property.

//ObjectAnimator.java
@Override
void animateValue(float fraction) {
    super.animateValue(fraction);
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) { mValues[i].setAnimatedValue(mTarget); }}//PropertyValuesHolder.java
void setAnimatedValue(Object target) {
    if(mProperty ! =null) {
        mProperty.set(target, getAnimatedValue());
    }
    if(mSetter ! =null) {
        try {
            mTmpValueArray[0] = getAnimatedValue();
            mSetter.invoke(target, mTmpValueArray);
        } catch (InvocationTargetException e) {
            Log.e("PropertyValuesHolder", e.toString());
        } catch (IllegalAccessException e) {
            Log.e("PropertyValuesHolder", e.toString()); }}}Copy the code

The animateValue and ValueAnmiation implementations of ObjectAnimator are different. ObjectAnimator sets the value of the Target property via the setAnimatedValue method of The PropertyValuesHolder.