/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.startingsurface; import static android.view.Choreographer.CALLBACK_COMMIT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Configuration; import android.graphics.BlendMode; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.Shader; import android.util.MathUtils; import android.util.Slog; import android.view.Choreographer; import android.view.SurfaceControl; import android.view.SyncRtSurfaceTransactionApplier; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.window.SplashScreenView; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.TransactionPool; /** * Utilities for creating the splash screen window animations. * @hide */ public class SplashScreenExitAnimationUtils { private static final boolean DEBUG_EXIT_ANIMATION = false; private static final boolean DEBUG_EXIT_ANIMATION_BLEND = false; private static final String TAG = "SplashScreenExitAnimationUtils"; private static final Interpolator ICON_INTERPOLATOR = new PathInterpolator(0.15f, 0f, 1f, 1f); private static final Interpolator MASK_RADIUS_INTERPOLATOR = new PathInterpolator(0f, 0f, 0.4f, 1f); private static final Interpolator SHIFT_UP_INTERPOLATOR = new PathInterpolator(0f, 0f, 0f, 1f); /** * Creates and starts the animator to fade out the icon, reveal the app, and shift up main * window with rounded corner radius. */ static void 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) { ValueAnimator animator = createAnimator(splashScreenView, firstWindowSurface, mainWindowShiftLength, transactionPool, firstWindowFrame, animationDuration, iconFadeOutDuration, iconStartAlpha, brandingStartAlpha, appRevealDelay, appRevealDuration, animatorListener, roundedCornerRadius); animator.start(); } /** * Creates and starts the animator to fade out the icon, reveal the app, and shift up main * window. * @hide */ public static void 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) { startAnimations(splashScreenView, firstWindowSurface, mainWindowShiftLength, transactionPool, firstWindowFrame, animationDuration, iconFadeOutDuration, iconStartAlpha, brandingStartAlpha, appRevealDelay, appRevealDuration, animatorListener, 0f /* roundedCornerRadius */); } /** * Creates the animator to fade out the icon, reveal the app, and shift up main window. * @hide */ private static ValueAnimator 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) { // reveal app final float transparentRatio = 0.8f; final int globalHeight = splashScreenView.getHeight(); final int verticalCircleCenter = 0; final int finalVerticalLength = globalHeight - verticalCircleCenter; final int halfWidth = splashScreenView.getWidth() / 2; final int endRadius = (int) (0.5 + (1f / transparentRatio * (int) Math.sqrt(finalVerticalLength * finalVerticalLength + halfWidth * halfWidth))); final int[] colors = {Color.WHITE, Color.WHITE, Color.TRANSPARENT}; final float[] stops = {0f, transparentRatio, 1f}; RadialVanishAnimation radialVanishAnimation = new RadialVanishAnimation(splashScreenView); radialVanishAnimation.setCircleCenter(halfWidth, verticalCircleCenter); radialVanishAnimation.setRadius(0 /* initRadius */, endRadius); radialVanishAnimation.setRadialPaintParam(colors, stops); View occludeHoleView = null; ShiftUpAnimation shiftUpAnimation = null; if (firstWindowSurface != null && firstWindowSurface.isValid()) { // shift up main window occludeHoleView = new View(splashScreenView.getContext()); if (DEBUG_EXIT_ANIMATION_BLEND) { occludeHoleView.setBackgroundColor(Color.BLUE); } else if (splashScreenView instanceof SplashScreenView) { occludeHoleView.setBackgroundColor( ((SplashScreenView) splashScreenView).getInitBackgroundColor()); } else { occludeHoleView.setBackgroundColor( isDarkTheme(splashScreenView.getContext()) ? Color.BLACK : Color.WHITE); } final ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( WindowManager.LayoutParams.MATCH_PARENT, mMainWindowShiftLength); splashScreenView.addView(occludeHoleView, params); shiftUpAnimation = new ShiftUpAnimation(0, -mMainWindowShiftLength, occludeHoleView, firstWindowSurface, splashScreenView, transactionPool, firstWindowFrame, mMainWindowShiftLength, roundedCornerRadius); } ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(animationDuration); animator.setInterpolator(Interpolators.LINEAR); if (animatorListener != null) { animator.addListener(animatorListener); } View finalOccludeHoleView = occludeHoleView; ShiftUpAnimation finalShiftUpAnimation = shiftUpAnimation; animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (finalShiftUpAnimation != null) { finalShiftUpAnimation.finish(); } splashScreenView.removeView(radialVanishAnimation); splashScreenView.removeView(finalOccludeHoleView); } }); animator.addUpdateListener(animation -> { float linearProgress = (float) animation.getAnimatedValue(); // Fade out progress final float iconProgress = ICON_INTERPOLATOR.getInterpolation(getProgress( linearProgress, 0 /* delay */, iconFadeOutDuration, animationDuration)); View iconView = null; View brandingView = null; if (splashScreenView instanceof SplashScreenView) { iconView = ((SplashScreenView) splashScreenView).getIconView(); brandingView = ((SplashScreenView) splashScreenView).getBrandingView(); } if (iconView != null) { iconView.setAlpha(iconStartAlpha * (1 - iconProgress)); } if (brandingView != null) { brandingView.setAlpha(brandingStartAlpha * (1 - iconProgress)); } final float revealLinearProgress = getProgress(linearProgress, appRevealDelay, appRevealDuration, animationDuration); radialVanishAnimation.onAnimationProgress(revealLinearProgress); if (finalShiftUpAnimation != null) { finalShiftUpAnimation.onAnimationProgress(revealLinearProgress); } }); return animator; } private static float getProgress(float linearProgress, long delay, long duration, int animationDuration) { return MathUtils.constrain( (linearProgress * (animationDuration) - delay) / duration, 0.0f, 1.0f ); } private static boolean isDarkTheme(Context context) { Configuration configuration = context.getResources().getConfiguration(); int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK; return nightMode == Configuration.UI_MODE_NIGHT_YES; } /** * View which creates a circular reveal of the underlying view. * @hide */ @SuppressLint("ViewConstructor") public static class RadialVanishAnimation extends View { private final ViewGroup mView; private int mInitRadius; private int mFinishRadius; private final Point mCircleCenter = new Point(); private final Matrix mVanishMatrix = new Matrix(); private final Paint mVanishPaint = new Paint(Paint.ANTI_ALIAS_FLAG); public RadialVanishAnimation(ViewGroup target) { super(target.getContext()); mView = target; mView.addView(this); if (getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { ((ViewGroup.MarginLayoutParams) getLayoutParams()).setMargins(0, 0, 0, 0); } mVanishPaint.setAlpha(0); } void onAnimationProgress(float linearProgress) { if (mVanishPaint.getShader() == null) { return; } final float radiusProgress = MASK_RADIUS_INTERPOLATOR.getInterpolation(linearProgress); final float alphaProgress = Interpolators.ALPHA_OUT.getInterpolation(linearProgress); final float scale = mInitRadius + (mFinishRadius - mInitRadius) * radiusProgress; mVanishMatrix.setScale(scale, scale); mVanishMatrix.postTranslate(mCircleCenter.x, mCircleCenter.y); mVanishPaint.getShader().setLocalMatrix(mVanishMatrix); mVanishPaint.setAlpha(Math.round(0xFF * alphaProgress)); postInvalidate(); } void setRadius(int initRadius, int finishRadius) { if (DEBUG_EXIT_ANIMATION) { Slog.v(TAG, "RadialVanishAnimation setRadius init: " + initRadius + " final " + finishRadius); } mInitRadius = initRadius; mFinishRadius = finishRadius; } void setCircleCenter(int x, int y) { if (DEBUG_EXIT_ANIMATION) { Slog.v(TAG, "RadialVanishAnimation setCircleCenter x: " + x + " y " + y); } mCircleCenter.set(x, y); } void setRadialPaintParam(int[] colors, float[] stops) { // setup gradient shader final RadialGradient rShader = new RadialGradient(0, 0, 1, colors, stops, Shader.TileMode.CLAMP); mVanishPaint.setShader(rShader); if (!DEBUG_EXIT_ANIMATION_BLEND) { // We blend the reveal gradient with the splash screen using DST_OUT so that the // splash screen is fully visible when radius = 0 (or gradient opacity is 0) and // fully invisible when radius = finishRadius AND gradient opacity is 1. mVanishPaint.setBlendMode(BlendMode.DST_OUT); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawRect(0, 0, mView.getWidth(), mView.getHeight(), mVanishPaint); } } /** * Shifts up the main window. * @hide */ public static final class ShiftUpAnimation { private final float mFromYDelta; private final float mToYDelta; private final View mOccludeHoleView; private final SyncRtSurfaceTransactionApplier mApplier; private final Matrix mTmpTransform = new Matrix(); private final SurfaceControl mFirstWindowSurface; private final ViewGroup mSplashScreenView; private final TransactionPool mTransactionPool; private final Rect mFirstWindowFrame; private final int mMainWindowShiftLength; public ShiftUpAnimation(float fromYDelta, float toYDelta, View occludeHoleView, SurfaceControl firstWindowSurface, ViewGroup splashScreenView, TransactionPool transactionPool, Rect firstWindowFrame, int mainWindowShiftLength, float roundedCornerRadius) { mFromYDelta = fromYDelta - roundedCornerRadius; mToYDelta = toYDelta; mOccludeHoleView = occludeHoleView; mApplier = new SyncRtSurfaceTransactionApplier(occludeHoleView); mFirstWindowSurface = firstWindowSurface; mSplashScreenView = splashScreenView; mTransactionPool = transactionPool; mFirstWindowFrame = firstWindowFrame; mMainWindowShiftLength = mainWindowShiftLength; } void onAnimationProgress(float linearProgress) { if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid() || !mSplashScreenView.isAttachedToWindow()) { return; } final float progress = SHIFT_UP_INTERPOLATOR.getInterpolation(linearProgress); final float dy = mFromYDelta + (mToYDelta - mFromYDelta) * progress; mOccludeHoleView.setTranslationY(dy); mTmpTransform.setTranslate(0 /* dx */, dy); // set the vsyncId to ensure the transaction doesn't get applied too early. final SurfaceControl.Transaction tx = mTransactionPool.acquire(); tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); mTmpTransform.postTranslate(mFirstWindowFrame.left, mFirstWindowFrame.top + mMainWindowShiftLength); SyncRtSurfaceTransactionApplier.SurfaceParams params = new SyncRtSurfaceTransactionApplier.SurfaceParams .Builder(mFirstWindowSurface) .withMatrix(mTmpTransform) .withMergeTransaction(tx) .build(); mApplier.scheduleApply(params); mTransactionPool.release(tx); } void finish() { if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid()) { return; } final SurfaceControl.Transaction tx = mTransactionPool.acquire(); if (mSplashScreenView.isAttachedToWindow()) { tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); SyncRtSurfaceTransactionApplier.SurfaceParams params = new SyncRtSurfaceTransactionApplier.SurfaceParams .Builder(mFirstWindowSurface) .withWindowCrop(null) .withMergeTransaction(tx) .build(); mApplier.scheduleApply(params); } else { tx.setWindowCrop(mFirstWindowSurface, null); tx.apply(); } mTransactionPool.release(tx); Choreographer.getSfInstance().postCallback(CALLBACK_COMMIT, mFirstWindowSurface::release, null); } } }