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
This commit is contained in:
wilsonshih
2022-02-08 10:06:28 +08:00
parent 17a66a2236
commit 5e0c72a2c5
6 changed files with 144 additions and 82 deletions

View File

@@ -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.

View File

@@ -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();

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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,14 +270,37 @@ 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 scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what,
long when) {
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
unscheduleSelf(what);
}
};
mForegroundDrawable.setCallback(callback);
mSplashScreenExecutor = splashScreenExecutor;
mAnimatableIcon = (Animatable) mForegroundDrawable;
}
@Override
@@ -282,83 +309,68 @@ public class SplashscreenIconDrawableFactory {
}
@Override
public boolean prepareAnimate(long duration, Runnable startListener) {
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();
}
try {
if (mJankMonitoringListener != null) {
mJankMonitoringListener.onAnimationStart(animation);
}
mAnimatableIcon.start();
} catch (Exception ex) {
Log.e(TAG, "Error while running the splash screen animated icon", ex);
animation.cancel();
}
}
public void prepareAnimate(long duration, LongConsumer startListener) {
stopAnimation();
mDuration = duration;
mStartListener = startListener;
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimatableIcon.stop();
if (mJankMonitoringListener != null) {
mJankMonitoringListener.onAnimationEnd(animation);
}
private void startAnimation() {
if (mJankMonitoringListener != null) {
mJankMonitoringListener.onAnimationStart(null);
}
try {
mAnimatableIcon.start();
} catch (Exception ex) {
Log.e(TAG, "Error while running the splash screen animated icon", ex);
mRunning = false;
if (mJankMonitoringListener != null) {
mJankMonitoringListener.onAnimationCancel(null);
}
if (mStartListener != null) {
mStartListener.accept(mDuration);
}
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();
}
mRunning = true;
mSplashScreenExecutor.executeDelayed(this::stopAnimation, animDuration);
if (mStartListener != null) {
mStartListener.accept(Math.max(animDuration, 0));
}
}
@Override
public void onAnimationCancel(Animator animation) {
mAnimatableIcon.stop();
if (mJankMonitoringListener != null) {
mJankMonitoringListener.onAnimationCancel(animation);
}
}
@Override
public void onAnimationRepeat(Animator animation) {
// do not repeat
mAnimatableIcon.stop();
}
});
return true;
private void onAnimationEnd() {
mAnimatableIcon.stop();
if (mJankMonitoringListener != null) {
mJankMonitoringListener.onAnimationEnd(null);
}
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;
}

View File

@@ -148,7 +148,8 @@ public class StartingSurfaceDrawer {
mContext = context;
mDisplayManager = mContext.getSystemService(DisplayManager.class);
mSplashScreenExecutor = splashScreenExecutor;
mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool);
mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool,
mSplashScreenExecutor);
mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance());
mWindowManagerGlobal = WindowManagerGlobal.getInstance();
mDisplayManager.getDisplay(DEFAULT_DISPLAY);