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