From ae7ee3f3ef81ff730cfa99344d9983f2e5148537 Mon Sep 17 00:00:00 2001 From: wilsonshih Date: Fri, 30 Oct 2020 17:00:11 +0800 Subject: [PATCH] Provide new APIs for customize splash screen window.(3/N) Create new APIs for developer to customize starting window - windowSplashScreenBackground: specify the background color - windowSplashScreenAnimatedIcon: replace center icon on starting window, and it can be animatable. - windowSplashScreenAnimationDuration: the animation duration if the replace icon is animatable, it cannot exceed max_starting_window_intro_icon_anim_duration. Support ADV to replace the icon on starting window. Ref doc: go/improved_app_launch_animations Bug: 73289295 Test: build/flash Test: check splash screen starting window. Test: atest StartingSurfaceDrawerTests SplashscreenTests Test: atest ShellTaskOrganizerTests Change-Id: Id3de3e9b57d769f096baf43713c1e3c327ecfdc8 --- core/api/current.txt | 3 + core/api/test-current.txt | 4 + .../java/android/window/SplashScreenView.java | 80 +++++++++++++++++++ core/res/res/values/attrs.xml | 12 +++ core/res/res/values/public.xml | 3 + core/res/res/values/themes.xml | 2 + .../WindowManager/Shell/res/values/config.xml | 3 + .../android/wm/shell/ShellTaskOrganizer.java | 19 +++-- .../SplashscreenContentDrawer.java | 77 ++++++++++++++---- .../StartingSurfaceDrawer.java | 74 ++++++++++++++++- .../wm/shell/ShellTaskOrganizerTests.java | 5 +- 11 files changed, 254 insertions(+), 28 deletions(-) diff --git a/core/api/current.txt b/core/api/current.txt index ffe14c3f19542..efbc60fb5280c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -1655,6 +1655,9 @@ package android { field public static final int windowShowAnimation = 16842934; // 0x10100b6 field public static final int windowShowWallpaper = 16843410; // 0x1010292 field public static final int windowSoftInputMode = 16843307; // 0x101022b + field public static final int windowSplashScreenAnimatedIcon = 16844333; // 0x101062d + field public static final int windowSplashScreenAnimationDuration = 16844334; // 0x101062e + field public static final int windowSplashScreenBackground = 16844332; // 0x101062c field public static final int windowSplashscreenContent = 16844132; // 0x1010564 field @Deprecated public static final int windowSwipeToDismiss = 16843763; // 0x10103f3 field public static final int windowTitleBackgroundStyle = 16842844; // 0x101005c diff --git a/core/api/test-current.txt b/core/api/test-current.txt index cefd1b153268b..7b373201a3fc9 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2729,6 +2729,10 @@ package android.window { field public static final int FEATURE_WINDOW_TOKENS = 2; // 0x2 } + public final class SplashScreenView extends android.widget.FrameLayout { + method public boolean isIconAnimating(); + } + public final class StartingWindowInfo implements android.os.Parcelable { ctor public StartingWindowInfo(); method public int describeContents(); diff --git a/core/java/android/window/SplashScreenView.java b/core/java/android/window/SplashScreenView.java index b8d3fc2eda05b..3d3d3468c6da4 100644 --- a/core/java/android/window/SplashScreenView.java +++ b/core/java/android/window/SplashScreenView.java @@ -17,14 +17,18 @@ package android.window; import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; +import android.animation.Animator; +import android.animation.ValueAnimator; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; +import android.graphics.drawable.Animatable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Parcel; @@ -65,6 +69,11 @@ public final class SplashScreenView extends FrameLayout { private int mInitBackgroundColor; private View mIconView; private Bitmap mParceledBitmap; + + private Animatable mAnimatableIcon; + private ValueAnimator mAnimator; + private Runnable mAnimationFinishListener; + // cache original window and status private Window mWindow; private boolean mDrawBarBackground; @@ -81,6 +90,7 @@ public final class SplashScreenView extends FrameLayout { private @ColorInt int mBackgroundColor; private Bitmap mParceledBitmap; private Drawable mIconDrawable; + private int mIconAnimationDuration; public Builder(@NonNull Context context) { mContext = context; @@ -124,6 +134,14 @@ public final class SplashScreenView extends FrameLayout { return this; } + /** + * Set the animation duration if icon is animatable. + */ + public Builder setAnimationDuration(int duration) { + mIconAnimationDuration = duration; + return this; + } + /** * Create SplashScreenWindowView object from materials. */ @@ -142,6 +160,7 @@ public final class SplashScreenView extends FrameLayout { } if (mIconDrawable != null) { view.mIconView.setBackground(mIconDrawable); + view.initIconAnimation(mIconDrawable, mIconAnimationDuration); } if (mParceledBitmap != null) { view.mParceledBitmap = mParceledBitmap; @@ -180,6 +199,66 @@ public final class SplashScreenView extends FrameLayout { return !mNotCopyable; } + void initIconAnimation(Drawable iconDrawable, int duration) { + if (iconDrawable instanceof Animatable) { + mAnimatableIcon = (Animatable) iconDrawable; + mAnimator = ValueAnimator.ofInt(0, 1); + mAnimator.setDuration(duration); + + mAnimator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + mAnimatableIcon.start(); + } + + @Override + public void onAnimationEnd(Animator animation) { + mAnimatableIcon.stop(); + onIconAnimationFinish(); + } + + @Override + public void onAnimationCancel(Animator animation) { + mAnimatableIcon.stop(); + onIconAnimationFinish(); + } + + @Override + public void onAnimationRepeat(Animator animation) { + // do not repeat + mAnimatableIcon.stop(); + } + }); + } + } + + private void onIconAnimationFinish() { + if (mAnimationFinishListener != null) { + mAnimationFinishListener.run(); + mAnimationFinishListener = null; + } + } + + /** + * @hide + */ + @TestApi + public boolean isIconAnimating() { + return mAnimatableIcon != null && mAnimator.isRunning(); + } + + /** + * @hide + */ + public void startIntroAnimation(Runnable finishListener) { + if (mAnimatableIcon != null) { + mAnimationFinishListener = finishListener; + mAnimator.start(); + } else if (finishListener != null) { + finishListener.run(); + } + } + /** *

Remove this view and release its resource.

*

Do not invoke this method from a drawing method @@ -244,6 +323,7 @@ public final class SplashScreenView extends FrameLayout { /** * Get the view containing the Splash Screen icon and its background. + * @see android.R.attr#windowSplashScreenAnimatedIcon */ public @Nullable View getIconView() { return mIconView; diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 823e10edaad51..6949c4bb9f41c 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2262,6 +2262,18 @@ --> + + + + + + + diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 3c97aec5d55d8..8f4d8baa845d6 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -3066,6 +3066,9 @@ + + + diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml index 049ba23a1a97e..325e0578425d8 100644 --- a/core/res/res/values/themes.xml +++ b/core/res/res/values/themes.xml @@ -167,6 +167,8 @@ please see themes_device_defaults.xml. @drawable/screen_background_selector_dark ?attr/colorBackground + @color/transparent + @null false @null false diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 13f1fddfdfb6c..24198659e15d6 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -48,4 +48,7 @@ 0.5 + + + 1000 diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index a9d66ac945932..03724171da217 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -25,6 +25,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.os.Binder; @@ -38,9 +40,6 @@ import android.window.StartingWindowInfo; import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; @@ -111,23 +110,23 @@ public class ShellTaskOrganizer extends TaskOrganizer { private final SizeCompatUIController mSizeCompatUI; public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) { - this(null /* taskOrganizerController */, mainExecutor, context, null /* sizeCompatUI */); + this(null /* taskOrganizerController */, mainExecutor, context, null /* sizeCompatUI */, + new StartingSurfaceDrawer(context, mainExecutor)); } public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable SizeCompatUIController sizeCompatUI) { - this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI); + this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI, + new StartingSurfaceDrawer(context, mainExecutor)); } @VisibleForTesting ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, ShellExecutor mainExecutor, - Context context, @Nullable SizeCompatUIController sizeCompatUI) { + Context context, @Nullable SizeCompatUIController sizeCompatUI, + StartingSurfaceDrawer startingSurfaceDrawer) { super(taskOrganizerController, mainExecutor); - // TODO(b/131727939) temporarily live here, the starting surface drawer should be controlled - // by a controller, that class should be create while porting - // ActivityRecord#addStartingWindow to WMShell. - mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, mainExecutor); mSizeCompatUI = sizeCompatUI; + mStartingSurfaceDrawer = startingSurfaceDrawer; } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index 0e74c642520b4..db0a4a6ad731b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -43,8 +43,9 @@ import java.util.List; /** * Util class to create the view for a splash screen content. + * @hide */ -class SplashscreenContentDrawer { +public class SplashscreenContentDrawer { private static final String TAG = StartingSurfaceDrawer.TAG; private static final boolean DEBUG = StartingSurfaceDrawer.DEBUG_SPLASH_SCREEN; @@ -55,10 +56,13 @@ class SplashscreenContentDrawer { // also 108*108 pixels, then do not enlarge this icon if only need to show foreground icon. private static final float ENLARGE_FOREGROUND_ICON_THRESHOLD = (72f * 72f) / (108f * 108f); private final Context mContext; + private final int mMaxIconAnimationDuration; + private int mIconSize; - SplashscreenContentDrawer(Context context) { + SplashscreenContentDrawer(Context context, int maxIconAnimationDuration) { mContext = context; + mMaxIconAnimationDuration = maxIconAnimationDuration; } private void updateDensity() { @@ -90,33 +94,71 @@ class SplashscreenContentDrawer { return ssc; } - final TypedArray typedArray = context.obtainStyledAttributes( - com.android.internal.R.styleable.Window); - final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0); - typedArray.recycle(); + final SplashScreenWindowAttrs attrs = + SplashScreenWindowAttrs.createWindowAttrs(context); + final StartingWindowViewBuilder builder = new StartingWindowViewBuilder(); final Drawable themeBGDrawable; - if (resId == 0) { + + if (attrs.mWindowBgColor != 0) { + themeBGDrawable = new ColorDrawable(attrs.mWindowBgColor); + } else if (attrs.mWindowBgResId != 0) { + themeBGDrawable = context.getDrawable(attrs.mWindowBgResId); + } else { Slog.w(TAG, "Window background not exist!"); themeBGDrawable = createDefaultBackgroundDrawable(); - } else { - themeBGDrawable = context.getDrawable(resId); } - final Drawable iconDrawable = iconRes != 0 ? context.getDrawable(iconRes) - : context.getPackageManager().getDefaultActivityIcon(); + final int animationDuration; + final Drawable iconDrawable; + if (attrs.mReplaceIcon != null) { + iconDrawable = attrs.mReplaceIcon; + animationDuration = Math.max(0, + Math.min(attrs.mAnimationDuration, mMaxIconAnimationDuration)); + } else { + iconDrawable = iconRes != 0 ? context.getDrawable(iconRes) + : context.getPackageManager().getDefaultActivityIcon(); + animationDuration = 0; + } // TODO (b/173975965) Tracking the performance on improved splash screen. - final StartingWindowViewBuilder builder = new StartingWindowViewBuilder(); return builder .setWindow(win) .setContext(context) .setThemeDrawable(themeBGDrawable) - .setIconDrawable(iconDrawable).build(); + .setIconDrawable(iconDrawable) + .setIconAnimationDuration(animationDuration).build(); + } + + private static class SplashScreenWindowAttrs { + private int mWindowBgResId = 0; + private int mWindowBgColor = Color.TRANSPARENT; + private Drawable mReplaceIcon = null; + private int mAnimationDuration = 0; + + static SplashScreenWindowAttrs createWindowAttrs(Context context) { + final SplashScreenWindowAttrs attrs = new SplashScreenWindowAttrs(); + final TypedArray typedArray = context.obtainStyledAttributes( + com.android.internal.R.styleable.Window); + attrs.mWindowBgResId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0); + attrs.mWindowBgColor = typedArray.getColor( + R.styleable.Window_windowSplashScreenBackground, Color.TRANSPARENT); + attrs.mReplaceIcon = typedArray.getDrawable( + R.styleable.Window_windowSplashScreenAnimatedIcon); + attrs.mAnimationDuration = typedArray.getInt( + R.styleable.Window_windowSplashScreenAnimationDuration, 0); + typedArray.recycle(); + if (DEBUG) { + Slog.d(TAG, "window attributes color: " + + Integer.toHexString(attrs.mWindowBgColor) + + " icon " + attrs.mReplaceIcon + " duration " + attrs.mAnimationDuration); + } + return attrs; + } } private class StartingWindowViewBuilder { - // materials private Drawable mThemeBGDrawable; private Drawable mIconDrawable; private Window mWindow; + private int mIconAnimationDuration; private Context mContext; // result @@ -144,6 +186,12 @@ class SplashscreenContentDrawer { return this; } + StartingWindowViewBuilder setIconAnimationDuration(int iconAnimationDuration) { + mIconAnimationDuration = iconAnimationDuration; + mBuildComplete = false; + return this; + } + StartingWindowViewBuilder setContext(Context context) { mContext = context; mBuildComplete = false; @@ -253,6 +301,7 @@ class SplashscreenContentDrawer { if (iconDrawable != null) { builder.setCenterViewDrawable(iconDrawable); } + builder.setAnimationDuration(mIconAnimationDuration); final SplashScreenView splashScreenView = builder.build(); if (DEBUG) { Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + splashScreenView); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java index 03935cfd4259d..e4c3a0569c661 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java @@ -74,6 +74,8 @@ public class StartingSurfaceDrawer { private final DisplayManager mDisplayManager; final ShellExecutor mMainExecutor; private final SplashscreenContentDrawer mSplashscreenContentDrawer; + private final IconAnimationFinishListener mIconAnimationFinishListener = + new IconAnimationFinishListener(); // TODO(b/131727939) remove this when clearing ActivityRecord private static final int REMOVE_WHEN_TIMEOUT = 2000; @@ -82,7 +84,10 @@ public class StartingSurfaceDrawer { mContext = context; mDisplayManager = mContext.getSystemService(DisplayManager.class); mMainExecutor = mainExecutor; - mSplashscreenContentDrawer = new SplashscreenContentDrawer(context); + + final int maxIconAnimDuration = context.getResources().getInteger( + com.android.wm.shell.R.integer.max_starting_window_intro_icon_anim_duration); + mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, maxIconAnimDuration); } private final SparseArray mStartingWindowRecords = new SparseArray<>(); @@ -401,10 +406,19 @@ public class StartingSurfaceDrawer { */ public void copySplashScreenView(int taskId) { final StartingWindowRecord preView = mStartingWindowRecords.get(taskId); - final SplashScreenViewParcelable parcelable; + SplashScreenViewParcelable parcelable; if (preView != null && preView.mContentView != null && preView.mContentView.isCopyable()) { - parcelable = new SplashScreenViewParcelable(preView.mContentView); + if (preView.mContentView.isIconAnimating()) { + // if animating, wait until animation finish + if (DEBUG_SPLASH_SCREEN) { + Slog.v(TAG, "Copying splash screen view but icon is animating " + taskId); + } + mIconAnimationFinishListener.waitingForCopyTask(taskId); + return; + } else { + parcelable = new SplashScreenViewParcelable(preView.mContentView); + } } else { parcelable = null; } @@ -445,11 +459,60 @@ public class StartingSurfaceDrawer { mMainExecutor.executeDelayed(() -> removeWindowSynced(taskId), REMOVE_WHEN_TIMEOUT); final StartingWindowRecord tView = new StartingWindowRecord(view, null /* TaskSnapshotWindow */, splashScreenView); + splashScreenView.startIntroAnimation( + mIconAnimationFinishListener.makeListener(taskId)); mStartingWindowRecords.put(taskId, tView); } }); } + private class IconAnimationFinishListener { + private final SparseArray mListeners = new SparseArray<>(); + + private class TaskListener implements Runnable { + private int mTargetTaskId; + private boolean mWaitingForCopy; + private boolean mWaitingForRemove; + @Override + public void run() { + if (mWaitingForCopy) { + if (DEBUG_SPLASH_SCREEN) { + Slog.v(TAG, "Icon animation finish and waiting for copy at task " + + mTargetTaskId); + } + copySplashScreenView(mTargetTaskId); + } + if (mWaitingForRemove) { + if (DEBUG_SPLASH_SCREEN) { + Slog.v(TAG, "Icon animation finish and waiting for remove at task " + + mTargetTaskId); + } + mMainExecutor.execute(() -> removeWindowSynced(mTargetTaskId)); + } + mListeners.remove(mTargetTaskId); + } + } + + private Runnable makeListener(int taskId) { + final TaskListener listener = new TaskListener(); + listener.mTargetTaskId = taskId; + mListeners.put(taskId, listener); + return listener; + } + + private void waitingForCopyTask(int taskId) { + if (mListeners.contains(taskId)) { + mListeners.get(taskId).mWaitingForCopy = true; + } + } + + private void waitingForRemove(int taskId) { + if (mListeners.contains(taskId)) { + mListeners.get(taskId).mWaitingForRemove = true; + } + } + } + protected void removeWindowSynced(int taskId) { final StartingWindowRecord record = mStartingWindowRecords.get(taskId); if (record != null) { @@ -457,6 +520,11 @@ public class StartingSurfaceDrawer { if (DEBUG_SPLASH_SCREEN) { Slog.v(TAG, "Removing splash screen window for task: " + taskId); } + if (record.mContentView != null && record.mContentView.isIconAnimating()) { + // do not remove until the animation is finish + mIconAnimationFinishListener.waitingForRemove(taskId); + return; + } final WindowManager wm = record.mDecorView.getContext() .getSystemService(WindowManager.class); wm.removeView(record.mDecorView); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 176b33dda020e..c815e65189e61 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -52,6 +52,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.sizecompatui.SizeCompatUIController; +import com.android.wm.shell.startingsurface.StartingSurfaceDrawer; import org.junit.Before; import org.junit.Test; @@ -77,6 +78,8 @@ public class ShellTaskOrganizerTests { private Context mContext; @Mock private SizeCompatUIController mSizeCompatUI; + @Mock + private StartingSurfaceDrawer mStartingSurfaceDrawer; ShellTaskOrganizer mOrganizer; private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class); @@ -112,7 +115,7 @@ public class ShellTaskOrganizerTests { .when(mTaskOrganizerController).registerTaskOrganizer(any()); } catch (RemoteException e) {} mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext, - mSizeCompatUI)); + mSizeCompatUI, mStartingSurfaceDrawer)); } @Test