1. The introduction

As an important part of Android animation function, attribute animation can achieve many interesting animation effects. Understanding the execution process of attribute animation helps us to better use attribute animation to achieve requirements. This article will explore the realization process of attribute animation from the perspective of source code, and deepen everyone’s cognition and understanding of it.

2. Property animation-related classes

2.1 ValueAnimator

This class is an important one for animating properties, OfFloat (), ValueAnimator.ofint (), ValueAnimator.ofObject(), ValueAnimator.ofarGB (), ValueAnimator.ofProperty Methods such as ValuesHolder() can get ValueAnimator objects, which can then be animated by manipulating them. Use ValueAnimator implement attribute animation, need to implement ValueAnimator AnimatorUpdateListener () interface, and in onAnimationUpdate () method to add animation objects within the set property values.

2.2 ObjectAnimator

ObjectAnimator is a subclass of ValueAnimator that operates on the animating properties of the target object. The constructor of this class supports passing in the name of the object and property to animate as arguments.

3. The realization process of attribute animation

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(iv, "alpha".1.0 f.0f);
objectAnimator.setDuration(3000);
objectAnimator.start();
Copy the code

This is a simple code, it uses the property animation to achieve a picture of the effect of transparency gradient, we start from this code, to analyze the property animation implementation process.

3.1 Creating property animations

/** * target: the object to which the animation is to be added * propertyName: the propertyName of the animation * values: the set of values the animation will execute between this time */
public static ObjectAnimator ofFloat(Object target, String propertyName, float. values){
    ObjectAnimator anim = new ObjectAnimator(target, propertyName);
    anim.setFloatValues(values);
    return anim;
}
Copy the code

The first parameter is the target object to animate and the second parameter is the property name. The property name of the target object should have the corresponding set() method. For example, if we pass in the property name “alpha”, the target object should also have the setAlpha() method. When values are passed, that value is the end value of the animation. When values are passed, the first value is the start value and the second value is the end value. When there are more than two values, the first value is the start value and the last value is the end value.

private ObjectAnimator(Object target, String propertyName) {
    setTarget(target);
    setPropertyName(propertyName);
}
Copy the code

This is the property animation constructor, which executes two methods setTarget(Target) and setPropertyName(propertyName).

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

MValues is an PropertyValuesHolder array. PropertyValuesHolder holds the property name and property value information for the animation. MValuesMap is a HashMap array that manages the PropertyValuesHolder object. When the getAnimatedValue(String) method is called, the map looks up the value of the animation by the property name. When mValues are not empty, the attribute name information is put into mValuesMap.

//ObjectAnimator
@Override
public void setFloatValues(float. values) {
    if (mValues == null || mValues.length == 0) {
        // No values yet - this animator is being constructed piecemeal. Init the values with
        // whatever the current propertyName is
        if(mProperty ! =null) {
            setValues(PropertyValuesHolder.ofFloat(mProperty, values));
        } else{ setValues(PropertyValuesHolder.ofFloat(mPropertyName, values)); }}else {
        super.setFloatValues(values); }}Copy the code

When mValues is null or the number of elements in the array is zero, the setValues() method of its parent ValueAnimator class is called. The setValues() method initializes mValues and mValuesMap. And put PropertyValuesHolder into mValuesMap. When mValues is not null and the number of elements is not zero, call setFloatValues() of its parent ValueAnimator, Satisfying the condition in the setFloatValues() method calls the setFloatValues() method of the PropertyValuesHolder.

//PropertyValuesHolder
public void setFloatValues(float. values) {
    mValueType = float.class;
    mKeyframes = KeyframeSet.ofFloat(values);
}
Copy the code

Here mValueType refers to the type of value provided, and mKeyframes is the set of key frames that define this animation.

//KeyframeSet
public static KeyframeSet ofFloat(float. values) {
    boolean badValue = false;
    int numKeyframes = values.length;
    FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)];
    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");
    }
    return new FloatKeyframeSet(keyframes);
}
Copy the code

In this method, we create an array of at least two FloatKeyframe elements. FloatKeyframe is an internal subclass of Keyframe that holds the time pair of the animation. The Keyframe class is used by ValueAnimator to define the value of the object during the animation. As time passes from frame to frame, the value of the target object also moves from the value of the previous frame to the value of the next frame.

/** * fraction: the value ranges from 0 to 1. * value: the value corresponding to the time in the keyframe */
public static Keyframe ofFloat(float fraction, float value) {
    return new FloatKeyframe(fraction, value);
}
Copy the code

This method creates a keyframe object with the given time and value. At this point, the creation of the property animation is almost complete.

3.2 Property animation execution process

//ObjectAnimator
@Override
public void start(a) {
    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();
}
Copy the code

The animation starts when objectAnimator.start() is called in the code, internally calling the start() method of its parent, ValueAnimator.

//ValueAnimator
private void start(boolean playBackwards) {
    if (Looper.myLooper() == null) {
        throw new AndroidRuntimeException("Animators may only be run on Looper threads"); } mReversing = playBackwards; mSelfPulse = ! mSuppressSelfPulseRequested;// Special case: reversing from seek-to-0 should act as if not seeked at all.
    if(playBackwards && mSeekFraction ! = -1&& mSeekFraction ! =0) {
        if (mRepeatCount == INFINITE) {
            // Calculate the fraction of the current iteration.
            float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
            mSeekFraction = 1 - fraction;
        } else {
            mSeekFraction = 1 + mRepeatCount - mSeekFraction;
        }
    }
    mStarted = true;
    mPaused = false;
    mRunning = false;
    mAnimationEndRequested = false;
    // Resets mLastFrameTime when start() is called, so that if the animation was running,
    // calling start() would put the animation in the
    // started-but-not-yet-reached-the-first-frame phase.
    mLastFrameTime = -1;
    mFirstFrameTime = -1;
    mStartTime = -1;
    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); }}}Copy the code

A few assignments are done within this method, with addAnimationCallback(0) and startAnimation() being the important ones.

//ValueAnimator
private void addAnimationCallback(long delay) {
    if(! mSelfPulse) {return;
    }
    getAnimationHandler().addAnimationFrameCallback(this, delay);
}
Copy the code

This method carried out AnimationHandler addAnimationFrameCallback register callback () method, we continue to see addAnimationFrameCallback () method.

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

This method adds an AnimationFrameCallback callback. AnimationFrameCallback is an internal interface to the AnimationHandler, Two important methods are doAnimationFrame() and commitAnimationFrame().

//AnimationHandler
interface AnimationFrameCallback {
		boolean doAnimationFrame(long frameTime);
  
    void commitAnimationFrame(long frameTime);
}
Copy the code

AnimationFrameCallback is a callback that receives notification of animation execution time and frame submission time. There are two methods, doAnimationFrame() and commitAnimationFrame().

//AnimationHandler
private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {

    final Choreographer mChoreographer = Choreographer.getInstance();

    @Override
    public void postFrameCallback(Choreographer.FrameCallback callback) {
        mChoreographer.postFrameCallback(callback);
    }

    @Override
    public void postCommitCallback(Runnable runnable) {
        mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
    }

    @Override
    public long getFrameTime(a) {
        return mChoreographer.getFrameTime();
    }

    @Override
    public long getFrameDelay(a) {
        return Choreographer.getFrameDelay();
    }

    @Override
    public void setFrameDelay(long delay) { Choreographer.setFrameDelay(delay); }}Copy the code

The previous getProvider() method gets an instance of MyFrameCallbackProvider, which is an inner class of AnimationHandler, Implements AnimationFrameCallbackProvider interface, use the Choreographer as a provider of timing pulse, to send a frame callback. Choreographer gets the time pulse from the display subsystem, and the postFrameCallback() method sends frame callbacks.

//AnimationHandler
public interface AnimationFrameCallbackProvider {
    void postFrameCallback(Choreographer.FrameCallback callback);
    void postCommitCallback(Runnable runnable);
    long getFrameTime(a);
    long getFrameDelay(a);
    void setFrameDelay(long delay);
}
Copy the code
//AnimationHandler
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        doAnimationFrame(getProvider().getFrameTime());
        if (mAnimationCallbacks.size() > 0) {
            getProvider().postFrameCallback(this); }}};Copy the code

Within the callback performed doAnimationFrame () method, if mAnimationCallbacks number greater than zero, AnimationFrameCallbackProvider will continue to send frame callback, Continue repeating doAnimationFrame().

//AnimationHandler   
private void doAnimationFrame(long frameTime) {
    long currentTime = SystemClock.uptimeMillis();
    final int size = mAnimationCallbacks.size();
    for (int i = 0; i < size; i++) {
        final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
        if (callback == null) {
            continue;
        }
        if (isCallbackDue(callback, currentTime)) {
            callback.doAnimationFrame(frameTime);
            if (mCommitCallbacks.contains(callback)) {
                getProvider().postCommitCallback(new Runnable() {
                    @Override
                    public void run(a) { commitAnimationFrame(callback, getProvider().getFrameTime()); }}); } } } cleanUpList(); }Copy the code

Within this method opens a loop, which perform the callback doAnimationFrame (), the operation will trigger the ValueAnimator doAnimationFrame in class ().

//ValueAnimator
private void startAnimation(a) {
    if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
        Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                System.identityHashCode(this));
    }

    mAnimationEndRequested = false;
    initAnimation();
    mRunning = true;
    if (mSeekFraction >= 0) {
        mOverallFraction = mSeekFraction;
    } else {
        mOverallFraction = 0f;
    }
    if(mListeners ! =null) { notifyStartListeners(); }}Copy the code

The startAnimation() method calls initAnimation() to initialize the animation.

//ValueAnimator
public final boolean doAnimationFrame(long frameTime) {
    // Omit some code.final long currentTime = Math.max(frameTime, mStartTime);
    boolean finished = animateBasedOnTime(currentTime);

    if (finished) {
        endAnimation();
    }
    return finished;
}   
Copy the code

This method is called several times during animation execution, and the important operation is animateBasedOnTime(currentTime).

//ValueAnimator
boolean animateBasedOnTime(long currentTime) {
    boolean done = false;
    if (mRunning) {
        final long scaledDuration = getScaledDuration();
        final float fraction = scaledDuration > 0 ?
                (float)(currentTime - mStartTime) / scaledDuration : 1f;
        final float lastFraction = mOverallFraction;
        final boolean newIteration = (int) fraction > (int) lastFraction;
        final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) && (mRepeatCount ! = INFINITE);if (scaledDuration == 0) {
            // 0 duration animator, ignore the repeat count and skip to the end
            done = true;
        } else if(newIteration && ! lastIterationFinished) {// Time to repeat
            if(mListeners ! =null) {
                int numListeners = mListeners.size();
                for (int i = 0; i < numListeners; ++i) {
                    mListeners.get(i).onAnimationRepeat(this); }}}else if (lastIterationFinished) {
            done = true;
        }
        mOverallFraction = clampFraction(fraction);
        float currentIterationFraction = getCurrentIterationFraction(
                mOverallFraction, mReversing);
        animateValue(currentIterationFraction);
    }
    return done;
} 
Copy the code

The animateBasedOnTime() method calculates the animation length and animation score that has been executed, and calls the animateValue() method to calculate the animation value.

//ValueAnimator
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

The animateValue() method of ValueAnimator first calculates the interpolation score based on the animation score, then calculates the animation value based on the interpolation score, and calls the onAnimationUpdate() method of AnimatorUpdateListener to notify the update.

//ObjectAnimator
@Override
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;
    }

    super.animateValue(fraction);
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) { mValues[i].setAnimatedValue(target); }}Copy the code

The animateValue() method of ObjectAnimator not only calls the parent animateValue() method, but also calls the setAnimatedValue() method of PropertyValuesHolder within the loop. The parameter passed in is the target object to animate.

//PropertyValuesHolder
@Override
void setAnimatedValue(Object target) {
    if(mFloatProperty ! =null) {
        mFloatProperty.setValue(target, mFloatAnimatedValue);
        return;
    }
    if(mProperty ! =null) {
        mProperty.set(target, mFloatAnimatedValue);
        return;
    }
    if(mJniSetter ! =0) {
        nCallFloatMethod(target, mJniSetter, mFloatAnimatedValue);
        return;
    }
    if(mSetter ! =null) {
        try {
            mTmpValueArray[0] = mFloatAnimatedValue;
            mSetter.invoke(target, mTmpValueArray);
        } catch (InvocationTargetException e) {
            Log.e("PropertyValuesHolder", e.toString());
        } catch (IllegalAccessException e) {
            Log.e("PropertyValuesHolder", e.toString()); }}}Copy the code

Inside the setAnimatedValue() method of PropertyValuesHolder, the property value of the target object is first modified by JNI. If no corresponding method can be found by JNI, the property value of the target object is modified by using reflection mechanism.

4. To summarize

Property animation is done by changing the property value of the target object to which the animation is to be animated. ValueAnimator calculates the animation score based on the duration of the animation and how long it has been executed. Interpolator is then used to compute the interpolating score of the animation. TypeEvaluator is then called to compute the attribute values of the object based on interpolating score, starting and ending values. The ObjectAnimator class automatically updates the object’s property values after calculating new values for the animation, while the ValueAnimator class manually sets the object’s property values.