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.SuppressLint;
24 import android.content.Context;
25 import android.content.res.Configuration;
26 import android.graphics.BlendMode;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.Matrix;
30 import android.graphics.Paint;
31 import android.graphics.Point;
32 import android.graphics.RadialGradient;
33 import android.graphics.Rect;
34 import android.graphics.Shader;
35 import android.util.MathUtils;
36 import android.util.Slog;
37 import android.view.Choreographer;
38 import android.view.SurfaceControl;
39 import android.view.SyncRtSurfaceTransactionApplier;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.WindowManager;
43 import android.view.animation.Interpolator;
44 import android.view.animation.PathInterpolator;
45 import android.window.SplashScreenView;
46 
47 import com.android.wm.shell.animation.Interpolators;
48 import com.android.wm.shell.common.TransactionPool;
49 
50 /**
51  * Utilities for creating the splash screen window animations.
52  * @hide
53  */
54 public class SplashScreenExitAnimationUtils {
55     private static final boolean DEBUG_EXIT_ANIMATION = false;
56     private static final boolean DEBUG_EXIT_ANIMATION_BLEND = false;
57     private static final String TAG = "SplashScreenExitAnimationUtils";
58 
59     private static final Interpolator ICON_INTERPOLATOR = new PathInterpolator(0.15f, 0f, 1f, 1f);
60     private static final Interpolator MASK_RADIUS_INTERPOLATOR =
61             new PathInterpolator(0f, 0f, 0.4f, 1f);
62     private static final Interpolator SHIFT_UP_INTERPOLATOR = new PathInterpolator(0f, 0f, 0f, 1f);
63 
64     /**
65      * Creates and starts the animator to fade out the icon, reveal the app, and shift up main
66      * window with rounded corner radius.
67      */
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, float roundedCornerRadius)68     static void startAnimations(ViewGroup splashScreenView,
69             SurfaceControl firstWindowSurface, int mainWindowShiftLength,
70             TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration,
71             int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha,
72             int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener,
73             float roundedCornerRadius) {
74         ValueAnimator animator =
75                 createAnimator(splashScreenView, firstWindowSurface, mainWindowShiftLength,
76                         transactionPool, firstWindowFrame, animationDuration, iconFadeOutDuration,
77                         iconStartAlpha, brandingStartAlpha, appRevealDelay, appRevealDuration,
78                         animatorListener, roundedCornerRadius);
79         animator.start();
80     }
81 
82     /**
83      * Creates and starts the animator to fade out the icon, reveal the app, and shift up main
84      * window.
85      * @hide
86      */
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)87     public static void startAnimations(ViewGroup splashScreenView,
88             SurfaceControl firstWindowSurface, int mainWindowShiftLength,
89             TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration,
90             int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha,
91             int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener) {
92         startAnimations(splashScreenView, firstWindowSurface, mainWindowShiftLength,
93                 transactionPool, firstWindowFrame, animationDuration, iconFadeOutDuration,
94                 iconStartAlpha, brandingStartAlpha, appRevealDelay, appRevealDuration,
95                 animatorListener, 0f /* roundedCornerRadius */);
96     }
97 
98     /**
99      * Creates the animator to fade out the icon, reveal the app, and shift up main window.
100      * @hide
101      */
createAnimator(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)102     private static ValueAnimator createAnimator(ViewGroup splashScreenView,
103             SurfaceControl firstWindowSurface, int mMainWindowShiftLength,
104             TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration,
105             int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha,
106             int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener,
107             float roundedCornerRadius) {
108         // reveal app
109         final float transparentRatio = 0.8f;
110         final int globalHeight = splashScreenView.getHeight();
111         final int verticalCircleCenter = 0;
112         final int finalVerticalLength = globalHeight - verticalCircleCenter;
113         final int halfWidth = splashScreenView.getWidth() / 2;
114         final int endRadius = (int) (0.5 + (1f / transparentRatio * (int)
115                 Math.sqrt(finalVerticalLength * finalVerticalLength + halfWidth * halfWidth)));
116         final int[] colors = {Color.WHITE, Color.WHITE, Color.TRANSPARENT};
117         final float[] stops = {0f, transparentRatio, 1f};
118 
119         RadialVanishAnimation radialVanishAnimation = new RadialVanishAnimation(splashScreenView);
120         radialVanishAnimation.setCircleCenter(halfWidth, verticalCircleCenter);
121         radialVanishAnimation.setRadius(0 /* initRadius */, endRadius);
122         radialVanishAnimation.setRadialPaintParam(colors, stops);
123 
124         View occludeHoleView = null;
125         ShiftUpAnimation shiftUpAnimation = null;
126         if (firstWindowSurface != null && firstWindowSurface.isValid()) {
127             // shift up main window
128             occludeHoleView = new View(splashScreenView.getContext());
129             if (DEBUG_EXIT_ANIMATION_BLEND) {
130                 occludeHoleView.setBackgroundColor(Color.BLUE);
131             } else if (splashScreenView instanceof SplashScreenView) {
132                 occludeHoleView.setBackgroundColor(
133                         ((SplashScreenView) splashScreenView).getInitBackgroundColor());
134             } else {
135                 occludeHoleView.setBackgroundColor(
136                         isDarkTheme(splashScreenView.getContext()) ? Color.BLACK : Color.WHITE);
137             }
138             final ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
139                     WindowManager.LayoutParams.MATCH_PARENT, mMainWindowShiftLength);
140             splashScreenView.addView(occludeHoleView, params);
141 
142             shiftUpAnimation = new ShiftUpAnimation(0, -mMainWindowShiftLength, occludeHoleView,
143                     firstWindowSurface, splashScreenView, transactionPool, firstWindowFrame,
144                     mMainWindowShiftLength, roundedCornerRadius);
145         }
146 
147         ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
148         animator.setDuration(animationDuration);
149         animator.setInterpolator(Interpolators.LINEAR);
150         if (animatorListener != null) {
151             animator.addListener(animatorListener);
152         }
153         View finalOccludeHoleView = occludeHoleView;
154         ShiftUpAnimation finalShiftUpAnimation = shiftUpAnimation;
155         animator.addListener(new AnimatorListenerAdapter() {
156             @Override
157             public void onAnimationEnd(Animator animation) {
158                 super.onAnimationEnd(animation);
159                 if (finalShiftUpAnimation != null) {
160                     finalShiftUpAnimation.finish();
161                 }
162                 splashScreenView.removeView(radialVanishAnimation);
163                 splashScreenView.removeView(finalOccludeHoleView);
164             }
165         });
166         animator.addUpdateListener(animation -> {
167             float linearProgress = (float) animation.getAnimatedValue();
168 
169             // Fade out progress
170             final float iconProgress =
171                     ICON_INTERPOLATOR.getInterpolation(getProgress(
172                             linearProgress, 0 /* delay */, iconFadeOutDuration, animationDuration));
173             View iconView = null;
174             View brandingView = null;
175             if (splashScreenView instanceof SplashScreenView) {
176                 iconView = ((SplashScreenView) splashScreenView).getIconView();
177                 brandingView = ((SplashScreenView) splashScreenView).getBrandingView();
178             }
179             if (iconView != null) {
180                 iconView.setAlpha(iconStartAlpha * (1 - iconProgress));
181             }
182             if (brandingView != null) {
183                 brandingView.setAlpha(brandingStartAlpha * (1 - iconProgress));
184             }
185 
186             final float revealLinearProgress = getProgress(linearProgress, appRevealDelay,
187                     appRevealDuration, animationDuration);
188 
189             radialVanishAnimation.onAnimationProgress(revealLinearProgress);
190 
191             if (finalShiftUpAnimation != null) {
192                 finalShiftUpAnimation.onAnimationProgress(revealLinearProgress);
193             }
194         });
195         return animator;
196     }
197 
getProgress(float linearProgress, long delay, long duration, int animationDuration)198     private static float getProgress(float linearProgress, long delay, long duration,
199                                      int animationDuration) {
200         return MathUtils.constrain(
201                 (linearProgress * (animationDuration) - delay) / duration,
202                 0.0f,
203                 1.0f
204         );
205     }
206 
isDarkTheme(Context context)207     private static boolean isDarkTheme(Context context) {
208         Configuration configuration = context.getResources().getConfiguration();
209         int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
210         return nightMode == Configuration.UI_MODE_NIGHT_YES;
211     }
212 
213     /**
214      * View which creates a circular reveal of the underlying view.
215      * @hide
216      */
217     @SuppressLint("ViewConstructor")
218     public static class RadialVanishAnimation extends View {
219         private final ViewGroup mView;
220         private int mInitRadius;
221         private int mFinishRadius;
222 
223         private final Point mCircleCenter = new Point();
224         private final Matrix mVanishMatrix = new Matrix();
225         private final Paint mVanishPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
226 
RadialVanishAnimation(ViewGroup target)227         public RadialVanishAnimation(ViewGroup target) {
228             super(target.getContext());
229             mView = target;
230             mView.addView(this);
231             if (getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
232                 ((ViewGroup.MarginLayoutParams) getLayoutParams()).setMargins(0, 0, 0, 0);
233             }
234             mVanishPaint.setAlpha(0);
235         }
236 
onAnimationProgress(float linearProgress)237         void onAnimationProgress(float linearProgress) {
238             if (mVanishPaint.getShader() == null) {
239                 return;
240             }
241 
242             final float radiusProgress = MASK_RADIUS_INTERPOLATOR.getInterpolation(linearProgress);
243             final float alphaProgress = Interpolators.ALPHA_OUT.getInterpolation(linearProgress);
244             final float scale = mInitRadius + (mFinishRadius - mInitRadius) * radiusProgress;
245 
246             mVanishMatrix.setScale(scale, scale);
247             mVanishMatrix.postTranslate(mCircleCenter.x, mCircleCenter.y);
248             mVanishPaint.getShader().setLocalMatrix(mVanishMatrix);
249             mVanishPaint.setAlpha(Math.round(0xFF * alphaProgress));
250 
251             postInvalidate();
252         }
253 
setRadius(int initRadius, int finishRadius)254         void setRadius(int initRadius, int finishRadius) {
255             if (DEBUG_EXIT_ANIMATION) {
256                 Slog.v(TAG, "RadialVanishAnimation setRadius init: " + initRadius
257                         + " final " + finishRadius);
258             }
259             mInitRadius = initRadius;
260             mFinishRadius = finishRadius;
261         }
262 
setCircleCenter(int x, int y)263         void setCircleCenter(int x, int y) {
264             if (DEBUG_EXIT_ANIMATION) {
265                 Slog.v(TAG, "RadialVanishAnimation setCircleCenter x: " + x + " y " + y);
266             }
267             mCircleCenter.set(x, y);
268         }
269 
setRadialPaintParam(int[] colors, float[] stops)270         void setRadialPaintParam(int[] colors, float[] stops) {
271             // setup gradient shader
272             final RadialGradient rShader =
273                     new RadialGradient(0, 0, 1, colors, stops, Shader.TileMode.CLAMP);
274             mVanishPaint.setShader(rShader);
275             if (!DEBUG_EXIT_ANIMATION_BLEND) {
276                 // We blend the reveal gradient with the splash screen using DST_OUT so that the
277                 // splash screen is fully visible when radius = 0 (or gradient opacity is 0) and
278                 // fully invisible when radius = finishRadius AND gradient opacity is 1.
279                 mVanishPaint.setBlendMode(BlendMode.DST_OUT);
280             }
281         }
282 
283         @Override
onDraw(Canvas canvas)284         protected void onDraw(Canvas canvas) {
285             super.onDraw(canvas);
286             canvas.drawRect(0, 0, mView.getWidth(), mView.getHeight(), mVanishPaint);
287         }
288     }
289 
290     /**
291      * Shifts up the main window.
292      * @hide
293      */
294     public static final class ShiftUpAnimation {
295         private final float mFromYDelta;
296         private final float mToYDelta;
297         private final View mOccludeHoleView;
298         private final SyncRtSurfaceTransactionApplier mApplier;
299         private final Matrix mTmpTransform = new Matrix();
300         private final SurfaceControl mFirstWindowSurface;
301         private final ViewGroup mSplashScreenView;
302         private final TransactionPool mTransactionPool;
303         private final Rect mFirstWindowFrame;
304         private final int mMainWindowShiftLength;
305 
ShiftUpAnimation(float fromYDelta, float toYDelta, View occludeHoleView, SurfaceControl firstWindowSurface, ViewGroup splashScreenView, TransactionPool transactionPool, Rect firstWindowFrame, int mainWindowShiftLength, float roundedCornerRadius)306         public ShiftUpAnimation(float fromYDelta, float toYDelta, View occludeHoleView,
307                                 SurfaceControl firstWindowSurface, ViewGroup splashScreenView,
308                                 TransactionPool transactionPool, Rect firstWindowFrame,
309                                 int mainWindowShiftLength, float roundedCornerRadius) {
310             mFromYDelta = fromYDelta - roundedCornerRadius;
311             mToYDelta = toYDelta;
312             mOccludeHoleView = occludeHoleView;
313             mApplier = new SyncRtSurfaceTransactionApplier(occludeHoleView);
314             mFirstWindowSurface = firstWindowSurface;
315             mSplashScreenView = splashScreenView;
316             mTransactionPool = transactionPool;
317             mFirstWindowFrame = firstWindowFrame;
318             mMainWindowShiftLength = mainWindowShiftLength;
319         }
320 
onAnimationProgress(float linearProgress)321         void onAnimationProgress(float linearProgress) {
322             if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid()
323                     || !mSplashScreenView.isAttachedToWindow()) {
324                 return;
325             }
326 
327             final float progress = SHIFT_UP_INTERPOLATOR.getInterpolation(linearProgress);
328             final float dy = mFromYDelta + (mToYDelta - mFromYDelta) * progress;
329 
330             mOccludeHoleView.setTranslationY(dy);
331             mTmpTransform.setTranslate(0 /* dx */, dy);
332 
333             // set the vsyncId to ensure the transaction doesn't get applied too early.
334             final SurfaceControl.Transaction tx = mTransactionPool.acquire();
335             tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
336             mTmpTransform.postTranslate(mFirstWindowFrame.left,
337                     mFirstWindowFrame.top + mMainWindowShiftLength);
338 
339             SyncRtSurfaceTransactionApplier.SurfaceParams
340                     params = new SyncRtSurfaceTransactionApplier.SurfaceParams
341                     .Builder(mFirstWindowSurface)
342                     .withMatrix(mTmpTransform)
343                     .withMergeTransaction(tx)
344                     .build();
345             mApplier.scheduleApply(params);
346 
347             mTransactionPool.release(tx);
348         }
349 
finish()350         void finish() {
351             if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid()) {
352                 return;
353             }
354             final SurfaceControl.Transaction tx = mTransactionPool.acquire();
355             if (mSplashScreenView.isAttachedToWindow()) {
356                 tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
357 
358                 SyncRtSurfaceTransactionApplier.SurfaceParams
359                         params = new SyncRtSurfaceTransactionApplier.SurfaceParams
360                         .Builder(mFirstWindowSurface)
361                         .withWindowCrop(null)
362                         .withMergeTransaction(tx)
363                         .build();
364                 mApplier.scheduleApply(params);
365             } else {
366                 tx.setWindowCrop(mFirstWindowSurface, null);
367                 tx.apply();
368             }
369             mTransactionPool.release(tx);
370 
371             Choreographer.getSfInstance().postCallback(CALLBACK_COMMIT,
372                     mFirstWindowSurface::release, null);
373         }
374     }
375 }
376