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/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..ad06c9192bc3e
--- /dev/null
+++ b/packages/SystemUI/animation/res/values/styles.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
\ No newline at end of file
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/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
new file mode 100644
index 0000000000000..c2b36089d0a7b
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -0,0 +1,541 @@
+/*
+ * 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) }
+ }
+
+ /**
+ * 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 {
+ /**
+ * 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,
+ theme: Int,
+ 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, R.style.HostDialogTheme, 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
+ var exitAnimationDisabled = 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
+ )
+
+ // 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,
+
+ // We give it the size of its original dialog window.
+ FrameLayout.LayoutParams(
+ originalDialog.window.attributes.width,
+ originalDialog.window.attributes.height,
+ 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 {
+ 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) {
+ 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/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/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 918635d666faa..c1d7308be5a82 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 70149267e3dd8..8650654ea2884 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1596,7 +1596,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 93d60cce2915e..6594216e4290f 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -331,9 +331,6 @@
-
-
@@ -436,10 +433,6 @@
- true
-
-