1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.wm.shell.startingsurface;
17 
18 import static android.view.Choreographer.CALLBACK_COMMIT;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.ValueAnimator;
23 import android.annotation.IntDef;
24 import android.annotation.SuppressLint;
25 import android.content.Context;
26 import android.content.res.Configuration;
27 import android.graphics.BlendMode;
28 import android.graphics.Canvas;
29 import android.graphics.Color;
30 import android.graphics.Matrix;
31 import android.graphics.Paint;
32 import android.graphics.Point;
33 import android.graphics.RadialGradient;
34 import android.graphics.Rect;
35 import android.graphics.Shader;
36 import android.util.MathUtils;
37 import android.util.Slog;
38 import android.view.Choreographer;
39 import android.view.SurfaceControl;
40 import android.view.SyncRtSurfaceTransactionApplier;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.view.WindowManager;
44 import android.view.animation.Interpolator;
45 import android.view.animation.PathInterpolator;
46 import android.window.SplashScreenView;
47 
48 import com.android.wm.shell.animation.Interpolators;
49 import com.android.wm.shell.common.TransactionPool;
50 
51 /**
52  * Utilities for creating the splash screen window animations.
53  * @hide
54  */
55 public class SplashScreenExitAnimationUtils {
56     private static final boolean DEBUG_EXIT_ANIMATION = false;
57     private static final boolean DEBUG_EXIT_ANIMATION_BLEND = false;
58     private static final boolean DEBUG_EXIT_FADE_ANIMATION = false;
59     private static final String TAG = "SplashScreenExitAnimationUtils";
60 
61     private static final Interpolator ICON_INTERPOLATOR = new PathInterpolator(0.15f, 0f, 1f, 1f);
62     private static final Interpolator MASK_RADIUS_INTERPOLATOR =
63             new PathInterpolator(0f, 0f, 0.4f, 1f);
64     private static final Interpolator SHIFT_UP_INTERPOLATOR = new PathInterpolator(0f, 0f, 0f, 1f);
65 
66     /**
67      * This splash screen exit animation type uses a radial vanish to hide
68      * the starting window and slides up the main window content.
69      * @hide
70      */
71     public static final int TYPE_RADIAL_VANISH_SLIDE_UP = 0;
72 
73     /**
74      * This splash screen exit animation type fades out the starting window
75      * to reveal the main window content.
76      * @hide
77      */
78     public static final int TYPE_FADE_OUT = 1;
79 
80     /** @hide */
81     @IntDef(prefix = { "TYPE_" }, value = {
82             TYPE_RADIAL_VANISH_SLIDE_UP,
83             TYPE_FADE_OUT,
84     })
85     public @interface ExitAnimationType {}
86 
87     /**
88      * Creates and starts the animator to fade out the icon, reveal the app, and shift up main
89      * window with rounded corner radius.
90      */
startAnimations(@xitAnimationType int animationType, ViewGroup splashScreenView, SurfaceControl firstWindowSurface, int mainWindowShiftLength, TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration, int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha, int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener, float roundedCornerRadius)91     static void startAnimations(@ExitAnimationType int animationType,
92             ViewGroup splashScreenView, SurfaceControl firstWindowSurface,
93             int mainWindowShiftLength, TransactionPool transactionPool, Rect firstWindowFrame,
94             int animationDuration, int iconFadeOutDuration, float iconStartAlpha,
95             float brandingStartAlpha, int appRevealDelay, int appRevealDuration,
96             Animator.AnimatorListener animatorListener, float roundedCornerRadius) {
97         ValueAnimator animator;
98         if (animationType == TYPE_FADE_OUT) {
99             animator = createFadeOutAnimation(splashScreenView, animationDuration,
100                     iconFadeOutDuration, iconStartAlpha, brandingStartAlpha, appRevealDelay,
101                     appRevealDuration, animatorListener);
102         } else {
103             animator = createRadialVanishSlideUpAnimator(splashScreenView,
104                     firstWindowSurface, mainWindowShiftLength, transactionPool, firstWindowFrame,
105                     animationDuration, iconFadeOutDuration, iconStartAlpha, brandingStartAlpha,
106                     appRevealDelay, appRevealDuration, animatorListener, roundedCornerRadius);
107         }
108         animator.start();
109     }
110 
111     /**
112      * Creates and starts the animator to fade out the icon, reveal the app, and shift up main
113      * window.
114      * @hide
115      */
startAnimations(ViewGroup splashScreenView, SurfaceControl firstWindowSurface, int mainWindowShiftLength, TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration, int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha, int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener)116     public static void startAnimations(ViewGroup splashScreenView,
117             SurfaceControl firstWindowSurface, int mainWindowShiftLength,
118             TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration,
119             int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha,
120             int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener) {
121         // Start the default 'reveal' animation.
122         startAnimations(TYPE_RADIAL_VANISH_SLIDE_UP, splashScreenView,
123                 firstWindowSurface, mainWindowShiftLength, transactionPool, firstWindowFrame,
124                 animationDuration, iconFadeOutDuration, iconStartAlpha, brandingStartAlpha,
125                 appRevealDelay, appRevealDuration, animatorListener, 0f /* roundedCornerRadius */);
126     }
127 
128     /**
129      * Creates the animator to fade out the icon, reveal the app, and shift up main window.
130      * @hide
131      */
createRadialVanishSlideUpAnimator(ViewGroup splashScreenView, SurfaceControl firstWindowSurface, int mMainWindowShiftLength, TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration, int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha, int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener, float roundedCornerRadius)132     private static ValueAnimator createRadialVanishSlideUpAnimator(ViewGroup splashScreenView,
133             SurfaceControl firstWindowSurface, int mMainWindowShiftLength,
134             TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration,
135             int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha,
136             int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener,
137             float roundedCornerRadius) {
138         // reveal app
139         final float transparentRatio = 0.8f;
140         final int globalHeight = splashScreenView.getHeight();
141         final int verticalCircleCenter = 0;
142         final int finalVerticalLength = globalHeight - verticalCircleCenter;
143         final int halfWidth = splashScreenView.getWidth() / 2;
144         final int endRadius = (int) (0.5 + (1f / transparentRatio * (int)
145                 Math.sqrt(finalVerticalLength * finalVerticalLength + halfWidth * halfWidth)));
146         final int[] colors = {Color.WHITE, Color.WHITE, Color.TRANSPARENT};
147         final float[] stops = {0f, transparentRatio, 1f};
148 
149         RadialVanishAnimation radialVanishAnimation = new RadialVanishAnimation(splashScreenView);
150         radialVanishAnimation.setCircleCenter(halfWidth, verticalCircleCenter);
151         radialVanishAnimation.setRadius(0 /* initRadius */, endRadius);
152         radialVanishAnimation.setRadialPaintParam(colors, stops);
153 
154         View occludeHoleView = null;
155         ShiftUpAnimation shiftUpAnimation = null;
156         if (firstWindowSurface != null && firstWindowSurface.isValid()) {
157             // shift up main window
158             occludeHoleView = new View(splashScreenView.getContext());
159             if (DEBUG_EXIT_ANIMATION_BLEND) {
160                 occludeHoleView.setBackgroundColor(Color.BLUE);
161             } else if (splashScreenView instanceof SplashScreenView) {
162                 occludeHoleView.setBackgroundColor(
163                         ((SplashScreenView) splashScreenView).getInitBackgroundColor());
164             } else {
165                 occludeHoleView.setBackgroundColor(
166                         isDarkTheme(splashScreenView.getContext()) ? Color.BLACK : Color.WHITE);
167             }
168             final ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
169                     WindowManager.LayoutParams.MATCH_PARENT, mMainWindowShiftLength);
170             splashScreenView.addView(occludeHoleView, params);
171 
172             shiftUpAnimation = new ShiftUpAnimation(0, -mMainWindowShiftLength, occludeHoleView,
173                     firstWindowSurface, splashScreenView, transactionPool, firstWindowFrame,
174                     mMainWindowShiftLength, roundedCornerRadius);
175         }
176 
177         ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
178         animator.setDuration(animationDuration);
179         animator.setInterpolator(Interpolators.LINEAR);
180         if (animatorListener != null) {
181             animator.addListener(animatorListener);
182         }
183         View finalOccludeHoleView = occludeHoleView;
184         ShiftUpAnimation finalShiftUpAnimation = shiftUpAnimation;
185         animator.addListener(new AnimatorListenerAdapter() {
186             @Override
187             public void onAnimationEnd(Animator animation) {
188                 super.onAnimationEnd(animation);
189                 if (finalShiftUpAnimation != null) {
190                     finalShiftUpAnimation.finish();
191                 }
192                 splashScreenView.removeView(radialVanishAnimation);
193                 splashScreenView.removeView(finalOccludeHoleView);
194             }
195         });
196         animator.addUpdateListener(animation -> {
197             float linearProgress = (float) animation.getAnimatedValue();
198 
199             // Fade out progress
200             final float iconProgress =
201                     ICON_INTERPOLATOR.getInterpolation(getProgress(
202                             linearProgress, 0 /* delay */, iconFadeOutDuration, animationDuration));
203             View iconView = null;
204             View brandingView = null;
205             if (splashScreenView instanceof SplashScreenView) {
206                 iconView = ((SplashScreenView) splashScreenView).getIconView();
207                 brandingView = ((SplashScreenView) splashScreenView).getBrandingView();
208             }
209             if (iconView != null) {
210                 iconView.setAlpha(iconStartAlpha * (1 - iconProgress));
211             }
212             if (brandingView != null) {
213                 brandingView.setAlpha(brandingStartAlpha * (1 - iconProgress));
214             }
215 
216             final float revealLinearProgress = getProgress(linearProgress, appRevealDelay,
217                     appRevealDuration, animationDuration);
218 
219             radialVanishAnimation.onAnimationProgress(revealLinearProgress);
220 
221             if (finalShiftUpAnimation != null) {
222                 finalShiftUpAnimation.onAnimationProgress(revealLinearProgress);
223             }
224         });
225         return animator;
226     }
227 
getProgress(float linearProgress, long delay, long duration, int animationDuration)228     private static float getProgress(float linearProgress, long delay, long duration,
229                                      int animationDuration) {
230         return MathUtils.constrain(
231                 (linearProgress * (animationDuration) - delay) / duration,
232                 0.0f,
233                 1.0f
234         );
235     }
236 
isDarkTheme(Context context)237     private static boolean isDarkTheme(Context context) {
238         Configuration configuration = context.getResources().getConfiguration();
239         int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
240         return nightMode == Configuration.UI_MODE_NIGHT_YES;
241     }
242 
createFadeOutAnimation(ViewGroup splashScreenView, int animationDuration, int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha, int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener)243     private static ValueAnimator createFadeOutAnimation(ViewGroup splashScreenView,
244             int animationDuration, int iconFadeOutDuration, float iconStartAlpha,
245             float brandingStartAlpha, int appRevealDelay, int appRevealDuration,
246             Animator.AnimatorListener animatorListener) {
247 
248         if (DEBUG_EXIT_FADE_ANIMATION) {
249             splashScreenView.setBackgroundColor(Color.BLUE);
250         }
251 
252         final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
253         animator.setDuration(animationDuration);
254         animator.setInterpolator(Interpolators.LINEAR);
255         animator.addUpdateListener(animation -> {
256 
257             float linearProgress = (float) animation.getAnimatedValue();
258 
259             // Icon fade out progress (always starts immediately)
260             final float iconFadeProgress = ICON_INTERPOLATOR.getInterpolation(getProgress(
261                             linearProgress, 0 /* delay */, iconFadeOutDuration, animationDuration));
262             View iconView = null;
263             View brandingView = null;
264 
265             if (splashScreenView instanceof SplashScreenView) {
266                 iconView = ((SplashScreenView) splashScreenView).getIconView();
267                 brandingView = ((SplashScreenView) splashScreenView).getBrandingView();
268             }
269             if (iconView != null) {
270                 iconView.setAlpha(iconStartAlpha * (1f - iconFadeProgress));
271             }
272             if (brandingView != null) {
273                 brandingView.setAlpha(brandingStartAlpha * (1f - iconFadeProgress));
274             }
275 
276             // Splash screen fade out progress (possibly delayed)
277             final float splashFadeProgress = Interpolators.ALPHA_OUT.getInterpolation(
278                     getProgress(linearProgress, appRevealDelay,
279                     appRevealDuration, animationDuration));
280 
281             splashScreenView.setAlpha(1f - splashFadeProgress);
282 
283             if (DEBUG_EXIT_FADE_ANIMATION) {
284                 Slog.d(TAG, "progress -> animation: " + linearProgress
285                         + "\t icon alpha: " + ((iconView != null) ? iconView.getAlpha() : "n/a")
286                         + "\t splash alpha: " + splashScreenView.getAlpha()
287                 );
288             }
289         });
290         if (animatorListener != null) {
291             animator.addListener(animatorListener);
292         }
293         return animator;
294     }
295 
296     /**
297      * View which creates a circular reveal of the underlying view.
298      * @hide
299      */
300     @SuppressLint("ViewConstructor")
301     public static class RadialVanishAnimation extends View {
302         private final ViewGroup mView;
303         private int mInitRadius;
304         private int mFinishRadius;
305 
306         private final Point mCircleCenter = new Point();
307         private final Matrix mVanishMatrix = new Matrix();
308         private final Paint mVanishPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
309 
RadialVanishAnimation(ViewGroup target)310         public RadialVanishAnimation(ViewGroup target) {
311             super(target.getContext());
312             mView = target;
313             mView.addView(this);
314             if (getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
315                 ((ViewGroup.MarginLayoutParams) getLayoutParams()).setMargins(0, 0, 0, 0);
316             }
317             mVanishPaint.setAlpha(0);
318         }
319 
onAnimationProgress(float linearProgress)320         void onAnimationProgress(float linearProgress) {
321             if (mVanishPaint.getShader() == null) {
322                 return;
323             }
324 
325             final float radiusProgress = MASK_RADIUS_INTERPOLATOR.getInterpolation(linearProgress);
326             final float alphaProgress = Interpolators.ALPHA_OUT.getInterpolation(linearProgress);
327             final float scale = mInitRadius + (mFinishRadius - mInitRadius) * radiusProgress;
328 
329             mVanishMatrix.setScale(scale, scale);
330             mVanishMatrix.postTranslate(mCircleCenter.x, mCircleCenter.y);
331             mVanishPaint.getShader().setLocalMatrix(mVanishMatrix);
332             mVanishPaint.setAlpha(Math.round(0xFF * alphaProgress));
333 
334             postInvalidate();
335         }
336 
setRadius(int initRadius, int finishRadius)337         void setRadius(int initRadius, int finishRadius) {
338             if (DEBUG_EXIT_ANIMATION) {
339                 Slog.v(TAG, "RadialVanishAnimation setRadius init: " + initRadius
340                         + " final " + finishRadius);
341             }
342             mInitRadius = initRadius;
343             mFinishRadius = finishRadius;
344         }
345 
setCircleCenter(int x, int y)346         void setCircleCenter(int x, int y) {
347             if (DEBUG_EXIT_ANIMATION) {
348                 Slog.v(TAG, "RadialVanishAnimation setCircleCenter x: " + x + " y " + y);
349             }
350             mCircleCenter.set(x, y);
351         }
352 
setRadialPaintParam(int[] colors, float[] stops)353         void setRadialPaintParam(int[] colors, float[] stops) {
354             // setup gradient shader
355             final RadialGradient rShader =
356                     new RadialGradient(0, 0, 1, colors, stops, Shader.TileMode.CLAMP);
357             mVanishPaint.setShader(rShader);
358             if (!DEBUG_EXIT_ANIMATION_BLEND) {
359                 // We blend the reveal gradient with the splash screen using DST_OUT so that the
360                 // splash screen is fully visible when radius = 0 (or gradient opacity is 0) and
361                 // fully invisible when radius = finishRadius AND gradient opacity is 1.
362                 mVanishPaint.setBlendMode(BlendMode.DST_OUT);
363             }
364         }
365 
366         @Override
onDraw(Canvas canvas)367         protected void onDraw(Canvas canvas) {
368             super.onDraw(canvas);
369             canvas.drawRect(0, 0, mView.getWidth(), mView.getHeight(), mVanishPaint);
370         }
371     }
372 
373     /**
374      * Shifts up the main window.
375      * @hide
376      */
377     public static final class ShiftUpAnimation {
378         private final float mFromYDelta;
379         private final float mToYDelta;
380         private final View mOccludeHoleView;
381         private final SyncRtSurfaceTransactionApplier mApplier;
382         private final Matrix mTmpTransform = new Matrix();
383         private final SurfaceControl mFirstWindowSurface;
384         private final ViewGroup mSplashScreenView;
385         private final TransactionPool mTransactionPool;
386         private final Rect mFirstWindowFrame;
387         private final int mMainWindowShiftLength;
388 
ShiftUpAnimation(float fromYDelta, float toYDelta, View occludeHoleView, SurfaceControl firstWindowSurface, ViewGroup splashScreenView, TransactionPool transactionPool, Rect firstWindowFrame, int mainWindowShiftLength, float roundedCornerRadius)389         public ShiftUpAnimation(float fromYDelta, float toYDelta, View occludeHoleView,
390                                 SurfaceControl firstWindowSurface, ViewGroup splashScreenView,
391                                 TransactionPool transactionPool, Rect firstWindowFrame,
392                                 int mainWindowShiftLength, float roundedCornerRadius) {
393             mFromYDelta = fromYDelta - Math.max(firstWindowFrame.top, roundedCornerRadius);
394             mToYDelta = toYDelta;
395             mOccludeHoleView = occludeHoleView;
396             mApplier = new SyncRtSurfaceTransactionApplier(occludeHoleView);
397             mFirstWindowSurface = firstWindowSurface;
398             mSplashScreenView = splashScreenView;
399             mTransactionPool = transactionPool;
400             mFirstWindowFrame = firstWindowFrame;
401             mMainWindowShiftLength = mainWindowShiftLength;
402         }
403 
onAnimationProgress(float linearProgress)404         void onAnimationProgress(float linearProgress) {
405             if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid()
406                     || !mSplashScreenView.isAttachedToWindow()) {
407                 return;
408             }
409 
410             final float progress = SHIFT_UP_INTERPOLATOR.getInterpolation(linearProgress);
411             final float dy = mFromYDelta + (mToYDelta - mFromYDelta) * progress;
412 
413             mOccludeHoleView.setTranslationY(dy);
414             mTmpTransform.setTranslate(0 /* dx */, dy);
415 
416             // set the vsyncId to ensure the transaction doesn't get applied too early.
417             final SurfaceControl.Transaction tx = mTransactionPool.acquire();
418             tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
419             mTmpTransform.postTranslate(mFirstWindowFrame.left,
420                     mFirstWindowFrame.top + mMainWindowShiftLength);
421 
422             SyncRtSurfaceTransactionApplier.SurfaceParams
423                     params = new SyncRtSurfaceTransactionApplier.SurfaceParams
424                     .Builder(mFirstWindowSurface)
425                     .withMatrix(mTmpTransform)
426                     .withMergeTransaction(tx)
427                     .build();
428             mApplier.scheduleApply(params);
429 
430             mTransactionPool.release(tx);
431         }
432 
finish()433         void finish() {
434             if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid()) {
435                 return;
436             }
437             final SurfaceControl.Transaction tx = mTransactionPool.acquire();
438             if (mSplashScreenView.isAttachedToWindow()) {
439                 tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
440 
441                 SyncRtSurfaceTransactionApplier.SurfaceParams
442                         params = new SyncRtSurfaceTransactionApplier.SurfaceParams
443                         .Builder(mFirstWindowSurface)
444                         .withWindowCrop(null)
445                         .withMergeTransaction(tx)
446                         .build();
447                 mApplier.scheduleApply(params);
448             } else {
449                 tx.setWindowCrop(mFirstWindowSurface, null);
450                 tx.apply();
451             }
452             mTransactionPool.release(tx);
453 
454             Choreographer.getSfInstance().postCallback(CALLBACK_COMMIT,
455                     mFirstWindowSurface::release, null);
456         }
457     }
458 }
459