The way to advanced Android [3] after reading this article, I won't be Android animation framework again. I kneel on the washboard

Posted by rklapwijk on Mon, 08 Nov 2021 01:32:35 +0100

Android animation can act on View/ViewGroup, activity and Fragment to achieve cool interaction effects. After several days of exploration, we have made clear the use of all kinds of animation and the implementation principle of animation, which is recorded below.
Although there are several categories of Android animation, the implementation core of various animation is timeinterpolator - > Interpolator - > various interpolators. The general process is to calculate the time-dependent input through the Interpolator, calculate various fractions through this input, calculate various state parameters (time-dependent) using the fractions calculated by various interpolators, and use these parameters to the animation effect. Android will continue to repeat this process through a certain mechanism (16ms is the cycle) to form the animation we see.

From the classification of animation, there are the following categories:

1,FrameAnimation

FrameAnimation, as its name implies, is a Frame Animation, which is realized by playing frame by frame. Frame Animation can be implemented through xml or application code:
When implementing xml, the root node must be < animation list >, and the root node contains multiple < item > elements, such as:

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot=["true" | "false"] >
    <item
        android:drawable="@drawable/frame1"
        android:duration="250" />
    <item
        android:drawable="@drawable/frame2"
        android:duration="250" />
    <item
        android:drawable="@drawable/frame3"
        android:duration="250" />
    <item
        android:drawable="@drawable/frame4"
        android:duration="250" />
</animation-list>

For this kind of animation, it is to constantly change the displayed Drawable to achieve dynamic effects.

2,TweenAnimation

You can perform a series of transformations on the View, such as pan, flip, zoom, fade in and fade out, or combine them to form a mixed animation effect. TweenAnimation can also be implemented in two ways: code and xml.
xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@[package:]anim/interpolator_resource"
    android:shareInterpolator=["true" | "false"] >
    <alpha
        android:fromAlpha="float"
        android:toAlpha="float" />
    <scale
        android:fromXScale="float"
        android:toXScale="float"
        android:fromYScale="float"
        android:toYScale="float"
        android:pivotX="float"
        android:pivotY="float" />
    <translate
        android:fromXDelta="float"
        android:toXDelta="float"
        android:fromYDelta="float"
        android:toYDelta="float" />
    <rotate
        android:fromDegrees="float"
        android:toDegrees="float"
        android:pivotX="float"
        android:pivotY="float" />
    <set>
        ...
    </set>
</set>

To load xml animation, you can use the tool classes provided by the Android SDK:

AnimationUtils.loadAnimations();

As for the code implementation method:

//The following animations are provided:
AlphaAnimation TranslateAnimation ScaleAnimation RotateAnimation
AnimationSet.addAnimation(Animation)
//The usage method is basically consistent with the xml definition

The operation of Animation depends on two methods:

 /**
     * Gets the transformation to apply at a specified point in time. Implementations of this
     * method should always replace the specified Transformation or document they are doing
     * otherwise.
     *
     * @param currentTime Where we are in the animation. This is wall clock time.
     * @param outTransformation A transformation object that is provided by the
     *        caller and will be filled in by the animation.
     * @return True if the animation is still running
     */
    public boolean getTransformation(long currentTime, Transformation outTransformation) {
        if (mStartTime == -1) {
            mStartTime = currentTime;
        }

        final long startOffset = getStartOffset();
        final long duration = mDuration;
        float normalizedTime;
        if (duration != 0) {
            normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
                    (float) duration;
        } else {
            // time is a step-change with a zero duration
            normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
        }

        final boolean expired = normalizedTime >= 1.0f || isCanceled();
        mMore = !expired;

        if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

        if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
            if (!mStarted) {
                fireAnimationStart();
                mStarted = true;
                if (NoImagePreloadHolder.USE_CLOSEGUARD) {
                    guard.open("cancel or detach or getTransformation");
                }
            }

            if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

            if (mCycleFlip) {
                normalizedTime = 1.0f - normalizedTime;
            }

            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
            applyTransformation(interpolatedTime, outTransformation);
        }

        if (expired) {
            if (mRepeatCount == mRepeated || isCanceled()) {
                if (!mEnded) {
                    mEnded = true;
                    guard.close();
                    fireAnimationEnd();
                }
            } else {
                if (mRepeatCount > 0) {
                    mRepeated++;
                }

                if (mRepeatMode == REVERSE) {
                    mCycleFlip = !mCycleFlip;
                }

                mStartTime = -1;
                mMore = true;

                fireAnimationRepeat();
            }
        }

        if (!mMore && mOneMoreTime) {
            mOneMoreTime = false;
            return true;
        }

        return mMore;
    }

protected void applyTransformation(float interpolatedTime, Transformation t) {
    }

Each time getTransformation is called, a normalized time will be calculated, and the normalized time will be passed to the interpolator as the input of the interpolator:

            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);

Get a fraction (interpolated time) calculated by the interperpolator. Then call back applyTransformation with this interpolatedTime as the parameter:

applyTransformation(interpolatedTime, outTransformation);

Another parameter of applyTransformation, outTransformation, is a Transformation object, in which two member variables are as follows:

    protected Matrix mMatrix;
    protected float mAlpha;

In other words, through this outTransformation, you can calculate its alpha or matrix, and then Android will read these two variables in the outTransformation after Animation operation, and then act on the components View/ViewGroup, Actvity and Fragment to achieve Animation effect. As for how to implement this process, there will be analysis below.
According to the above analysis, the operation of Animation depends on Android's own mechanism. The callback (getTransformation and applyTransformation have to be called back every frame) cannot calculate the fraction itself, and only the alpha and matrix in the Transformation object can participate in the operation. Therefore, Animation can only achieve simple alpha, Scale, Translate and Rotate Transformation effects.

3,PropertyAnimator

Android provides three types:

ObjectAnimator
TimeAnimator
ValueAnimator

The Animation analyzed above is limited by the callback of Android itself and can only realize the transformation of Alpha, Scale, Translate and Rotate. Animator does not have this limitation. It does not rely on the callback mechanism of Android itself, but it does rely on Looper's Thread. The source code is as follows:

 private void start(boolean playBackwards) {
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        mReversing = playBackwards;
        // 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 = 0;
        AnimationHandler animationHandler = AnimationHandler.getInstance();
        animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale));

        if (mStartDelay == 0 || mSeekFraction >= 0) {
            // 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);
            }
        }
    }

Starting from start, we can see from the code that Animator can only be started on Looper Thread. As soon as we see Looper, we suddenly realize that the implementation of Animator is inseparable from the card Handler mechanism.

AnimationHandler animationHandler = AnimationHandler.getInstance();
        animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale));

Animator obtains an AnimationHandler instance and passes itself as a callback to this AnimationHandler:

public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
     ...
}
public class AnimationHandler {
    ...

/**
     * Callbacks that receives notifications for animation timing and frame commit timing.
     */
    interface AnimationFrameCallback {
        /**
         * Run animation based on the frame time.
         * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
         *                  base.
         */
        void doAnimationFrame(long frameTime);

        /**
         * This notifies the callback of frame commit time. Frame commit time is the time after
         * traversals happen, as opposed to the normal animation frame time that is before
         * traversals. This is used to compensate expensive traversals that happen as the
         * animation starts. When traversals take a long time to complete, the rendering of the
         * initial frame will be delayed (by a long time). But since the startTime of the
         * animation is set before the traversal, by the time of next frame, a lot of time would
         * have passed since startTime was set, the animation will consequently skip a few frames
         * to respect the new frameTime. By having the commit time, we can adjust the start time to
         * when the first frame was drawn (after any expensive traversals) so that no frames
         * will be skipped.
         *
         * @param frameTime The frame time after traversals happen, if any, in the
         *                  {@link SystemClock#uptimeMillis()} time base.
         */
        void commitAnimationFrame(long frameTime);
    }
}

But we found that... This AnimationHandler is not a Handler at all.
Go back to the Animator execution process...
In the start function, we see one:

animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale));

Follow in:

/**
     * 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));
        }
    }

It is found that there is such a sentence:

getProvider().postFrameCallback(mFrameCallback);

What is a Provider?

private AnimationFrameCallbackProvider getProvider() {
        if (mProvider == null) {
            mProvider = new MyFrameCallbackProvider();
        }
        return mProvider;
    }

What is MyFrameCallbackProvider?

/**
     * Default provider of timing pulse that uses Choreographer for frame callbacks.
     */
    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() {
            return mChoreographer.getFrameTime();
        }

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

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

What is Choreographer?

public final class Choreographer {
    ...
/**
     * Posts a frame callback to run on the next frame.
     * <p>
     * The callback runs once then is automatically removed.
     * </p>
     *
     * @param callback The frame callback to run during the next frame.
     *
     * @see #postFrameCallbackDelayed
     * @see #removeFrameCallback
     */
    public void postFrameCallback(FrameCallback callback) {
        postFrameCallbackDelayed(callback, 0);
    }
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
        if (callback == null) {
            throw new IllegalArgumentException("callback must not be null");
        }

        postCallbackDelayedInternal(CALLBACK_ANIMATION,
                callback, FRAME_CALLBACK_TOKEN, delayMillis);
    }
private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        if (DEBUG_FRAMES) {
            Log.d(TAG, "PostCallback: type=" + callbackType
                    + ", action=" + action + ", token=" + token
                    + ", delayMillis=" + delayMillis);
        }

        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

See here... Finally, the prototype appears. There is an mHandler. What is mHandler?

public final class Choreographer {
    private final FrameHandler mHandler;
    ...
}
It is Choreographer A member variable of seems to be a member variable with Frame(Frame) related Handler: 
Java
private final class FrameHandler extends Handler {
        public FrameHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_DO_FRAME:
                    doFrame(System.nanoTime(), 0);
                    break;
                case MSG_DO_SCHEDULE_VSYNC:
                    doScheduleVsync();
                    break;
                case MSG_DO_SCHEDULE_CALLBACK:
                    doScheduleCallback(msg.arg1);
                    break;
            }
        }
    }

But one more thing:

getProvider().postFrameCallback(mFrameCallback);

The corresponding classes of this mFrameCallback are:

private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                getProvider().postFrameCallback(this);
            }
        }
    };

This is another series of complex callbacks, which can not be understood or written after analysis, but the FrameHandler's doFrame will eventually call our mFrameCallback, that is, it will call doAnimationFrame, and finally call the animateValue method of Animator. Then everyone knows... onAnimationUpdate of AnimatorUpdateListener is called. After a meal of analysis, I understand the general process, but the mechanism is too complex, involving too many aspects and too many classes. When can I have such design ability..
About the Choreographer class:
*https://www.cnblogs.com/kross/p/4087780.html
This document has a more detailed analysis, so I can't understand so much for the time being.

Others

The comments on the source code of the chromeographer are as follows:

/**
 * Coordinates the timing of animations, input and drawing.
 * <p>
 * The choreographer receives timing pulses (such as vertical synchronization)
 * from the display subsystem then schedules work to occur as part of rendering
 * the next display frame.
 * </p><p>
 * Applications typically interact with the choreographer indirectly using
 * higher level abstractions in the animation framework or the view hierarchy.
 * Here are some examples of things you can do using the higher-level APIs.
 * </p>
 * <ul>
 * <li>To post an animation to be processed on a regular time basis synchronized with
 * display frame rendering, use {@link android.animation.ValueAnimator#start}.</li>
 * <li>To post a {@link Runnable} to be invoked once at the beginning of the next display
 * frame, use {@link View#postOnAnimation}.</li>
 * <li>To post a {@link Runnable} to be invoked once at the beginning of the next display
 * frame after a delay, use {@link View#postOnAnimationDelayed}.</li>
 * <li>To post a call to {@link View#invalidate()} to occur once at the beginning of the
 * next display frame, use {@link View#postInvalidateOnAnimation()} or
 * {@link View#postInvalidateOnAnimation(int, int, int, int)}.</li>
 * <li>To ensure that the contents of a {@link View} scroll smoothly and are drawn in
 * sync with display frame rendering, do nothing.  This already happens automatically.
 * {@link View#onDraw} will be called at the appropriate time.</li>
 * </ul>
 * <p>
 * However, there are a few cases where you might want to use the functions of the
 * choreographer directly in your application.  Here are some examples.
 * </p>
 * <ul>
 * <li>If your application does its rendering in a different thread, possibly using GL,
 * or does not use the animation framework or view hierarchy at all
 * and you want to ensure that it is appropriately synchronized with the display, then use
 * {@link Choreographer#postFrameCallback}.</li>
 * <li>... and that's about it.</li>
 * </ul>
 * <p>
 * Each {@link Looper} thread has its own choreographer.  Other threads can
 * post callbacks to run on the choreographer but they will run on the {@link Looper}
 * to which the choreographer belongs.
 * </p>
 */

A sentence caught my attention:
If your application does its rendering in a different thread, possibly using GL,

  • or does not use the animation framework or view hierarchy at all
    In other words, our View can only handle events, animate, or draw within the thread that created it. The reason is that the implementation of these frameworks and mechanisms depends on this class, which is thread related. To render in other threads, you can directly use Choreographer#postFrameCallback???
    To be explored..

Article transferred from https://www.jianshu.com/p/435... , in case of infringement, please contact to delete.

Related videos:


[Android basic course] advanced UI, from zero to proficient (2)_ Beep beep beep_ bilibili
[advanced Android system learning - what should an architect do in the face of an endless stream of third-party SDK s? _ bilibili bili
Teach you to quickly grow into a mobile rack engineer with core competitiveness
Android architect's core technology - general design scheme of request framework

Topics: Android Programmer