From c0a85b7c3b1eae482a25cd879a538728e13bd20b Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Tue, 24 Aug 2021 16:29:52 +0200 Subject: [PATCH 1/3] Split ActivityLaunchAnimator (1/2) This CL splits ActivityLaunchAnimator by extracting common logic into LaunchAnimator. The goal of this CL is to be able to reuse the LaunchAnimator logic for dialog launches. This CL also improves GhostedViewLaunchAnimator such that it also works with: * launch containers that are not located at (0, 0). * ghosted views that move. Test: atest ActivityLaunchAnimatorTest Bug: 193634619 Change-Id: Ic05603600d4a7d4e27182c79bef54286087d00d0 --- core/java/android/util/MathUtils.java | 4 + .../animation/ActivityLaunchAnimator.kt | 383 ++++-------------- .../DelegateLaunchAnimatorController.kt | 16 + .../GhostedViewLaunchAnimatorController.kt | 94 ++++- .../systemui/animation/LaunchAnimator.kt | 355 ++++++++++++++++ .../dagger/StatusBarDependenciesModule.java | 18 + .../notification/ExpandAnimationParameters.kt | 6 +- .../NotificationLaunchAnimatorController.kt | 21 +- .../NotificationPanelViewController.java | 6 +- .../systemui/statusbar/phone/StatusBar.java | 8 +- .../StatusBarLaunchAnimatorController.kt | 5 +- .../phone/dagger/StatusBarPhoneModule.java | 7 +- .../animation/ActivityLaunchAnimatorTest.kt | 11 +- ...GhostedViewLaunchAnimatorControllerTest.kt | 2 +- .../statusbar/phone/StatusBarTest.java | 5 +- 15 files changed, 589 insertions(+), 352 deletions(-) create mode 100644 packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt diff --git a/core/java/android/util/MathUtils.java b/core/java/android/util/MathUtils.java index 971e16185815b..aecde4415117f 100644 --- a/core/java/android/util/MathUtils.java +++ b/core/java/android/util/MathUtils.java @@ -165,6 +165,10 @@ public final class MathUtils { return start + (stop - start) * amount; } + public static float lerp(int start, int stop, float amount) { + return lerp((float) start, (float) stop, amount); + } + /** * Returns the interpolation scalar (s) that satisfies the equation: {@code value = }{@link * #lerp}{@code (a, b, s)} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index 9c1e12923b43b..7020603383591 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -1,23 +1,31 @@ +/* + * Copyright (C) 2021 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.systemui.animation -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.ValueAnimator import android.app.ActivityManager import android.app.ActivityTaskManager import android.app.PendingIntent import android.app.TaskInfo -import android.content.Context import android.graphics.Matrix -import android.graphics.PorterDuff -import android.graphics.PorterDuffXfermode import android.graphics.Rect import android.graphics.RectF -import android.graphics.drawable.GradientDrawable import android.os.Looper import android.os.RemoteException import android.util.Log -import android.util.MathUtils import android.view.IRemoteAnimationFinishedCallback import android.view.IRemoteAnimationRunner import android.view.RemoteAnimationAdapter @@ -26,7 +34,6 @@ import android.view.SyncRtSurfaceTransactionApplier import android.view.View import android.view.ViewGroup import android.view.WindowManager -import android.view.animation.AnimationUtils import android.view.animation.PathInterpolator import com.android.internal.annotations.VisibleForTesting import com.android.internal.policy.ScreenDecorationsUtils @@ -38,52 +45,23 @@ private const val TAG = "ActivityLaunchAnimator" * A class that allows activities to be started in a seamless way from a view that is transforming * nicely into the starting window. */ -class ActivityLaunchAnimator( - private val callback: Callback, - context: Context -) { +class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) { companion object { - private const val DEBUG = false - const val ANIMATION_DURATION = 500L - private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L - private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L - private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT private const val ANIMATION_DURATION_NAV_FADE_IN = 266L private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L private const val ANIMATION_DELAY_NAV_FADE_IN = - ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN + LaunchAnimator.ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN private const val LAUNCH_TIMEOUT = 1000L - @JvmField val CONTENT_FADE_OUT_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f) - private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f) private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f) private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f) - - private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC) - - /** - * Given the [linearProgress] of a launch animation, return the linear progress of the - * sub-animation starting [delay] ms after the launch animation and that lasts [duration]. - */ - @JvmStatic - fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float { - return MathUtils.constrain( - (linearProgress * ANIMATION_DURATION - delay) / duration, - 0.0f, - 1.0f - ) - } } - /** The interpolator used for the width, height, Y position and corner radius. */ - private val animationInterpolator = AnimationUtils.loadInterpolator(context, - R.interpolator.launch_animation_interpolator_y) - - /** The interpolator used for the X position. */ - private val animationInterpolatorX = AnimationUtils.loadInterpolator(context, - R.interpolator.launch_animation_interpolator_x) - - private val cornerRadii = FloatArray(8) + /** + * The callback of this animator. This should be set before any call to + * [start(Pending)IntentWithAnimation]. + */ + var callback: Callback? = null /** * Start an intent and animate the opening window. The intent will be started by running @@ -119,6 +97,8 @@ class ActivityLaunchAnimator( return } + val callback = this.callback ?: throw IllegalStateException( + "ActivityLaunchAnimator.callback must be set before using this animator") val runner = Runner(controller) val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen @@ -126,9 +106,9 @@ class ActivityLaunchAnimator( // keyguard with the animation val animationAdapter = if (!hideKeyguardWithAnimation) { RemoteAnimationAdapter( - runner, - ANIMATION_DURATION, - ANIMATION_DURATION - 150 /* statusBarTransitionDelay */ + runner, + LaunchAnimator.ANIMATION_DURATION, + LaunchAnimator.ANIMATION_DURATION - 150 /* statusBarTransitionDelay */ ) } else { null @@ -150,10 +130,10 @@ class ActivityLaunchAnimator( // Only animate if the app is not already on top and will be opened, unless we are on the // keyguard. val willAnimate = - launchResult == ActivityManager.START_TASK_TO_FRONT || - launchResult == ActivityManager.START_SUCCESS || - (launchResult == ActivityManager.START_DELIVERED_TO_TOP && - hideKeyguardWithAnimation) + launchResult == ActivityManager.START_TASK_TO_FRONT || + launchResult == ActivityManager.START_SUCCESS || + (launchResult == ActivityManager.START_DELIVERED_TO_TOP && + hideKeyguardWithAnimation) Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " + "hideKeyguardWithAnimation=$hideKeyguardWithAnimation") @@ -234,7 +214,7 @@ class ActivityLaunchAnimator( * * Note that all callbacks (onXXX methods) are all called on the main thread. */ - interface Controller { + interface Controller : LaunchAnimator.Controller { companion object { /** * Return a [Controller] that will animate and expand [view] into the opening window. @@ -258,53 +238,12 @@ class ActivityLaunchAnimator( } } - /** - * The container in which the view that started the intent will be animating together with - * the opening window. - * - * This will be used to: - * - Get the associated [Context]. - * - Compute whether we are expanding fully above the current window. - * - Apply surface transactions in sync with RenderThread. - * - * This container can be changed to force this [Controller] to animate the expanding view - * inside a different location, for instance to ensure correct layering during the - * animation. - */ - var launchContainer: ViewGroup - - /** - * Return the [State] of the view that will be animated. We will animate from this state to - * the final window state. - * - * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the - * animation. - */ - fun createAnimatorState(): State - /** * The intent was started. If [willAnimate] is false, nothing else will happen and the * animation will not be started. */ fun onIntentStarted(willAnimate: Boolean) {} - /** - * The animation started. This is typically used to initialize any additional resource - * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding - * fully above the [root view][getRootView]. - */ - fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {} - - /** The animation made progress and the expandable view [state] should be updated. */ - fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {} - - /** - * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was - * called previously. This is typically used to clean up the resources initialized when the - * animation was started. - */ - fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {} - /** * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called @@ -313,66 +252,11 @@ class ActivityLaunchAnimator( fun onLaunchAnimationCancelled() {} } - /** The state of an expandable view during an [ActivityLaunchAnimator] animation. */ - open class State( - /** The position of the view in screen space coordinates. */ - var top: Int, - var bottom: Int, - var left: Int, - var right: Int, - - var topCornerRadius: Float = 0f, - var bottomCornerRadius: Float = 0f - ) { - private val startTop = top - private val startBottom = bottom - private val startLeft = left - private val startRight = right - private val startWidth = width - private val startHeight = height - val startCenterX = centerX - val startCenterY = centerY - - val width: Int - get() = right - left - - val height: Int - get() = bottom - top - - open val topChange: Int - get() = top - startTop - - open val bottomChange: Int - get() = bottom - startBottom - - val leftChange: Int - get() = left - startLeft - - val rightChange: Int - get() = right - startRight - - val widthRatio: Float - get() = width.toFloat() / startWidth - - val heightRatio: Float - get() = height.toFloat() / startHeight - - val centerX: Float - get() = left + width / 2f - - val centerY: Float - get() = top + height / 2f - - /** Whether the expanded view should be visible or hidden. */ - var visible: Boolean = true - } - @VisibleForTesting inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() { private val launchContainer = controller.launchContainer private val context = launchContainer.context private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer) - private var animator: ValueAnimator? = null private val matrix = Matrix() private val invertMatrix = Matrix() @@ -380,6 +264,7 @@ class ActivityLaunchAnimator( private var windowCropF = RectF() private var timedOut = false private var cancelled = false + private var animation: LaunchAnimator.Animation? = null // A timeout to cancel the remote animation if it is not started within X milliseconds after // the intent was started. @@ -429,7 +314,7 @@ class ActivityLaunchAnimator( nonApps: Array?, iCallback: IRemoteAnimationFinishedCallback? ) { - if (DEBUG) { + if (LaunchAnimator.DEBUG) { Log.d(TAG, "Remote animation started") } @@ -449,36 +334,20 @@ class ActivityLaunchAnimator( it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR } - // Start state. - val state = controller.createAnimatorState() - - val startTop = state.top - val startBottom = state.bottom - val startLeft = state.left - val startRight = state.right - val startXCenter = (startLeft + startRight) / 2f - val startWidth = startRight - startLeft - - val startTopCornerRadius = state.topCornerRadius - val startBottomCornerRadius = state.bottomCornerRadius - - // End state. val windowBounds = window.screenSpaceBounds - val endTop = windowBounds.top - val endBottom = windowBounds.bottom - val endLeft = windowBounds.left - val endRight = windowBounds.right - val endXCenter = (endLeft + endRight) / 2f - val endWidth = endRight - endLeft + val endState = LaunchAnimator.State( + top = windowBounds.top, + bottom = windowBounds.bottom, + left = windowBounds.left, + right = windowBounds.right + ) + val callback = this@ActivityLaunchAnimator.callback!! + val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo) - // TODO(b/184121838): Ensure that we are launching on the same screen. - val rootViewLocation = launchContainer.locationOnScreen - val isExpandingFullyAbove = endTop <= rootViewLocation[1] && - endBottom >= rootViewLocation[1] + launchContainer.height && - endLeft <= rootViewLocation[0] && - endRight >= rootViewLocation[0] + launchContainer.width - - // TODO(b/184121838): We should somehow get the top and bottom radius of the window. + // TODO(b/184121838): We should somehow get the top and bottom radius of the window + // instead of recomputing isExpandingFullyAbove here. + val isExpandingFullyAbove = + launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState) val endRadius = if (isExpandingFullyAbove) { // Most of the time, expanding fully above the root view means expanding in full // screen. @@ -488,97 +357,40 @@ class ActivityLaunchAnimator( // a radius of 0. 0f } + endState.topCornerRadius = endRadius + endState.bottomCornerRadius = endRadius - // We add an extra layer with the same color as the app splash screen background color, - // which is usually the same color of the app background. We first fade in this layer - // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the - // launch container and reveal the opening window. - val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo) - val windowBackgroundLayer = GradientDrawable().apply { - setColor(windowBackgroundColor) - alpha = 0 - } - - // Update state. - val animator = ValueAnimator.ofFloat(0f, 1f) - this.animator = animator - animator.duration = ANIMATION_DURATION - animator.interpolator = Interpolators.LINEAR - - val launchContainerOverlay = launchContainer.overlay - animator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?, isReverse: Boolean) { - if (DEBUG) { - Log.d(TAG, "Animation started") - } - + // We animate the opening window and delegate the view expansion to [this.controller]. + val delegate = this.controller + val controller = object : LaunchAnimator.Controller by delegate { + override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { callback.setBlursDisabledForAppLaunch(true) - controller.onLaunchAnimationStart(isExpandingFullyAbove) - - // Add the drawable to the launch container overlay. Overlays always draw - // drawables after views, so we know that it will be drawn above any view added - // by the controller. - launchContainerOverlay.add(windowBackgroundLayer) + delegate.onLaunchAnimationStart(isExpandingFullyAbove) } - override fun onAnimationEnd(animation: Animator?) { - if (DEBUG) { - Log.d(TAG, "Animation ended") - } - + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { callback.setBlursDisabledForAppLaunch(false) iCallback?.invoke() - controller.onLaunchAnimationEnd(isExpandingFullyAbove) - launchContainerOverlay.remove(windowBackgroundLayer) - } - }) - - animator.addUpdateListener { animation -> - if (cancelled) { - return@addUpdateListener + delegate.onLaunchAnimationEnd(isExpandingFullyAbove) } - val linearProgress = animation.animatedFraction - val progress = animationInterpolator.getInterpolation(linearProgress) - val xProgress = animationInterpolatorX.getInterpolation(linearProgress) - val xCenter = MathUtils.lerp(startXCenter, endXCenter, xProgress) - val halfWidth = lerp(startWidth, endWidth, progress) / 2 - - state.top = lerp(startTop, endTop, progress).roundToInt() - state.bottom = lerp(startBottom, endBottom, progress).roundToInt() - state.left = (xCenter - halfWidth).roundToInt() - state.right = (xCenter + halfWidth).roundToInt() - - state.topCornerRadius = MathUtils.lerp(startTopCornerRadius, endRadius, progress) - state.bottomCornerRadius = - MathUtils.lerp(startBottomCornerRadius, endRadius, progress) - - // The expanding view can/should be hidden once it is completely coverred by the - // windowBackgroundLayer. - state.visible = - getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1 - - applyStateToWindow(window, state) - applyStateToWindowBackgroundLayer(windowBackgroundLayer, state, linearProgress) - navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } - - // If we started expanding the view, we make it 1 pixel smaller on all sides to - // avoid artefacts on the corners caused by anti-aliasing of the view background and - // the window background layer. - if (state.top != startTop && state.left != startLeft && - state.bottom != startBottom && state.right != startRight) { - state.top += 1 - state.left += 1 - state.right -= 1 - state.bottom -= 1 + override fun onLaunchAnimationProgress( + state: LaunchAnimator.State, + progress: Float, + linearProgress: Float + ) { + applyStateToWindow(window, state) + navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } + delegate.onLaunchAnimationProgress(state, progress, linearProgress) } - controller.onLaunchAnimationProgress(state, progress, linearProgress) } - animator.start() + // We draw a hole when the additional layer is fading out to reveal the opening window. + animation = launchAnimator.startAnimation( + controller, endState, windowBackgroundColor, drawHole = true) } - private fun applyStateToWindow(window: RemoteAnimationTarget, state: State) { + private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) { val screenBounds = window.screenSpaceBounds val centerX = (screenBounds.left + screenBounds.right) / 2f val centerY = (screenBounds.top + screenBounds.bottom) / 2f @@ -632,48 +444,13 @@ class ActivityLaunchAnimator( transactionApplier.scheduleApply(params) } - private fun applyStateToWindowBackgroundLayer( - drawable: GradientDrawable, - state: State, - linearProgress: Float - ) { - // Update position. - drawable.setBounds(state.left, state.top, state.right, state.bottom) - - // Update radius. - cornerRadii[0] = state.topCornerRadius - cornerRadii[1] = state.topCornerRadius - cornerRadii[2] = state.topCornerRadius - cornerRadii[3] = state.topCornerRadius - cornerRadii[4] = state.bottomCornerRadius - cornerRadii[5] = state.bottomCornerRadius - cornerRadii[6] = state.bottomCornerRadius - cornerRadii[7] = state.bottomCornerRadius - drawable.cornerRadii = cornerRadii - - // We first fade in the background layer to hide the expanding view, then fade it out - // with SRC mode to draw a hole punch in the status bar and reveal the opening window. - val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) - if (fadeInProgress < 1) { - val alpha = CONTENT_FADE_OUT_INTERPOLATOR.getInterpolation(fadeInProgress) - drawable.alpha = (alpha * 0xFF).roundToInt() - drawable.setXfermode(null) - } else { - val fadeOutProgress = getProgress(linearProgress, - ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW) - val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress) - drawable.alpha = (alpha * 0xFF).roundToInt() - drawable.setXfermode(SRC_MODE) - } - } - private fun applyStateToNavigationBar( navigationBar: RemoteAnimationTarget, - state: State, + state: LaunchAnimator.State, linearProgress: Float ) { - val fadeInProgress = getProgress(linearProgress, ANIMATION_DELAY_NAV_FADE_IN, - ANIMATION_DURATION_NAV_FADE_OUT) + val fadeInProgress = LaunchAnimator.getProgress(linearProgress, + ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT) val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash) if (fadeInProgress > 0) { @@ -682,13 +459,13 @@ class ActivityLaunchAnimator( 0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat()) windowCrop.set(state.left, 0, state.right, state.height) params - .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress)) - .withMatrix(matrix) - .withWindowCrop(windowCrop) - .withVisibility(true) + .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress)) + .withMatrix(matrix) + .withWindowCrop(windowCrop) + .withVisibility(true) } else { - val fadeOutProgress = getProgress(linearProgress, 0, - ANIMATION_DURATION_NAV_FADE_OUT) + val fadeOutProgress = LaunchAnimator.getProgress(linearProgress, 0, + ANIMATION_DURATION_NAV_FADE_OUT) params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress)) } @@ -714,7 +491,7 @@ class ActivityLaunchAnimator( cancelled = true removeTimeout() context.mainExecutor.execute { - animator?.cancel() + animation?.cancel() controller.onLaunchAnimationCancelled() } } @@ -726,9 +503,5 @@ class ActivityLaunchAnimator( e.printStackTrace() } } - - private fun lerp(start: Int, stop: Int, amount: Float): Float { - return MathUtils.lerp(start.toFloat(), stop.toFloat(), amount) - } } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt index d4be25382395a..258ca6bdf79b3 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt @@ -1,3 +1,19 @@ +/* + * Copyright (C) 2021 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.systemui.animation /** diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt index b4ffb3f6cf4eb..f7e0d588407f0 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt @@ -1,7 +1,24 @@ +/* + * Copyright (C) 2021 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.systemui.animation import android.graphics.Canvas import android.graphics.ColorFilter +import android.graphics.Insets import android.graphics.Matrix import android.graphics.PixelFormat import android.graphics.Rect @@ -42,6 +59,7 @@ open class GhostedViewLaunchAnimatorController( override var launchContainer = ghostedView.rootView as ViewGroup private val launchContainerOverlay: ViewGroupOverlay get() = launchContainer.overlay + private val launchContainerLocation = IntArray(2) /** The ghost view that is drawn and animated instead of the ghosted view. */ private var ghostView: GhostView? = null @@ -59,8 +77,12 @@ open class GhostedViewLaunchAnimatorController( * [backgroundView]. */ private var backgroundDrawable: WrappedDrawable? = null + private val backgroundInsets by lazy { getBackground()?.opticalInsets ?: Insets.NONE } private var startBackgroundAlpha: Int = 0xFF + private val ghostedViewLocation = IntArray(2) + private val ghostedViewState = LaunchAnimator.State() + /** * Return the background of the [ghostedView]. This background will be used to draw the * background of the background view that is expanding up to the final animation position. This @@ -103,16 +125,24 @@ open class GhostedViewLaunchAnimatorController( return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius } - override fun createAnimatorState(): ActivityLaunchAnimator.State { - val location = ghostedView.locationOnScreen - return ActivityLaunchAnimator.State( - top = location[1], - bottom = location[1] + ghostedView.height, - left = location[0], - right = location[0] + ghostedView.width, + override fun createAnimatorState(): LaunchAnimator.State { + val state = LaunchAnimator.State( topCornerRadius = getCurrentTopCornerRadius(), bottomCornerRadius = getCurrentBottomCornerRadius() ) + fillGhostedViewState(state) + return state + } + + fun fillGhostedViewState(state: LaunchAnimator.State) { + // For the animation we are interested in the area that has a non transparent background, + // so we have to take the optical insets into account. + ghostedView.getLocationOnScreen(ghostedViewLocation) + val insets = backgroundInsets + state.top = ghostedViewLocation[1] + insets.top + state.bottom = ghostedViewLocation[1] + ghostedView.height - insets.bottom + state.left = ghostedViewLocation[0] + insets.left + state.right = ghostedViewLocation[0] + ghostedView.width - insets.right } override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { @@ -144,7 +174,7 @@ open class GhostedViewLaunchAnimatorController( } override fun onLaunchAnimationProgress( - state: ActivityLaunchAnimator.State, + state: LaunchAnimator.State, progress: Float, linearProgress: Float ) { @@ -162,19 +192,47 @@ open class GhostedViewLaunchAnimatorController( return } - val scale = min(state.widthRatio, state.heightRatio) - ghostViewMatrix.setValues(initialGhostViewMatrixValues) - ghostViewMatrix.postScale(scale, scale, state.startCenterX, state.startCenterY) + // The ghost and backgrounds views were made invisible earlier. That can for instance happen + // when animating a dialog into a view. + if (ghostView.visibility == View.INVISIBLE) { + ghostView.visibility = View.VISIBLE + backgroundView.visibility = View.VISIBLE + } + + fillGhostedViewState(ghostedViewState) + val leftChange = state.left - ghostedViewState.left + val rightChange = state.right - ghostedViewState.right + val topChange = state.top - ghostedViewState.top + val bottomChange = state.bottom - ghostedViewState.bottom + + val widthRatio = state.width.toFloat() / ghostedViewState.width + val heightRatio = state.height.toFloat() / ghostedViewState.height + val scale = min(widthRatio, heightRatio) + + launchContainer.getLocationOnScreen(launchContainerLocation) + GhostView.calculateMatrix(ghostedView, launchContainer, ghostViewMatrix) + ghostViewMatrix.postScale( + scale, scale, + ghostedViewState.centerX - launchContainerLocation[0], + ghostedViewState.centerY - launchContainerLocation[1] + ) ghostViewMatrix.postTranslate( - (state.leftChange + state.rightChange) / 2f, - (state.topChange + state.bottomChange) / 2f + (leftChange + rightChange) / 2f, + (topChange + bottomChange) / 2f ) ghostView.animationMatrix = ghostViewMatrix - backgroundView.top = state.top - backgroundView.bottom = state.bottom - backgroundView.left = state.left - backgroundView.right = state.right + // We need to take into account the background insets for the background position. + val insets = backgroundInsets + val topWithInsets = state.top - insets.top + val leftWithInsets = state.left - insets.left + val rightWithInsets = state.right + insets.right + val bottomWithInsets = state.bottom + insets.bottom + + backgroundView.top = topWithInsets - launchContainerLocation[1] + backgroundView.bottom = bottomWithInsets - launchContainerLocation[1] + backgroundView.left = leftWithInsets - launchContainerLocation[0] + backgroundView.right = rightWithInsets - launchContainerLocation[0] val backgroundDrawable = backgroundDrawable!! backgroundDrawable.wrapped?.let { @@ -207,7 +265,7 @@ open class GhostedViewLaunchAnimatorController( * [drawable] is a [LayerDrawable], this will return the first layer that is a * [GradientDrawable]. */ - private fun findGradientDrawable(drawable: Drawable): GradientDrawable? { + fun findGradientDrawable(drawable: Drawable): GradientDrawable? { if (drawable is GradientDrawable) { return drawable } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt new file mode 100644 index 0000000000000..3bf6c5ebd091a --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2021 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.systemui.animation + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.drawable.GradientDrawable +import android.util.Log +import android.util.MathUtils +import android.view.View +import android.view.ViewGroup +import android.view.animation.AnimationUtils +import android.view.animation.PathInterpolator +import kotlin.math.roundToInt + +private const val TAG = "LaunchAnimator" + +/** A base class to animate a window launch (activity or dialog) from a view . */ +class LaunchAnimator @JvmOverloads constructor( + context: Context, + private val isForTesting: Boolean = false +) { + companion object { + internal const val DEBUG = false + const val ANIMATION_DURATION = 500L + private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L + private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L + private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT + + private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f) + private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC) + + /** + * Given the [linearProgress] of a launch animation, return the linear progress of the + * sub-animation starting [delay] ms after the launch animation and that lasts [duration]. + */ + @JvmStatic + fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float { + return MathUtils.constrain( + (linearProgress * ANIMATION_DURATION - delay) / duration, + 0.0f, + 1.0f + ) + } + } + + /** The interpolator used for the width, height, Y position and corner radius. */ + private val animationInterpolator = AnimationUtils.loadInterpolator(context, + R.interpolator.launch_animation_interpolator_y) + + /** The interpolator used for the X position. */ + private val animationInterpolatorX = AnimationUtils.loadInterpolator(context, + R.interpolator.launch_animation_interpolator_x) + + private val launchContainerLocation = IntArray(2) + private val cornerRadii = FloatArray(8) + + /** + * A controller that takes care of applying the animation to an expanding view. + * + * Note that all callbacks (onXXX methods) are all called on the main thread. + */ + interface Controller { + /** + * The container in which the view that started the animation will be animating together + * with the opening window. + * + * This will be used to: + * - Get the associated [Context]. + * - Compute whether we are expanding fully above the launch container. + * - Apply surface transactions in sync with RenderThread when animating an activity + * launch. + * + * This container can be changed to force this [Controller] to animate the expanding view + * inside a different location, for instance to ensure correct layering during the + * animation. + */ + var launchContainer: ViewGroup + + /** + * Return the [State] of the view that will be animated. We will animate from this state to + * the final window state. + * + * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the + * animation. + */ + fun createAnimatorState(): State + + /** + * The animation started. This is typically used to initialize any additional resource + * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding + * fully above the [launchContainer]. + */ + fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {} + + /** The animation made progress and the expandable view [state] should be updated. */ + fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {} + + /** + * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was + * called previously. This is typically used to clean up the resources initialized when the + * animation was started. + */ + fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {} + } + + /** The state of an expandable view during a [LaunchAnimator] animation. */ + open class State( + /** The position of the view in screen space coordinates. */ + var top: Int = 0, + var bottom: Int = 0, + var left: Int = 0, + var right: Int = 0, + + var topCornerRadius: Float = 0f, + var bottomCornerRadius: Float = 0f + ) { + private val startTop = top + + val width: Int + get() = right - left + + val height: Int + get() = bottom - top + + open val topChange: Int + get() = top - startTop + + val centerX: Float + get() = left + width / 2f + + val centerY: Float + get() = top + height / 2f + + /** Whether the expanding view should be visible or hidden. */ + var visible: Boolean = true + } + + interface Animation { + /** Cancel the animation. */ + fun cancel() + } + + /** + * Start a launch animation controlled by [controller] towards [endState]. An intermediary + * layer with [windowBackgroundColor] will fade in then fade out above the expanding view, and + * should be the same background color as the opening (or closing) window. If [drawHole] is + * true, then this intermediary layer will be drawn with SRC blending mode while it fades out. + * + * TODO(b/184121838): Remove [drawHole] and instead make the StatusBar draw this hole instead. + */ + fun startAnimation( + controller: Controller, + endState: State, + windowBackgroundColor: Int, + drawHole: Boolean = false + ): Animation { + val state = controller.createAnimatorState() + + // Start state. + val startTop = state.top + val startBottom = state.bottom + val startLeft = state.left + val startRight = state.right + val startCenterX = (startLeft + startRight) / 2f + val startWidth = startRight - startLeft + val startTopCornerRadius = state.topCornerRadius + val startBottomCornerRadius = state.bottomCornerRadius + + // End state. + var endTop = endState.top + var endBottom = endState.bottom + var endLeft = endState.left + var endRight = endState.right + var endCenterX = (endLeft + endRight) / 2f + var endWidth = endRight - endLeft + val endTopCornerRadius = endState.topCornerRadius + val endBottomCornerRadius = endState.bottomCornerRadius + + fun maybeUpdateEndState() { + if (endTop != endState.top || endBottom != endState.bottom || + endLeft != endState.left || endRight != endState.right) { + endTop = endState.top + endBottom = endState.bottom + endLeft = endState.left + endRight = endState.right + endCenterX = (endLeft + endRight) / 2f + endWidth = endRight - endLeft + } + } + + val launchContainer = controller.launchContainer + val isExpandingFullyAbove = isExpandingFullyAbove(launchContainer, endState) + + // We add an extra layer with the same color as the dialog/app splash screen background + // color, which is usually the same color of the app background. We first fade in this layer + // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the + // launch container and reveal the opening window. + val windowBackgroundLayer = GradientDrawable().apply { + setColor(windowBackgroundColor) + alpha = 0 + } + + // Update state. + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = if (isForTesting) 0 else ANIMATION_DURATION + animator.interpolator = Interpolators.LINEAR + + val launchContainerOverlay = launchContainer.overlay + var cancelled = false + animator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator?, isReverse: Boolean) { + if (DEBUG) { + Log.d(TAG, "Animation started") + } + controller.onLaunchAnimationStart(isExpandingFullyAbove) + + // Add the drawable to the launch container overlay. Overlays always draw + // drawables after views, so we know that it will be drawn above any view added + // by the controller. + launchContainerOverlay.add(windowBackgroundLayer) + } + + override fun onAnimationEnd(animation: Animator?) { + if (DEBUG) { + Log.d(TAG, "Animation ended") + } + controller.onLaunchAnimationEnd(isExpandingFullyAbove) + launchContainerOverlay.remove(windowBackgroundLayer) + } + }) + + animator.addUpdateListener { animation -> + if (cancelled) { + // TODO(b/184121838): Cancel the animator directly instead of just skipping the + // update. + return@addUpdateListener + } + + maybeUpdateEndState() + + // TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non + // reversed animation. + val linearProgress = animation.animatedFraction + val progress = animationInterpolator.getInterpolation(linearProgress) + val xProgress = animationInterpolatorX.getInterpolation(linearProgress) + + val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress) + val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f + + state.top = MathUtils.lerp(startTop, endTop, progress).roundToInt() + state.bottom = MathUtils.lerp(startBottom, endBottom, progress).roundToInt() + state.left = (xCenter - halfWidth).roundToInt() + state.right = (xCenter + halfWidth).roundToInt() + + state.topCornerRadius = + MathUtils.lerp(startTopCornerRadius, endTopCornerRadius, progress) + state.bottomCornerRadius = + MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress) + + // The expanding view can/should be hidden once it is completely covered by the opening + // window. + state.visible = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1 + + applyStateToWindowBackgroundLayer( + windowBackgroundLayer, + state, + linearProgress, + launchContainer, + drawHole + ) + controller.onLaunchAnimationProgress(state, progress, linearProgress) + } + + animator.start() + return object : Animation { + override fun cancel() { + cancelled = true + animator.cancel() + } + } + } + + /** Return whether we are expanding fully above the [launchContainer]. */ + internal fun isExpandingFullyAbove(launchContainer: View, endState: State): Boolean { + launchContainer.getLocationOnScreen(launchContainerLocation) + return endState.top <= launchContainerLocation[1] && + endState.bottom >= launchContainerLocation[1] + launchContainer.height && + endState.left <= launchContainerLocation[0] && + endState.right >= launchContainerLocation[0] + launchContainer.width + } + + private fun applyStateToWindowBackgroundLayer( + drawable: GradientDrawable, + state: State, + linearProgress: Float, + launchContainer: View, + drawHole: Boolean + ) { + // Update position. + launchContainer.getLocationOnScreen(launchContainerLocation) + drawable.setBounds( + state.left - launchContainerLocation[0], + state.top - launchContainerLocation[1], + state.right - launchContainerLocation[0], + state.bottom - launchContainerLocation[1] + ) + + // Update radius. + cornerRadii[0] = state.topCornerRadius + cornerRadii[1] = state.topCornerRadius + cornerRadii[2] = state.topCornerRadius + cornerRadii[3] = state.topCornerRadius + cornerRadii[4] = state.bottomCornerRadius + cornerRadii[5] = state.bottomCornerRadius + cornerRadii[6] = state.bottomCornerRadius + cornerRadii[7] = state.bottomCornerRadius + drawable.cornerRadii = cornerRadii + + // We first fade in the background layer to hide the expanding view, then fade it out + // with SRC mode to draw a hole punch in the status bar and reveal the opening window. + val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) + if (fadeInProgress < 1) { + val alpha = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(fadeInProgress) + drawable.alpha = (alpha * 0xFF).roundToInt() + } else { + val fadeOutProgress = getProgress( + linearProgress, ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW) + val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress) + drawable.alpha = (alpha * 0xFF).roundToInt() + + if (drawHole) { + drawable.setXfermode(SRC_MODE) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index 94f186f007784..3a2364039f096 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -22,6 +22,8 @@ import android.content.Context; import android.os.Handler; import com.android.internal.statusbar.IStatusBarService; +import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.LaunchAnimator; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; @@ -261,4 +263,20 @@ public interface StatusBarDependenciesModule { @Binds QSCarrierGroupController.SlotIndexResolver provideSlotIndexResolver( QSCarrierGroupController.SubscriptionManagerSlotIndexResolver impl); + + /** + */ + @Provides + @SysUISingleton + static LaunchAnimator provideLaunchAnimator(Context context) { + return new LaunchAnimator(context); + } + + /** + */ + @Provides + @SysUISingleton + static ActivityLaunchAnimator provideActivityLaunchAnimator(LaunchAnimator launchAnimator) { + return new ActivityLaunchAnimator(launchAnimator); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt index f19cf5d8d9c7a..64a73054c4348 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt @@ -2,8 +2,8 @@ package com.android.systemui.statusbar.notification import android.util.MathUtils import com.android.internal.annotations.VisibleForTesting -import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Interpolators +import com.android.systemui.animation.LaunchAnimator import kotlin.math.min /** Parameters for the notifications expand animations. */ @@ -15,7 +15,7 @@ class ExpandAnimationParameters( topCornerRadius: Float = 0f, bottomCornerRadius: Float = 0f -) : ActivityLaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) { +) : LaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) { @VisibleForTesting constructor() : this( top = 0, bottom = 0, left = 0, right = 0, topCornerRadius = 0f, bottomCornerRadius = 0f @@ -55,6 +55,6 @@ class ExpandAnimationParameters( } fun getProgress(delay: Long, duration: Long): Float { - return ActivityLaunchAnimator.getProgress(linearProgress, delay, duration) + return LaunchAnimator.getProgress(linearProgress, delay, duration) } } \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt index 1bbef2562d217..22c3eda03b1e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt @@ -3,6 +3,7 @@ package com.android.systemui.statusbar.notification import android.view.ViewGroup import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.LaunchAnimator import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.statusbar.phone.HeadsUpManagerPhone @@ -54,7 +55,7 @@ class NotificationLaunchAnimatorController( // Do nothing. Notifications are always animated inside their rootView. } - override fun createAnimatorState(): ActivityLaunchAnimator.State { + override fun createAnimatorState(): LaunchAnimator.State { // If the notification panel is collapsed, the clip may be larger than the height. val height = max(0, notification.actualHeight - notification.clipBottomAmount) val location = notification.locationOnScreen @@ -72,12 +73,12 @@ class NotificationLaunchAnimatorController( notification.currentBackgroundRadiusTop } val params = ExpandAnimationParameters( - top = windowTop, - bottom = location[1] + height, - left = location[0], - right = location[0] + notification.width, - topCornerRadius = topCornerRadius, - bottomCornerRadius = notification.currentBackgroundRadiusBottom + top = windowTop, + bottom = location[1] + height, + left = location[0], + right = location[0] + notification.width, + topCornerRadius = topCornerRadius, + bottomCornerRadius = notification.currentBackgroundRadiusBottom ) params.startTranslationZ = notification.translationZ @@ -86,8 +87,8 @@ class NotificationLaunchAnimatorController( params.startClipTopAmount = notification.clipTopAmount if (notification.isChildInGroup) { params.startNotificationTop += notification.notificationParent.translationY - val parentRoundedClip = Math.max(clipStartLocation - - notification.notificationParent.locationOnScreen[1], 0) + val parentRoundedClip = Math.max( + clipStartLocation - notification.notificationParent.locationOnScreen[1], 0) params.parentStartRoundedTopClipping = parentRoundedClip val parentClip = notification.notificationParent.clipTopAmount @@ -157,7 +158,7 @@ class NotificationLaunchAnimatorController( } override fun onLaunchAnimationProgress( - state: ActivityLaunchAnimator.State, + state: LaunchAnimator.State, progress: Float, linearProgress: Float ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 198ad98b8a376..6890e4fc65f9d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -100,8 +100,8 @@ import com.android.keyguard.dagger.KeyguardUserSwitcherComponent; import com.android.systemui.DejankUtils; import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.Interpolators; +import com.android.systemui.animation.LaunchAnimator; import com.android.systemui.biometrics.AuthController; import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.FalsingCollector; @@ -209,7 +209,7 @@ public class NotificationPanelViewController extends PanelViewController { */ private static final int FLING_HIDE = 2; private static final long ANIMATION_DELAY_ICON_FADE_IN = - ActivityLaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION + LaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION - CollapsedStatusBarFragment.FADE_IN_DELAY - 48; private final DozeParameters mDozeParameters; @@ -3574,7 +3574,7 @@ public class NotificationPanelViewController extends PanelViewController { } public void applyLaunchAnimationProgress(float linearProgress) { - boolean hideIcons = ActivityLaunchAnimator.getProgress(linearProgress, + boolean hideIcons = LaunchAnimator.getProgress(linearProgress, ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f; if (hideIcons != mHideIconsDuringLaunchAnimation) { mHideIconsDuringLaunchAnimation = hideIcons; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 685b0625b9a2b..9da34be890c1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -663,7 +663,7 @@ public class StatusBar extends SystemUI implements private final SysuiStatusBarStateController mStatusBarStateController; private HeadsUpAppearanceController mHeadsUpAppearanceController; - private ActivityLaunchAnimator mActivityLaunchAnimator; + private final ActivityLaunchAnimator mActivityLaunchAnimator; private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider; protected StatusBarNotificationPresenter mPresenter; private NotificationActivityStarter mNotificationActivityStarter; @@ -782,7 +782,8 @@ public class StatusBar extends SystemUI implements UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, Optional startingSurfaceOptional, TunerService tunerService, - DumpManager dumpManager) { + DumpManager dumpManager, + ActivityLaunchAnimator activityLaunchAnimator) { super(context); mNotificationsController = notificationsController; mLightBarController = lightBarController; @@ -886,6 +887,7 @@ public class StatusBar extends SystemUI implements }); mActivityIntentHelper = new ActivityIntentHelper(mContext); + mActivityLaunchAnimator = activityLaunchAnimator; // TODO(b/190746471): Find a better home for this. DateTimeView.setReceiverHandler(timeTickHandler); @@ -1433,7 +1435,7 @@ public class StatusBar extends SystemUI implements private void setUpPresenter() { // Set up the initial notification state. - mActivityLaunchAnimator = new ActivityLaunchAnimator(mKeyguardHandler, mContext); + mActivityLaunchAnimator.setCallback(mKeyguardHandler); mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider( mNotificationShadeWindowViewController, mStackScrollerController.getNotificationListContainer(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt index 14e513a0556d7..32aae6c05df60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt @@ -1,6 +1,7 @@ package com.android.systemui.statusbar.phone import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.LaunchAnimator /** * A [ActivityLaunchAnimator.Controller] that takes care of collapsing the status bar at the right @@ -22,7 +23,7 @@ class StatusBarLaunchAnimatorController( delegate.onLaunchAnimationStart(isExpandingFullyAbove) statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true) if (!isExpandingFullyAbove) { - statusBar.collapsePanelWithDuration(ActivityLaunchAnimator.ANIMATION_DURATION.toInt()) + statusBar.collapsePanelWithDuration(LaunchAnimator.ANIMATION_DURATION.toInt()) } } @@ -33,7 +34,7 @@ class StatusBarLaunchAnimatorController( } override fun onLaunchAnimationProgress( - state: ActivityLaunchAnimator.State, + state: LaunchAnimator.State, progress: Float, linearProgress: Float ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java index 63ee701425ed2..a5cf1136ece9a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java @@ -28,6 +28,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.InitController; +import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollector; @@ -227,7 +228,8 @@ public interface StatusBarPhoneModule { UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, Optional startingSurfaceOptional, TunerService tunerService, - DumpManager dumpManager) { + DumpManager dumpManager, + ActivityLaunchAnimator activityLaunchAnimator) { return new StatusBar( context, notificationsController, @@ -321,6 +323,7 @@ public interface StatusBarPhoneModule { unlockedScreenOffAnimationController, startingSurfaceOptional, tunerService, - dumpManager); + dumpManager, + activityLaunchAnimator); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt index cc35a8f9e1b5b..d819fa2adc388 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt @@ -46,6 +46,7 @@ import org.mockito.junit.MockitoJUnit @RunWithLooper class ActivityLaunchAnimatorTest : SysuiTestCase() { private val launchContainer = LinearLayout(mContext) + private val launchAnimator = LaunchAnimator(mContext, isForTesting = true) @Mock lateinit var callback: ActivityLaunchAnimator.Callback @Spy private val controller = TestLaunchAnimatorController(launchContainer) @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback @@ -56,7 +57,8 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { @Before fun setup() { - activityLaunchAnimator = ActivityLaunchAnimator(callback, mContext) + activityLaunchAnimator = ActivityLaunchAnimator(launchAnimator) + activityLaunchAnimator.callback = callback } private fun startIntentWithAnimation( @@ -120,7 +122,8 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { @Test fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() { `when`(callback.isOnKeyguard()).thenReturn(true) - val animator = ActivityLaunchAnimator(callback, context) + val animator = ActivityLaunchAnimator(launchAnimator) + animator.callback = callback val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) var animationAdapter: RemoteAnimationAdapter? = null @@ -208,7 +211,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { private class TestLaunchAnimatorController( override var launchContainer: ViewGroup ) : ActivityLaunchAnimator.Controller { - override fun createAnimatorState() = ActivityLaunchAnimator.State( + override fun createAnimatorState() = LaunchAnimator.State( top = 100, bottom = 200, left = 300, @@ -232,7 +235,7 @@ private class TestLaunchAnimatorController( } override fun onLaunchAnimationProgress( - state: ActivityLaunchAnimator.State, + state: LaunchAnimator.State, progress: Float, linearProgress: Float ) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt index 8cba25dc1b920..58e0cb259bb22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt @@ -32,7 +32,7 @@ class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() { fun animatingOrphanViewDoesNotCrash() { val ghostedView = LinearLayout(mContext) val controller = GhostedViewLaunchAnimatorController(ghostedView) - val state = ActivityLaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0) + val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0) controller.onIntentStarted(willAnimate = true) controller.onLaunchAnimationStart(isExpandingFullyAbove = true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java index b23414bacf10a..73b97e15b5197 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java @@ -77,6 +77,7 @@ import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.InitController; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollectorFake; @@ -268,6 +269,7 @@ public class StatusBarTest extends SysuiTestCase { @Mock private StartingSurface mStartingSurface; @Mock private OperatorNameViewController mOperatorNameViewController; @Mock private OperatorNameViewController.Factory mOperatorNameViewControllerFactory; + @Mock private ActivityLaunchAnimator mActivityLaunchAnimator; private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); @@ -442,7 +444,8 @@ public class StatusBarTest extends SysuiTestCase { mUnlockedScreenOffAnimationController, Optional.of(mStartingSurface), mTunerService, - mock(DumpManager.class)); + mock(DumpManager.class), + mActivityLaunchAnimator); when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class), any(NotificationPanelViewController.class), any(BiometricUnlockController.class), any(ViewGroup.class), any(KeyguardBypassController.class))) From e5b54057c1ab98a56a5bda6b1907cfca615f4654 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Wed, 25 Aug 2021 12:52:29 +0200 Subject: [PATCH 2/3] Introduce DialogLaunchAnimator (1/2) This CL introduces the DialogLaunchAnimator that allows to animate the entry/exit of any Dialog from/to any View. Note that this CL does not use the animator for any dialog yet, that will be added in a follow-up CL. Test: atest DialogLaunchAnimatorTest Bug: 193634619 Change-Id: I87f71f8f127eb6c8f2ff7dc6a434b207d9ad4d11 --- .../res/anim/launch_host_dialog_enter.xml | 23 + .../res/anim/launch_host_dialog_exit.xml | 22 + .../SystemUI/animation/res/values/ids.xml | 19 + .../SystemUI/animation/res/values/styles.xml | 22 + .../animation/DialogLaunchAnimator.kt | 523 ++++++++++++++++++ .../dagger/StatusBarDependenciesModule.java | 11 + .../systemui/statusbar/phone/StatusBar.java | 8 +- .../statusbar/phone/SystemUIDialog.java | 46 +- .../phone/SystemUIHostDialogProvider.kt | 34 ++ .../phone/dagger/StatusBarPhoneModule.java | 7 +- .../animation/DialogLaunchAnimatorTest.kt | 185 +++++++ .../statusbar/phone/StatusBarTest.java | 5 +- 12 files changed, 899 insertions(+), 6 deletions(-) create mode 100644 packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml create mode 100644 packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml create mode 100644 packages/SystemUI/animation/res/values/ids.xml create mode 100644 packages/SystemUI/animation/res/values/styles.xml create mode 100644 packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt diff --git a/packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml b/packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml new file mode 100644 index 0000000000000..c6b87d38f7da1 --- /dev/null +++ b/packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml b/packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml new file mode 100644 index 0000000000000..a0f441eaeed48 --- /dev/null +++ b/packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml new file mode 100644 index 0000000000000..ef60a248f79ad --- /dev/null +++ b/packages/SystemUI/animation/res/values/ids.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/packages/SystemUI/animation/res/values/styles.xml b/packages/SystemUI/animation/res/values/styles.xml new file mode 100644 index 0000000000000..89f93b81fa994 --- /dev/null +++ b/packages/SystemUI/animation/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt new file mode 100644 index 0000000000000..fbf466e8cebe4 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -0,0 +1,523 @@ +/* + * Copyright (C) 2021 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.systemui.animation + +import android.app.Dialog +import android.content.Context +import android.graphics.Color +import android.os.Looper +import android.util.Log +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.view.WindowManager +import android.widget.FrameLayout + +private const val TAG = "DialogLaunchAnimator" + +/** + * A class that allows dialogs to be started in a seamless way from a view that is transforming + * nicely into the starting dialog. + * + * Important: Don't forget to call [DialogLaunchAnimator.onDozeAmountChanged] when the doze amount + * changes to gracefully handle dialogs fading out when the device is dozing. + */ +class DialogLaunchAnimator( + private val context: Context, + private val launchAnimator: LaunchAnimator, + private val hostDialogProvider: HostDialogProvider +) { + private companion object { + private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running + } + + // TODO(b/201264644): Remove this set. + private val currentAnimations = hashSetOf() + + /** + * Show [dialog] by expanding it from [view]. + * + * Caveats: When calling this function, the dialog content view will actually be stolen and + * attached to a different dialog (and thus a different window) which means that the actual + * dialog window will never be drawn. Moreover, unless [dialog] is a [ListenableDialog], you + * must call dismiss(), hide() and show() on the [Dialog] returned by this function to actually + * dismiss, hide or show the dialog. + */ + fun showFromView(dialog: Dialog, view: View): Dialog { + if (Looper.myLooper() != Looper.getMainLooper()) { + throw IllegalStateException( + "showFromView must be called from the main thread and dialog must be created in " + + "the main thread") + } + + // Make sure we don't run the launch animation from the same view twice at the same time. + if (view.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) { + Log.e(TAG, "Not running dialog launch animation as there is already one running") + dialog.show() + return dialog + } + + view.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true) + + val launchAnimation = DialogLaunchAnimation( + context, launchAnimator, hostDialogProvider, view, + onDialogDismissed = { currentAnimations.remove(it) }, originalDialog = dialog) + val hostDialog = launchAnimation.hostDialog + currentAnimations.add(launchAnimation) + + // If the dialog is dismissed/hidden/shown, then we should actually dismiss/hide/show the + // host dialog. + if (dialog is ListenableDialog) { + dialog.addListener(object : DialogListener { + override fun onDismiss() { + dialog.removeListener(this) + hostDialog.dismiss() + } + + override fun onHide() { + if (launchAnimation.ignoreNextCallToHide) { + launchAnimation.ignoreNextCallToHide = false + return + } + + hostDialog.hide() + } + + override fun onShow() { + hostDialog.show() + + // We don't actually want to show the original dialog, so hide it. + launchAnimation.ignoreNextCallToHide = true + dialog.hide() + } + }) + } + + launchAnimation.start() + return hostDialog + } + + /** Notify the current doze amount, to ensure that dialogs fade out when dozing. */ + // TODO(b/193634619): Replace this by some mandatory constructor parameter to make sure that we + // don't forget to call this when the doze amount changes. + fun onDozeAmountChanged(amount: Float) { + currentAnimations.forEach { it.onDozeAmountChanged(amount) } + } +} + +interface HostDialogProvider { + /** + * Create a host dialog that will be used to host a launch animation. This host dialog must: + * 1. call [onCreateCallback] in its onCreate() method, e.g. right after calling + * super.onCreate(). + * 2. call [dismissOverride] instead of doing any dismissing logic. The actual dismissing + * logic should instead be done inside the lambda passed to [dismissOverride], which will + * be called after the exit animation. + * + * See SystemUIHostDialogProvider for an example of implementation. + */ + fun createHostDialog( + context: Context, + onCreateCallback: () -> Unit, + dismissOverride: (() -> Unit) -> Unit + ): Dialog +} + +/** A dialog to/from which we can add/remove listeners. */ +interface ListenableDialog { + /** Add [listener] to the listeners. */ + fun addListener(listener: DialogListener) + + /** Remove [listener] from the listeners. */ + fun removeListener(listener: DialogListener) +} + +interface DialogListener { + /** Called when this dialog dismiss() is called. */ + fun onDismiss() + + /** Called when this dialog hide() is called. */ + fun onHide() + + /** Called when this dialog show() is called. */ + fun onShow() +} + +private class DialogLaunchAnimation( + private val context: Context, + private val launchAnimator: LaunchAnimator, + hostDialogProvider: HostDialogProvider, + + /** The view that triggered the dialog after being tapped. */ + private val touchSurface: View, + + /** + * A callback that will be called with this [DialogLaunchAnimation] after the dialog was + * dismissed and the exit animation is done. + */ + private val onDialogDismissed: (DialogLaunchAnimation) -> Unit, + + /** The original dialog whose content will be shown and animate in/out in [hostDialog]. */ + private val originalDialog: Dialog +) { + /** + * The fullscreen dialog to which we will add the content view [originalDialogView] of + * [originalDialog]. + */ + val hostDialog = hostDialogProvider.createHostDialog( + context, this::onHostDialogCreated, this::onHostDialogDismissed) + + /** The root content view of [hostDialog]. */ + private val hostDialogRoot = FrameLayout(context) + + /** + * The content view of [originalDialog], which will be stolen from that dialog and added to + * [hostDialogRoot]. + */ + private var originalDialogView: View? = null + + /** + * The background color of [originalDialogView], taking into consideration the [originalDialog] + * window background color. + */ + private var originalDialogBackgroundColor = Color.BLACK + + /** + * Whether we are currently launching/showing the dialog by animating it from [touchSurface]. + */ + private var isLaunching = true + + /** Whether we are currently dismissing/hiding the dialog by animating into [touchSurface]. */ + private var isDismissing = false + + private var dismissRequested = false + private var drawHostDialog = false + var ignoreNextCallToHide = false + + fun start() { + // Show the host (fullscreen) dialog, to which we will add the stolen dialog view. + hostDialog.show() + + // Steal the dialog view. We do that by showing it but preventing it from drawing, then + // hiding it as soon as its content is available. + stealOriginalDialogContentView(then = this::showDialogFromView) + } + + private fun onHostDialogCreated() { + // Make the dialog fullscreen with a transparent background. + hostDialog.setContentView( + hostDialogRoot, + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + + val window = hostDialog.window + ?: throw IllegalStateException("There is no window associated to the host dialog") + window.setBackgroundDrawableResource(android.R.color.transparent) + window.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT + ) + + // The host dialog animation is a translation of 0px so that it is shown directly. The + // translation lasts X ms, so that the scrim fades in during that amount of time. + window.attributes.windowAnimations = R.style.Animation_LaunchHostDialog + + // Prevent the host dialog from drawing until the animation starts. + hostDialogRoot.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + if (drawHostDialog) { + hostDialogRoot.viewTreeObserver.removeOnPreDrawListener(this) + return true + } + + return false + } + } + ) + } + + /** Get the content view of [originalDialog] and pass it to [then]. */ + private fun stealOriginalDialogContentView(then: (View) -> Unit) { + // The original dialog content view will be attached to android.R.id.content when the dialog + // is shown, so we show the dialog and add an observer to get the view but also prevents the + // original dialog from being drawn. + val androidContent = originalDialog.findViewById(android.R.id.content) + ?: throw IllegalStateException("Dialog does not have any android.R.id.content view") + + androidContent.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + if (androidContent.childCount == 1) { + androidContent.viewTreeObserver.removeOnPreDrawListener(this) + + // Hide the animated dialog. Because of the dialog listener set up + // earlier, this would also hide the host dialog, but in this case we + // need to keep the host dialog visible. + ignoreNextCallToHide = true + originalDialog.hide() + + then(androidContent.getChildAt(0)) + return false + } + + // Never draw the original dialog content. + return false + } + }) + originalDialog.show() + } + + private fun showDialogFromView(dialogView: View) { + // Save the dialog view for later as we will need it for the close animation. + this.originalDialogView = dialogView + + // Close the dialog when clicking outside of it. + hostDialogRoot.setOnClickListener { hostDialog.dismiss() } + dialogView.isClickable = true + + // Set the background of the window dialog to the dialog itself. + // TODO(b/193634619): Support dialog windows without background. + // TODO(b/193634619): Support dialog whose background comes from the content view instead of + // the window. + val typedArray = + originalDialog.context.obtainStyledAttributes(com.android.internal.R.styleable.Window) + val backgroundRes = + typedArray.getResourceId(com.android.internal.R.styleable.Window_windowBackground, 0) + typedArray.recycle() + if (backgroundRes == 0) { + throw IllegalStateException("Dialogs with no backgrounds on window are not supported") + } + + dialogView.setBackgroundResource(backgroundRes) + originalDialogBackgroundColor = + GhostedViewLaunchAnimatorController.findGradientDrawable(dialogView.background!!) + ?.color + ?.defaultColor ?: Color.BLACK + + // Add the dialog view to the host (fullscreen) dialog and make it invisible to make sure + // it's not drawn yet. + (dialogView.parent as? ViewGroup)?.removeView(dialogView) + hostDialogRoot.addView( + dialogView, + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.CENTER + ) + ) + dialogView.visibility = View.INVISIBLE + + // Start the animation when the dialog is laid out in the center of the host dialog. + dialogView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener { + override fun onLayoutChange( + view: View, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + dialogView.removeOnLayoutChangeListener(this) + startAnimation( + isLaunching = true, + onLaunchAnimationStart = { drawHostDialog = true }, + onLaunchAnimationEnd = { + touchSurface.setTag(R.id.launch_animation_running, null) + + // We hide the touch surface when the dialog is showing. We will make this + // view visible again when dismissing the dialog. + // TODO(b/193634619): Provide an easy way for views to check if they should + // be hidden because of a dialog launch so that they don't override this + // visibility when updating/refreshing itself. + touchSurface.visibility = View.INVISIBLE + + isLaunching = false + + // dismiss was called during the animation, dismiss again now to actually + // dismiss. + if (dismissRequested) { + hostDialog.dismiss() + } + } + ) + } + }) + } + + private fun onHostDialogDismissed(actualDismiss: () -> Unit) { + if (Looper.myLooper() != Looper.getMainLooper()) { + context.mainExecutor.execute { onHostDialogDismissed(actualDismiss) } + return + } + + // TODO(b/193634619): Support interrupting the launch animation in the middle. + if (isLaunching) { + dismissRequested = true + return + } + + if (isDismissing) { + return + } + + isDismissing = true + hideDialogIntoView { instantDismiss: Boolean -> + if (instantDismiss) { + originalDialog.hide() + hostDialog.hide() + } + + originalDialog.dismiss() + actualDismiss() + } + } + + /** + * Hide the dialog into the touch surface and call [dismissDialogs] when the animation is done + * (passing instantDismiss=true) or if it's skipped (passing instantDismiss=false) to actually + * dismiss the dialogs. + */ + private fun hideDialogIntoView(dismissDialogs: (Boolean) -> Unit) { + if (!shouldAnimateDialogIntoView()) { + Log.i(TAG, "Skipping animation of dialog into the touch surface") + + // If the view is invisible it's probably because of us, so we make it visible again. + if (touchSurface.visibility == View.INVISIBLE) { + touchSurface.visibility = View.VISIBLE + } + + dismissDialogs(false /* instantDismiss */) + onDialogDismissed(this@DialogLaunchAnimation) + return + } + + startAnimation( + isLaunching = false, + onLaunchAnimationStart = { + // Remove the dim background as soon as we start the animation. + hostDialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + }, + onLaunchAnimationEnd = { + touchSurface.visibility = View.VISIBLE + originalDialogView!!.visibility = View.INVISIBLE + dismissDialogs(true /* instantDismiss */) + onDialogDismissed(this@DialogLaunchAnimation) + } + ) + } + + private fun startAnimation( + isLaunching: Boolean, + onLaunchAnimationStart: () -> Unit = {}, + onLaunchAnimationEnd: () -> Unit = {} + ) { + val dialogView = this.originalDialogView!! + + // Create 2 ghost controllers to animate both the dialog and the touch surface in the host + // dialog. + val startView = if (isLaunching) touchSurface else dialogView + val endView = if (isLaunching) dialogView else touchSurface + val startViewController = GhostedViewLaunchAnimatorController(startView) + val endViewController = GhostedViewLaunchAnimatorController(endView) + startViewController.launchContainer = hostDialogRoot + endViewController.launchContainer = hostDialogRoot + + val endState = endViewController.createAnimatorState() + val controller = object : LaunchAnimator.Controller { + override var launchContainer: ViewGroup + get() = startViewController.launchContainer + set(value) { + startViewController.launchContainer = value + endViewController.launchContainer = value + } + + override fun createAnimatorState(): LaunchAnimator.State { + return startViewController.createAnimatorState() + } + + override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + startViewController.onLaunchAnimationStart(isExpandingFullyAbove) + endViewController.onLaunchAnimationStart(isExpandingFullyAbove) + + onLaunchAnimationStart() + } + + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + startViewController.onLaunchAnimationEnd(isExpandingFullyAbove) + endViewController.onLaunchAnimationEnd(isExpandingFullyAbove) + + onLaunchAnimationEnd() + } + + override fun onLaunchAnimationProgress( + state: LaunchAnimator.State, + progress: Float, + linearProgress: Float + ) { + startViewController.onLaunchAnimationProgress(state, progress, linearProgress) + + // The end view is visible only iff the starting view is not visible. + state.visible = !state.visible + endViewController.onLaunchAnimationProgress(state, progress, linearProgress) + + // If the dialog content is complex, its dimension might change during the launch + // animation. The animation end position might also change during the exit + // animation, for instance when locking the phone when the dialog is open. Therefore + // we update the end state to the new position/size. Usually the dialog dimension or + // position will change in the early frames, so changing the end state shouldn't + // really be noticeable. + endViewController.fillGhostedViewState(endState) + } + } + + launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor) + } + + private fun shouldAnimateDialogIntoView(): Boolean { + // The touch surface should be invisible by now, if it's not then something else changed its + // visibility and we probably don't want to run the animation. + if (touchSurface.visibility != View.INVISIBLE) { + return false + } + + // If the touch surface is not attached or one of its ancestors is not visible, then we + // don't run the animation either. + if (!touchSurface.isAttachedToWindow) { + return false + } + + return (touchSurface.parent as? View)?.isShown ?: true + } + + internal fun onDozeAmountChanged(amount: Float) { + val alpha = Interpolators.ALPHA_OUT.getInterpolation(1 - amount) + val decorView = this.hostDialog.window?.decorView ?: return + if (decorView.hasOverlappingRendering() && alpha > 0.0f && + alpha < 1.0f && decorView.layerType != View.LAYER_TYPE_HARDWARE) { + decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null) + } + decorView.alpha = alpha + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index 3a2364039f096..ea00d920c9b09 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -23,6 +23,7 @@ import android.os.Handler; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.animation.LaunchAnimator; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; @@ -64,6 +65,7 @@ import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl; import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback; +import com.android.systemui.statusbar.phone.SystemUIHostDialogProvider; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger; import com.android.systemui.statusbar.policy.RemoteInputUriController; @@ -279,4 +281,13 @@ public interface StatusBarDependenciesModule { static ActivityLaunchAnimator provideActivityLaunchAnimator(LaunchAnimator launchAnimator) { return new ActivityLaunchAnimator(launchAnimator); } + + /** + */ + @Provides + @SysUISingleton + static DialogLaunchAnimator provideDialogLaunchAnimator(Context context, + LaunchAnimator launchAnimator) { + return new DialogLaunchAnimator(context, launchAnimator, new SystemUIHostDialogProvider()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 9da34be890c1d..81abe38b97e39 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -135,6 +135,7 @@ import com.android.systemui.R; import com.android.systemui.SystemUI; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.DelegateLaunchAnimatorController; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.battery.BatteryMeterViewController; import com.android.systemui.biometrics.AuthRippleController; @@ -664,6 +665,7 @@ public class StatusBar extends SystemUI implements private HeadsUpAppearanceController mHeadsUpAppearanceController; private final ActivityLaunchAnimator mActivityLaunchAnimator; + private final DialogLaunchAnimator mDialogLaunchAnimator; private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider; protected StatusBarNotificationPresenter mPresenter; private NotificationActivityStarter mNotificationActivityStarter; @@ -783,7 +785,8 @@ public class StatusBar extends SystemUI implements Optional startingSurfaceOptional, TunerService tunerService, DumpManager dumpManager, - ActivityLaunchAnimator activityLaunchAnimator) { + ActivityLaunchAnimator activityLaunchAnimator, + DialogLaunchAnimator dialogLaunchAnimator) { super(context); mNotificationsController = notificationsController; mLightBarController = lightBarController; @@ -888,6 +891,7 @@ public class StatusBar extends SystemUI implements mActivityIntentHelper = new ActivityIntentHelper(mContext); mActivityLaunchAnimator = activityLaunchAnimator; + mDialogLaunchAnimator = dialogLaunchAnimator; // TODO(b/190746471): Find a better home for this. DateTimeView.setReceiverHandler(timeTickHandler); @@ -4392,6 +4396,8 @@ public class StatusBar extends SystemUI implements && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) { mLightRevealScrim.setRevealAmount(1f - linear); } + + mDialogLaunchAnimator.onDozeAmountChanged(linear); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index 1e98c75f26168..9415d5082d10b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -30,19 +30,24 @@ import android.view.WindowManager.LayoutParams; import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.animation.DialogListener; +import com.android.systemui.animation.ListenableDialog; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.statusbar.policy.KeyguardStateController; +import java.util.LinkedHashSet; +import java.util.Set; + /** * Base class for dialogs that should appear over panels and keyguard. * The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast, * and dismisses itself when it receives the broadcast. */ -public class SystemUIDialog extends AlertDialog { - +public class SystemUIDialog extends AlertDialog implements ListenableDialog { private final Context mContext; private final DismissReceiver mDismissReceiver; + private final Set mDialogListeners = new LinkedHashSet<>(); public SystemUIDialog(Context context) { this(context, R.style.Theme_SystemUI_Dialog); @@ -72,6 +77,43 @@ public class SystemUIDialog extends AlertDialog { mDismissReceiver.unregister(); } + @Override + public void addListener(DialogListener listener) { + mDialogListeners.add(listener); + } + + @Override + public void removeListener(DialogListener listener) { + mDialogListeners.remove(listener); + } + + @Override + public void dismiss() { + super.dismiss(); + + for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { + listener.onDismiss(); + } + } + + @Override + public void hide() { + super.hide(); + + for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { + listener.onHide(); + } + } + + @Override + public void show() { + super.show(); + + for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { + listener.onShow(); + } + } + public void setShowForAllUsers(boolean show) { setShowForAllUsers(this, show); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt new file mode 100644 index 0000000000000..17ecfa26f96d3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt @@ -0,0 +1,34 @@ +package com.android.systemui.statusbar.phone + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import com.android.systemui.animation.HostDialogProvider + +/** An implementation of [HostDialogProvider] to be used when animating SysUI dialogs. */ +class SystemUIHostDialogProvider : HostDialogProvider { + override fun createHostDialog( + context: Context, + onCreateCallback: () -> Unit, + dismissOverride: (() -> Unit) -> Unit + ): Dialog { + return SystemUIHostDialog(context, onCreateCallback, dismissOverride) + } + + private class SystemUIHostDialog( + context: Context, + private val onCreateCallback: () -> Unit, + private val dismissOverride: (() -> Unit) -> Unit + ) : SystemUIDialog(context) { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + onCreateCallback() + } + + override fun dismiss() { + dismissOverride { + super.dismiss() + } + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java index a5cf1136ece9a..4b3add294b570 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java @@ -29,6 +29,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.InitController; import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollector; @@ -229,7 +230,8 @@ public interface StatusBarPhoneModule { Optional startingSurfaceOptional, TunerService tunerService, DumpManager dumpManager, - ActivityLaunchAnimator activityLaunchAnimator) { + ActivityLaunchAnimator activityLaunchAnimator, + DialogLaunchAnimator dialogLaunchAnimator) { return new StatusBar( context, notificationsController, @@ -324,6 +326,7 @@ public interface StatusBarPhoneModule { startingSurfaceOptional, tunerService, dumpManager, - activityLaunchAnimator); + activityLaunchAnimator, + dialogLaunchAnimator); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt new file mode 100644 index 0000000000000..25ce072c92ebd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt @@ -0,0 +1,185 @@ +package com.android.systemui.animation + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.ViewUtils +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.LinearLayout +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class DialogLaunchAnimatorTest : SysuiTestCase() { + private val launchAnimator = LaunchAnimator(context, isForTesting = true) + private val hostDialogprovider = TestHostDialogProvider() + private val dialogLaunchAnimator = + DialogLaunchAnimator(context, launchAnimator, hostDialogprovider) + + @Test + fun testShowDialogFromView() { + // Show the dialog. showFromView() must be called on the main thread with a dialog created + // on the main thread too. + val (dialog, hostDialog) = runOnMainThreadAndWaitForIdleSync { + val touchSurfaceRoot = LinearLayout(context) + val touchSurface = View(context) + touchSurfaceRoot.addView(touchSurface) + + // We need to attach the root to the window manager otherwise the exit animation will + // be skipped + ViewUtils.attachView(touchSurfaceRoot) + + val dialog = TestDialog(context) + val hostDialog = + dialogLaunchAnimator.showFromView(dialog, touchSurface) as TestHostDialog + dialog to hostDialog + } + + // Only the host dialog is actually showing. + assertTrue(hostDialog.isShowing) + assertFalse(dialog.isShowing) + + // The dialog onStart() method was called but not onStop(). + assertTrue(dialog.onStartCalled) + assertFalse(dialog.onStopCalled) + + // The dialog content has been stolen and is shown inside the host dialog. + val hostDialogContent = hostDialog.findViewById(android.R.id.content) + assertEquals(0, dialog.findViewById(android.R.id.content).childCount) + assertEquals(1, hostDialogContent.childCount) + + val hostDialogRoot = hostDialogContent.getChildAt(0) as ViewGroup + assertEquals(1, hostDialogRoot.childCount) + assertEquals(dialog.contentView, hostDialogRoot.getChildAt(0)) + + // If we are dozing, the host dialog window also fades out. + runOnMainThreadAndWaitForIdleSync { dialogLaunchAnimator.onDozeAmountChanged(0.5f) } + assertTrue(hostDialog.window!!.decorView.alpha < 1f) + + // Hiding/showing/dismissing the dialog should hide/show/dismiss the host dialog given that + // it's a ListenableDialog. + runOnMainThreadAndWaitForIdleSync { dialog.hide() } + assertFalse(hostDialog.isShowing) + assertFalse(dialog.isShowing) + + runOnMainThreadAndWaitForIdleSync { dialog.show() } + assertTrue(hostDialog.isShowing) + assertFalse(dialog.isShowing) + + assertFalse(dialog.onStopCalled) + runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } + assertFalse(hostDialog.isShowing) + assertFalse(dialog.isShowing) + assertTrue(hostDialog.wasDismissed) + assertTrue(dialog.onStopCalled) + } + + private fun runOnMainThreadAndWaitForIdleSync(f: () -> T): T { + lateinit var result: T + context.mainExecutor.execute { + result = f() + } + waitForIdleSync() + return result + } + + private class TestHostDialogProvider : HostDialogProvider { + override fun createHostDialog( + context: Context, + onCreateCallback: () -> Unit, + dismissOverride: (() -> Unit) -> Unit + ): Dialog = TestHostDialog(context, onCreateCallback, dismissOverride) + } + + private class TestHostDialog( + context: Context, + private val onCreateCallback: () -> Unit, + private val dismissOverride: (() -> Unit) -> Unit + ) : Dialog(context) { + var wasDismissed = false + + init { + // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw. + window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + onCreateCallback() + } + + override fun dismiss() { + dismissOverride { + super.dismiss() + wasDismissed = true + } + } + } + + private class TestDialog(context: Context) : Dialog(context), ListenableDialog { + private val listeners = hashSetOf() + val contentView = View(context) + var onStartCalled = false + var onStopCalled = false + + init { + // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw. + window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(contentView) + } + + override fun onStart() { + super.onStart() + onStartCalled = true + } + + override fun onStop() { + super.onStart() + onStopCalled = true + } + + override fun addListener(listener: DialogListener) { + listeners.add(listener) + } + + override fun removeListener(listener: DialogListener) { + listeners.remove(listener) + } + + override fun dismiss() { + super.dismiss() + notifyListeners { onDismiss() } + } + + override fun hide() { + super.hide() + notifyListeners { onHide() } + } + + override fun show() { + super.show() + notifyListeners { onShow() } + } + + private fun notifyListeners(notify: DialogListener.() -> Unit) { + for (listener in HashSet(listeners)) { + listener.notify() + } + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java index 73b97e15b5197..9a8236dde0cdf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java @@ -78,6 +78,7 @@ import com.android.systemui.InitController; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollectorFake; @@ -270,6 +271,7 @@ public class StatusBarTest extends SysuiTestCase { @Mock private OperatorNameViewController mOperatorNameViewController; @Mock private OperatorNameViewController.Factory mOperatorNameViewControllerFactory; @Mock private ActivityLaunchAnimator mActivityLaunchAnimator; + @Mock private DialogLaunchAnimator mDialogLaunchAnimator; private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); @@ -445,7 +447,8 @@ public class StatusBarTest extends SysuiTestCase { Optional.of(mStartingSurface), mTunerService, mock(DumpManager.class), - mActivityLaunchAnimator); + mActivityLaunchAnimator, + mDialogLaunchAnimator); when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class), any(NotificationPanelViewController.class), any(BiometricUnlockController.class), any(ViewGroup.class), any(KeyguardBypassController.class))) From ef3876e165396d56e7ddddab0a947a7baa486979 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Wed, 25 Aug 2021 12:54:37 +0200 Subject: [PATCH 3/3] Animate media dialogs This CL applies the DialogLaunchAnimator to the media output and media output group dialogs. See b/200781817#comment2 for before/after videos. Bug: 200781817 Test: Open media output dialog Change-Id: I1bff28cc7647db6b97b8933e5b7605d7df15c63f --- .../SystemUI/animation/res/values/styles.xml | 9 +++- .../animation/DialogLaunchAnimator.kt | 32 ++++++++++--- .../media_output_dialog_background.xml | 23 ---------- .../layout/internet_connectivity_dialog.xml | 2 +- .../res/layout/media_output_dialog.xml | 2 +- packages/SystemUI/res/layout/media_view.xml | 1 + .../SystemUI/res/values-sw600dp/dimens.xml | 4 +- packages/SystemUI/res/values/dimens.xml | 4 +- packages/SystemUI/res/values/styles.xml | 7 --- .../systemui/media/MediaControlPanel.java | 6 +-- .../systemui/media/PlayerViewHolder.kt | 1 + .../media/dialog/MediaOutputAdapter.java | 13 +++--- .../media/dialog/MediaOutputBaseDialog.java | 12 +++-- .../media/dialog/MediaOutputController.java | 45 +++++++++++++++---- .../media/dialog/MediaOutputDialog.java | 3 +- .../media/dialog/MediaOutputDialogFactory.kt | 24 +++++++--- .../media/dialog/MediaOutputGroupDialog.java | 5 ++- .../qs/tiles/dialog/InternetDialog.java | 5 +-- .../systemui/statusbar/phone/StatusBar.java | 3 +- .../phone/SystemUIHostDialogProvider.kt | 6 ++- .../animation/DialogLaunchAnimatorTest.kt | 1 + .../media/dialog/MediaOutputAdapterTest.java | 3 +- .../dialog/MediaOutputBaseDialogTest.java | 4 +- .../dialog/MediaOutputControllerTest.java | 10 +++-- .../media/dialog/MediaOutputDialogTest.java | 6 ++- .../dialog/MediaOutputGroupDialogTest.java | 5 ++- 26 files changed, 147 insertions(+), 89 deletions(-) delete mode 100644 packages/SystemUI/res/drawable/media_output_dialog_background.xml diff --git a/packages/SystemUI/animation/res/values/styles.xml b/packages/SystemUI/animation/res/values/styles.xml index 89f93b81fa994..ad06c9192bc3e 100644 --- a/packages/SystemUI/animation/res/values/styles.xml +++ b/packages/SystemUI/animation/res/values/styles.xml @@ -15,7 +15,14 @@ limitations under the License. --> - + + diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index fbf466e8cebe4..c2b36089d0a7b 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -118,6 +118,20 @@ class DialogLaunchAnimator( fun onDozeAmountChanged(amount: Float) { currentAnimations.forEach { it.onDozeAmountChanged(amount) } } + + /** + * Ensure that all dialogs currently shown won't animate into their touch surface when + * dismissed. + * + * This is a temporary API meant to be called right before we both dismiss a dialog and start + * an activity, which currently does not look good if we animate the dialog into the touch + * surface at the same time as the activity starts. + * + * TODO(b/193634619): Remove this function and animate dialog into opening activity instead. + */ + fun disableAllCurrentDialogsExitAnimations() { + currentAnimations.forEach { it.exitAnimationDisabled = true } + } } interface HostDialogProvider { @@ -133,6 +147,7 @@ interface HostDialogProvider { */ fun createHostDialog( context: Context, + theme: Int, onCreateCallback: () -> Unit, dismissOverride: (() -> Unit) -> Unit ): Dialog @@ -180,7 +195,7 @@ private class DialogLaunchAnimation( * [originalDialog]. */ val hostDialog = hostDialogProvider.createHostDialog( - context, this::onHostDialogCreated, this::onHostDialogDismissed) + context, R.style.HostDialogTheme, this::onHostDialogCreated, this::onHostDialogDismissed) /** The root content view of [hostDialog]. */ private val hostDialogRoot = FrameLayout(context) @@ -208,6 +223,7 @@ private class DialogLaunchAnimation( private var dismissRequested = false private var drawHostDialog = false var ignoreNextCallToHide = false + var exitAnimationDisabled = false fun start() { // Show the host (fullscreen) dialog, to which we will add the stolen dialog view. @@ -236,10 +252,6 @@ private class DialogLaunchAnimation( WindowManager.LayoutParams.MATCH_PARENT ) - // The host dialog animation is a translation of 0px so that it is shown directly. The - // translation lasts X ms, so that the scrim fades in during that amount of time. - window.attributes.windowAnimations = R.style.Animation_LaunchHostDialog - // Prevent the host dialog from drawing until the animation starts. hostDialogRoot.viewTreeObserver.addOnPreDrawListener( object : ViewTreeObserver.OnPreDrawListener { @@ -318,9 +330,11 @@ private class DialogLaunchAnimation( (dialogView.parent as? ViewGroup)?.removeView(dialogView) hostDialogRoot.addView( dialogView, + + // We give it the size of its original dialog window. FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, + originalDialog.window.attributes.width, + originalDialog.window.attributes.height, Gravity.CENTER ) ) @@ -496,6 +510,10 @@ private class DialogLaunchAnimation( } private fun shouldAnimateDialogIntoView(): Boolean { + if (exitAnimationDisabled) { + return false + } + // The touch surface should be invisible by now, if it's not then something else changed its // visibility and we probably don't want to run the animation. if (touchSurface.visibility != View.INVISIBLE) { diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_background.xml deleted file mode 100644 index 3ceb0f6ac06ae..0000000000000 --- a/packages/SystemUI/res/drawable/media_output_dialog_background.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml index b841419c1c752..6743fca61441d 100644 --- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml +++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml @@ -19,7 +19,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/internet_connectivity_dialog" - android:layout_width="@dimen/internet_dialog_list_max_width" + android:layout_width="@dimen/large_dialog_width" android:layout_height="@dimen/internet_dialog_list_max_height" android:background="@drawable/internet_dialog_rounded_top_corner_background" android:orientation="vertical"> diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml index d996cee4b39e5..b33889469f489 100644 --- a/packages/SystemUI/res/layout/media_output_dialog.xml +++ b/packages/SystemUI/res/layout/media_output_dialog.xml @@ -18,7 +18,7 @@ diff --git a/packages/SystemUI/res/layout/media_view.xml b/packages/SystemUI/res/layout/media_view.xml index 566cd25e86a5a..b546a9cbe90e6 100644 --- a/packages/SystemUI/res/layout/media_view.xml +++ b/packages/SystemUI/res/layout/media_view.xml @@ -134,6 +134,7 @@ android:background="@drawable/qs_media_light_source" android:forceHasOverlappingRendering="false"> 0dp - - 624dp + + 624dp diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 26ee5ea7f914a..3ec8b06bdb2c8 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1600,7 +1600,9 @@ 12dp 646dp - @dimen/match_parent + + + @dimen/match_parent 24dp diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index d25474255bfc5..702e0da4cffa6 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -331,9 +331,6 @@ - - @@ -434,10 +431,6 @@ true - -