Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5e0c72a2 authored by wilsonshih's avatar wilsonshih
Browse files

Estimate animation duration for AVD and AnimationDrawable

For Android S, we use the API windowSplashScreenAnimationDuration to
know the duration of the animatable icon would be, however, developers
can be confused to thought that this API is used to declare the
duration of splash screen.
To get the real duration of the animatable icon, if it is an AVD or
AnimationDrawable, try to get the animation duration from it.

Test: atest StartingSurfaceDrawerTests SplashscreenTests
Bug: 211707095

Change-Id: Ia3f8c5cd50bab164360e07d9ced87ff7f4321e96
parent 17a66a22
Loading
Loading
Loading
Loading
+7 −4
Original line number Diff line number Diff line
@@ -65,6 +65,7 @@ import com.android.internal.util.ContrastColorUtil;
import java.time.Duration;
import java.time.Instant;
import java.util.function.Consumer;
import java.util.function.LongConsumer;

/**
 * <p>The view which allows an activity to customize its splash screen exit animation.</p>
@@ -234,7 +235,7 @@ public final class SplashScreenView extends FrameLayout {
        /**
         * Set the animation duration if icon is animatable.
         */
        public Builder setAnimationDurationMillis(int duration) {
        public Builder setAnimationDurationMillis(long duration) {
            mIconAnimationDuration = Duration.ofMillis(duration);
            return this;
        }
@@ -521,8 +522,11 @@ public final class SplashScreenView extends FrameLayout {
        });
    }

    private void animationStartCallback() {
    private void animationStartCallback(long animDuration) {
        mIconAnimationStart = Instant.now();
        if (animDuration > 0) {
            mIconAnimationDuration = Duration.ofMillis(animDuration);
        }
    }

    /**
@@ -693,9 +697,8 @@ public final class SplashScreenView extends FrameLayout {
         * Prepare the animation if this drawable also be animatable.
         * @param duration The animation duration.
         * @param startListener The callback listener used to receive the start of the animation.
         * @return true if this drawable object can also be animated and it can be played now.
         */
        boolean prepareAnimate(long duration, Runnable startListener);
        void prepareAnimate(long duration, LongConsumer startListener);

        /**
         * Stop animation.
+25 −2
Original line number Diff line number Diff line
@@ -690,6 +690,14 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 {
        }
    }

    /**
     * Gets the total duration of the animation
     * @hide
     */
    public long getTotalDuration() {
        return mAnimatorSet.getTotalDuration();
    }

    private static class AnimatedVectorDrawableState extends ConstantState {
        @Config int mChangingConfigurations;
        VectorDrawable mVectorDrawable;
@@ -1074,6 +1082,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 {
        boolean isInfinite();
        void pause();
        void resume();
        long getTotalDuration();
    }

    private static class VectorDrawableAnimatorUI implements VectorDrawableAnimator {
@@ -1085,6 +1094,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 {
        // setup by init().
        private ArrayList<AnimatorListener> mListenerArray = null;
        private boolean mIsInfinite = false;
        private long mTotalDuration;

        VectorDrawableAnimatorUI(@NonNull AnimatedVectorDrawable drawable) {
            mDrawable = drawable;
@@ -1100,7 +1110,8 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 {
            // Keep a deep copy of the set, such that set can be still be constantly representing
            // the static content from XML file.
            mSet = set.clone();
            mIsInfinite = mSet.getTotalDuration() == Animator.DURATION_INFINITE;
            mTotalDuration = mSet.getTotalDuration();
            mIsInfinite = mTotalDuration == Animator.DURATION_INFINITE;

            // If there are listeners added before calling init(), now they should be setup.
            if (mListenerArray != null && !mListenerArray.isEmpty()) {
@@ -1219,6 +1230,11 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 {
        private void invalidateOwningView() {
            mDrawable.invalidateSelf();
        }

        @Override
        public long getTotalDuration() {
            return mTotalDuration;
        }
    }

    /**
@@ -1249,6 +1265,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 {
        private int mLastListenerId = 0;
        private final IntArray mPendingAnimationActions = new IntArray();
        private final AnimatedVectorDrawable mDrawable;
        private long mTotalDuration;

        VectorDrawableAnimatorRT(AnimatedVectorDrawable drawable) {
            mDrawable = drawable;
@@ -1270,7 +1287,8 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 {
                    .getNativeTree();
            nSetVectorDrawableTarget(mSetPtr, vectorDrawableTreePtr);
            mInitialized = true;
            mIsInfinite = set.getTotalDuration() == Animator.DURATION_INFINITE;
            mTotalDuration = set.getTotalDuration();
            mIsInfinite = mTotalDuration == Animator.DURATION_INFINITE;

            // Check reversible.
            mIsReversible = true;
@@ -1796,6 +1814,11 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 {
            }
            mPendingAnimationActions.clear();
        }

        @Override
        public long getTotalDuration() {
            return mTotalDuration;
        }
    }

    private static native long nCreateAnimatorSet();
+19 −0
Original line number Diff line number Diff line
@@ -424,6 +424,17 @@ public class AnimationDrawable extends DrawableContainer implements Runnable, An
            System.arraycopy(mDurations, 0, newDurations, 0, oldSize);
            mDurations = newDurations;
        }

        public long getTotalDuration() {
            if (mDurations != null) {
                int total = 0;
                for (int dur : mDurations) {
                    total += dur;
                }
                return total;
            }
            return 0;
        }
    }

    @Override
@@ -435,6 +446,14 @@ public class AnimationDrawable extends DrawableContainer implements Runnable, An
        }
    }

    /**
     * Gets the total duration of the animation
     * @hide
     */
    public long getTotalDuration() {
        return mAnimationState.getTotalDuration();
    }

    private AnimationDrawable(AnimationState state, Resources res) {
        final AnimationState as = new AnimationState(state, this, res);
        setConstantState(as);
+9 −5
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.protolog.ShellProtoLogGroup;

@@ -115,8 +116,10 @@ public class SplashscreenContentDrawer {
    private final Handler mSplashscreenWorkerHandler;
    @VisibleForTesting
    final ColorCache mColorCache;
    private final ShellExecutor mSplashScreenExecutor;

    SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool) {
    SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool,
            ShellExecutor splashScreenExecutor) {
        mContext = context;
        mIconProvider = iconProvider;
        mTransactionPool = pool;
@@ -129,6 +132,7 @@ public class SplashscreenContentDrawer {
        shellSplashscreenWorkerThread.start();
        mSplashscreenWorkerHandler = shellSplashscreenWorkerThread.getThreadHandler();
        mColorCache = new ColorCache(mContext, mSplashscreenWorkerHandler);
        mSplashScreenExecutor = splashScreenExecutor;
    }

    /**
@@ -397,7 +401,7 @@ public class SplashscreenContentDrawer {

        SplashScreenView build() {
            Drawable iconDrawable;
            final int animationDuration;
            final long animationDuration;
            if (mSuggestType == STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN
                    || mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
                // empty or legacy splash screen case
@@ -455,8 +459,8 @@ public class SplashscreenContentDrawer {
                        iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler);
            } else {
                mFinalIconDrawables = SplashscreenIconDrawableFactory.makeIconDrawable(
                        mTmpAttrs.mIconBgColor, mThemeColor,
                        iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler);
                        mTmpAttrs.mIconBgColor, mThemeColor, iconDrawable, mDefaultIconSize,
                        mFinalIconSize, mSplashscreenWorkerHandler, mSplashScreenExecutor);
            }
        }

@@ -516,7 +520,7 @@ public class SplashscreenContentDrawer {
        }

        private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable,
                int animationDuration, Consumer<Runnable> uiThreadInitTask) {
                long animationDuration, Consumer<Runnable> uiThreadInitTask) {
            Drawable foreground = null;
            Drawable background = null;
            if (iconDrawable != null) {
+81 −69
Original line number Diff line number Diff line
@@ -18,9 +18,7 @@ package com.android.wm.shell.startingsurface;

import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -36,6 +34,8 @@ import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Trace;
@@ -44,6 +44,9 @@ import android.util.PathParser;
import android.window.SplashScreenView;

import com.android.internal.R;
import com.android.wm.shell.common.ShellExecutor;

import java.util.function.LongConsumer;

/**
 * Creating a lightweight Drawable object used for splash screen.
@@ -60,14 +63,15 @@ public class SplashscreenIconDrawableFactory {
     */
    static Drawable[] makeIconDrawable(@ColorInt int backgroundColor, @ColorInt int themeColor,
            @NonNull Drawable foregroundDrawable, int srcIconSize, int iconSize,
            Handler splashscreenWorkerHandler) {
            Handler splashscreenWorkerHandler, ShellExecutor splashScreenExecutor) {
        Drawable foreground;
        Drawable background = null;
        boolean drawBackground =
                backgroundColor != Color.TRANSPARENT && backgroundColor != themeColor;

        if (foregroundDrawable instanceof Animatable) {
            foreground = new AnimatableIconAnimateListener(foregroundDrawable);
            foreground = new AnimatableIconAnimateListener(foregroundDrawable,
                    splashScreenExecutor);
        } else if (foregroundDrawable instanceof AdaptiveIconDrawable) {
            // If the icon is Adaptive, we already use the icon background.
            drawBackground = false;
@@ -266,99 +270,107 @@ public class SplashscreenIconDrawableFactory {
     */
    public static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable
            implements SplashScreenView.IconAnimateListener {
        private Animatable mAnimatableIcon;
        private Animator mIconAnimator;
        private final Animatable mAnimatableIcon;
        private boolean mAnimationTriggered;
        private AnimatorListenerAdapter mJankMonitoringListener;
        private boolean mRunning;
        private long mDuration;
        private LongConsumer mStartListener;
        private final ShellExecutor mSplashScreenExecutor;

        AnimatableIconAnimateListener(@NonNull Drawable foregroundDrawable) {
        AnimatableIconAnimateListener(@NonNull Drawable foregroundDrawable,
                ShellExecutor splashScreenExecutor) {
            super(foregroundDrawable);
            mForegroundDrawable.setCallback(mCallback);
            Callback callback = new Callback() {
                @Override
                public void invalidateDrawable(@NonNull Drawable who) {
                    invalidateSelf();
                }

                @Override
        public void setAnimationJankMonitoring(AnimatorListenerAdapter listener) {
            mJankMonitoringListener = listener;
                public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what,
                        long when) {
                    scheduleSelf(what, when);
                }

                @Override
        public boolean prepareAnimate(long duration, Runnable startListener) {
                public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
                    unscheduleSelf(what);
                }
            };
            mForegroundDrawable.setCallback(callback);
            mSplashScreenExecutor = splashScreenExecutor;
            mAnimatableIcon = (Animatable) mForegroundDrawable;
            mIconAnimator = ValueAnimator.ofInt(0, 1);
            mIconAnimator.setDuration(duration);
            mIconAnimator.addListener(new Animator.AnimatorListener() {
        }

        @Override
                public void onAnimationStart(Animator animation) {
                    if (startListener != null) {
                        startListener.run();
        public void setAnimationJankMonitoring(AnimatorListenerAdapter listener) {
            mJankMonitoringListener = listener;
        }
                    try {

        @Override
        public void prepareAnimate(long duration, LongConsumer startListener) {
            stopAnimation();
            mDuration = duration;
            mStartListener = startListener;
        }

        private void startAnimation() {
            if (mJankMonitoringListener != null) {
                            mJankMonitoringListener.onAnimationStart(animation);
                mJankMonitoringListener.onAnimationStart(null);
            }
            try {
                mAnimatableIcon.start();
            } catch (Exception ex) {
                Log.e(TAG, "Error while running the splash screen animated icon", ex);
                        animation.cancel();
                mRunning = false;
                if (mJankMonitoringListener != null) {
                    mJankMonitoringListener.onAnimationCancel(null);
                }
                if (mStartListener != null) {
                    mStartListener.accept(mDuration);
                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    mAnimatableIcon.stop();
                    if (mJankMonitoringListener != null) {
                        mJankMonitoringListener.onAnimationEnd(animation);
                return;
            }
            long animDuration = mDuration;
            if (mAnimatableIcon instanceof AnimatedVectorDrawable
                    && ((AnimatedVectorDrawable) mAnimatableIcon).getTotalDuration() > 0) {
                animDuration = ((AnimatedVectorDrawable) mAnimatableIcon).getTotalDuration();
            } else if (mAnimatableIcon instanceof AnimationDrawable
                    && ((AnimationDrawable) mAnimatableIcon).getTotalDuration() > 0) {
                animDuration = ((AnimationDrawable) mAnimatableIcon).getTotalDuration();
            }

                @Override
                public void onAnimationCancel(Animator animation) {
                    mAnimatableIcon.stop();
                    if (mJankMonitoringListener != null) {
                        mJankMonitoringListener.onAnimationCancel(animation);
            mRunning = true;
            mSplashScreenExecutor.executeDelayed(this::stopAnimation, animDuration);
            if (mStartListener != null) {
                mStartListener.accept(Math.max(animDuration, 0));
            }
        }

                @Override
                public void onAnimationRepeat(Animator animation) {
                    // do not repeat
        private void onAnimationEnd() {
            mAnimatableIcon.stop();
            if (mJankMonitoringListener != null) {
                mJankMonitoringListener.onAnimationEnd(null);
            }
            });
            return true;
            mStartListener = null;
            mRunning = false;
        }

        @Override
        public void stopAnimation() {
            if (mIconAnimator != null && mIconAnimator.isRunning()) {
                mIconAnimator.end();
            if (mRunning) {
                mSplashScreenExecutor.removeCallbacks(this::stopAnimation);
                onAnimationEnd();
                mJankMonitoringListener = null;
            }
        }

        private final Callback mCallback = new Callback() {
            @Override
            public void invalidateDrawable(@NonNull Drawable who) {
                invalidateSelf();
            }

            @Override
            public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
                scheduleSelf(what, when);
            }

            @Override
            public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
                unscheduleSelf(what);
            }
        };

        private void ensureAnimationStarted() {
            if (mAnimationTriggered) {
                return;
            }
            if (mIconAnimator != null && !mIconAnimator.isRunning()) {
                mIconAnimator.start();
            if (!mRunning) {
                startAnimation();
            }
            mAnimationTriggered = true;
        }
Loading