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