Merge changes from topics "dialog-launch", "launch-animator" into sc-v2-dev

* changes:
  Animate media dialogs
  Introduce DialogLaunchAnimator (1/2)
  Split ActivityLaunchAnimator (1/2)
This commit is contained in:
Jordan Demeulenaere
2021-09-29 11:58:24 +00:00
committed by Android (Google) Code Review
44 changed files with 1621 additions and 430 deletions

View File

@@ -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)}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<!-- The enter animation of the host dialog is a translation of 0px that lasts 500ms so that the -->
<!-- host dialog is directly visible but the dim background still takes 500ms to fade in. -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="0"
android:toXDelta="0"
android:duration="500" />

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<alpha
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/decelerate_cubic"
android:duration="150"
android:fromAlpha="1.0"
android:toAlpha="0.0" />

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<item type="id" name="launch_animation_running"/>
</resources>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<style name="HostDialogTheme">
<item name="android:windowAnimationStyle">@style/Animation.HostDialog</item>
<item name="android:windowIsFloating">false</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="Animation.HostDialog" parent="@android:style/Animation">
<item name="android:windowEnterAnimation">@anim/launch_host_dialog_enter</item>
<item name="android:windowExitAnimation">@anim/launch_host_dialog_exit</item>
</style>
</resources>

View File

@@ -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<out RemoteAnimationTarget>?,
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)
}
}
}

View File

@@ -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
/**

View File

@@ -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<DialogLaunchAnimation>()
/**
* 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<ViewGroup>(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
}
}

View File

@@ -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
}

View File

@@ -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)
}
}
}
}

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2020 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.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android">
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="?android:attr/colorBackground" />
</shape>
</inset>

View File

@@ -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">

View File

@@ -18,7 +18,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/media_output_dialog"
android:layout_width="match_parent"
android:layout_width="@dimen/large_dialog_width"
android:layout_height="wrap_content"
android:orientation="vertical">

View File

@@ -134,6 +134,7 @@
android:background="@drawable/qs_media_light_source"
android:forceHasOverlappingRendering="false">
<LinearLayout
android:id="@+id/media_seamless_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="@dimen/qs_seamless_height"

View File

@@ -104,6 +104,6 @@
<!-- When split shade is used, this panel should be aligned to the top -->
<dimen name="qs_detail_margin_top">0dp</dimen>
<!-- Internet panel related dimensions -->
<dimen name="internet_dialog_list_max_width">624dp</dimen>
<!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) -->
<dimen name="large_dialog_width">624dp</dimen>
</resources>

View File

@@ -1596,7 +1596,9 @@
<!-- Internet panel related dimensions -->
<dimen name="internet_dialog_list_margin">12dp</dimen>
<dimen name="internet_dialog_list_max_height">646dp</dimen>
<dimen name="internet_dialog_list_max_width">@dimen/match_parent</dimen>
<!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) -->
<dimen name="large_dialog_width">@dimen/match_parent</dimen>
<!-- Signal icon in internet dialog -->
<dimen name="signal_strength_icon_size">24dp</dimen>

View File

@@ -331,9 +331,6 @@
<style name="Animation.ShutdownUi" parent="@android:style/Animation.Toast">
</style>
<style name="Animation.MediaOutputDialog" parent="@android:style/Animation.InputMethod">
</style>
<!-- Standard animations for hiding and showing the status bar. -->
<style name="Animation.StatusBar">
</style>
@@ -436,10 +433,6 @@
<item name="android:windowCloseOnTouchOutside">true</item>
</style>
<style name="Theme.SystemUI.Dialog.MediaOutput">
<item name="android:windowBackground">@drawable/media_output_dialog_background</item>
</style>
<style name="QSBorderlessButton">
<item name="android:padding">12dp</item>
<item name="android:background">@drawable/qs_btn_borderless_rect</item>

View File

@@ -364,9 +364,9 @@ public class MediaControlPanel {
seamlessView.setVisibility(View.VISIBLE);
setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
seamlessView.setOnClickListener(v -> {
mMediaOutputDialogFactory.create(data.getPackageName(), true);
});
seamlessView.setOnClickListener(
v -> mMediaOutputDialogFactory.create(data.getPackageName(), true,
mPlayerViewHolder.getSeamlessButton()));
ImageView iconView = mPlayerViewHolder.getSeamlessIcon();
TextView deviceName = mPlayerViewHolder.getSeamlessText();

View File

@@ -43,6 +43,7 @@ class PlayerViewHolder private constructor(itemView: View) {
val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image)
val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text)
val seamlessButton = itemView.requireViewById<View>(R.id.media_seamless_button)
// Seek bar
val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)

View File

@@ -45,11 +45,14 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
private static final String TAG = "MediaOutputAdapter";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final MediaOutputDialog mMediaOutputDialog;
private ViewGroup mConnectedItem;
private boolean mIncludeDynamicGroup;
public MediaOutputAdapter(MediaOutputController controller) {
public MediaOutputAdapter(MediaOutputController controller,
MediaOutputDialog mediaOutputDialog) {
super(controller);
mMediaOutputDialog = mediaOutputDialog;
}
@Override
@@ -136,7 +139,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
mDivider.setTransitionAlpha(1);
mAddIcon.setVisibility(View.VISIBLE);
mAddIcon.setTransitionAlpha(1);
mAddIcon.setOnClickListener(v -> onEndItemClick());
mAddIcon.setOnClickListener(this::onEndItemClick);
} else {
// Init non-active device layout
mDivider.setVisibility(View.GONE);
@@ -197,7 +200,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
mDivider.setTransitionAlpha(1);
mAddIcon.setVisibility(View.VISIBLE);
mAddIcon.setTransitionAlpha(1);
mAddIcon.setOnClickListener(v -> onEndItemClick());
mAddIcon.setOnClickListener(this::onEndItemClick);
} else {
mDivider.setVisibility(View.GONE);
mAddIcon.setVisibility(View.GONE);
@@ -232,8 +235,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
}
}
private void onEndItemClick() {
mController.launchMediaOutputGroupDialog();
private void onEndItemClick(View view) {
mController.launchMediaOutputGroupDialog(mMediaOutputDialog.getDialogView());
}
}
}

View File

@@ -82,7 +82,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
};
public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) {
super(context, R.style.Theme_SystemUI_Dialog_MediaOutput);
super(context);
mContext = context;
mMediaOutputController = mediaOutputController;
mLayoutManager = new LinearLayoutManager(mContext);
@@ -97,15 +97,15 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null);
final Window window = getWindow();
final WindowManager.LayoutParams lp = window.getAttributes();
lp.gravity = Gravity.BOTTOM;
lp.gravity = Gravity.CENTER;
// Config insets to make sure the layout is above the navigation bar
lp.setFitInsetsTypes(statusBars() | navigationBars());
lp.setFitInsetsSides(WindowInsets.Side.all());
lp.setFitInsetsIgnoringVisibility(true);
window.setAttributes(lp);
window.setContentView(mDialogView);
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
window.setWindowAnimations(R.style.Animation_MediaOutputDialog);
window.setLayout(mContext.getResources().getDimensionPixelSize(R.dimen.large_dialog_width),
ViewGroup.LayoutParams.WRAP_CONTENT);
mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
@@ -229,4 +229,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
void onHeaderIconClick() {
}
View getDialogView() {
return mDialogView;
}
}

View File

@@ -32,6 +32,7 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@@ -48,6 +49,7 @@ import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.media.MediaOutputConstants;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.R;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -73,6 +75,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
private final MediaSessionManager mMediaSessionManager;
private final ShadeController mShadeController;
private final ActivityStarter mActivityStarter;
private final DialogLaunchAnimator mDialogLaunchAnimator;
private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
private final boolean mAboveStatusbar;
private final NotificationEntryManager mNotificationEntryManager;
@@ -82,6 +85,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
private MediaController mMediaController;
@VisibleForTesting
Callback mCallback;
Callback mPreviousCallback;
@VisibleForTesting
LocalMediaManager mLocalMediaManager;
@@ -92,7 +96,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
public MediaOutputController(@NonNull Context context, String packageName,
boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager
lbm, ShadeController shadeController, ActivityStarter starter,
NotificationEntryManager notificationEntryManager, UiEventLogger uiEventLogger) {
NotificationEntryManager notificationEntryManager, UiEventLogger uiEventLogger,
DialogLaunchAnimator dialogLaunchAnimator) {
mContext = context;
mPackageName = packageName;
mMediaSessionManager = mediaSessionManager;
@@ -104,6 +109,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
mUiEventLogger = uiEventLogger;
mDialogLaunchAnimator = dialogLaunchAnimator;
}
void start(@NonNull Callback cb) {
@@ -129,7 +135,19 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
}
return;
}
if (mPreviousCallback != null) {
Log.w(TAG,
"Callback started when mPreviousCallback is not null, which is unexpected");
mPreviousCallback.dismissDialog();
}
// If we start the output group dialog when the output dialog is shown, we need to keep a
// reference to the output dialog to set it back as the callback once we dismiss the output
// group dialog.
mPreviousCallback = mCallback;
mCallback = cb;
mLocalMediaManager.unregisterCallback(this);
mLocalMediaManager.stopScan();
mLocalMediaManager.registerCallback(this);
@@ -145,6 +163,15 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
mLocalMediaManager.stopScan();
}
mMediaDevices.clear();
// If there was a previous callback, i.e. we just dismissed the output group dialog and are
// now back on the output dialog, then we reset the callback to its previous value.
mCallback = null;
Callback previous = mPreviousCallback;
mPreviousCallback = null;
if (previous != null) {
start(previous);
}
}
@Override
@@ -436,6 +463,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
}
void launchBluetoothPairing() {
// Dismissing a dialog into its touch surface and starting an activity at the same time
// looks bad, so let's make sure the dialog just fades out quickly.
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
mCallback.dismissDialog();
final ActivityStarter.OnDismissAction postKeyguardAction = () -> {
mContext.sendBroadcast(new Intent()
@@ -447,14 +478,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true);
}
void launchMediaOutputDialog() {
mCallback.dismissDialog();
new MediaOutputDialog(mContext, mAboveStatusbar, this, mUiEventLogger);
}
void launchMediaOutputGroupDialog() {
mCallback.dismissDialog();
new MediaOutputGroupDialog(mContext, mAboveStatusbar, this);
void launchMediaOutputGroupDialog(View mediaOutputDialog) {
// We show the output group dialog from the output dialog.
MediaOutputGroupDialog dialog = new MediaOutputGroupDialog(mContext, mAboveStatusbar, this);
mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog);
}
boolean isActiveRemoteDevice(@NonNull MediaDevice device) {

View File

@@ -40,11 +40,10 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
mediaOutputController, UiEventLogger uiEventLogger) {
super(context, mediaOutputController);
mUiEventLogger = uiEventLogger;
mAdapter = new MediaOutputAdapter(mMediaOutputController);
mAdapter = new MediaOutputAdapter(mMediaOutputController, this);
if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}
show();
}
@Override

View File

@@ -18,8 +18,10 @@ package com.android.systemui.media.dialog
import android.content.Context
import android.media.session.MediaSessionManager
import android.view.View
import com.android.internal.logging.UiEventLogger
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.notification.NotificationEntryManager
import com.android.systemui.statusbar.phone.ShadeController
@@ -35,19 +37,29 @@ class MediaOutputDialogFactory @Inject constructor(
private val shadeController: ShadeController,
private val starter: ActivityStarter,
private val notificationEntryManager: NotificationEntryManager,
private val uiEventLogger: UiEventLogger
private val uiEventLogger: UiEventLogger,
private val dialogLaunchAnimator: DialogLaunchAnimator
) {
companion object {
var mediaOutputDialog: MediaOutputDialog? = null
}
/** Creates a [MediaOutputDialog] for the given package. */
fun create(packageName: String, aboveStatusBar: Boolean) {
fun create(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
// Dismiss the previous dialog, if any.
mediaOutputDialog?.dismiss()
mediaOutputDialog = MediaOutputController(context, packageName, aboveStatusBar,
mediaSessionManager, lbm, shadeController, starter, notificationEntryManager,
uiEventLogger).run {
MediaOutputDialog(context, aboveStatusBar, this, uiEventLogger)
val controller = MediaOutputController(context, packageName, aboveStatusBar,
mediaSessionManager, lbm, shadeController, starter, notificationEntryManager,
uiEventLogger, dialogLaunchAnimator)
val dialog = MediaOutputDialog(context, aboveStatusBar, controller, uiEventLogger)
mediaOutputDialog = dialog
// Show the dialog.
if (view != null) {
dialogLaunchAnimator.showFromView(dialog, view)
} else {
dialog.show()
}
}

View File

@@ -38,7 +38,6 @@ public class MediaOutputGroupDialog extends MediaOutputBaseDialog {
if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}
show();
}
@Override
@@ -83,6 +82,8 @@ public class MediaOutputGroupDialog extends MediaOutputBaseDialog {
@Override
void onHeaderIconClick() {
mMediaOutputController.launchMediaOutputDialog();
// Given that we launched the media output group dialog from the media output dialog,
// dismissing this dialog will show the media output dialog again.
dismiss();
}
}

View File

@@ -206,7 +206,7 @@ public class InternetDialog extends SystemUIDialog implements
window.setContentView(mDialogView);
//Only fix the width for large screen or tablet.
window.setLayout(mContext.getResources().getDimensionPixelSize(
R.dimen.internet_dialog_list_max_width), ViewGroup.LayoutParams.WRAP_CONTENT);
R.dimen.large_dialog_width), ViewGroup.LayoutParams.WRAP_CONTENT);
window.setWindowAnimations(R.style.Animation_InternetDialog);
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
window.addFlags(FLAG_LAYOUT_NO_LIMITS);

View File

@@ -22,6 +22,9 @@ 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.DialogLaunchAnimator;
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;
@@ -62,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;
@@ -261,4 +265,29 @@ 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);
}
/**
*/
@Provides
@SysUISingleton
static DialogLaunchAnimator provideDialogLaunchAnimator(Context context,
LaunchAnimator launchAnimator) {
return new DialogLaunchAnimator(context, launchAnimator, new SystemUIHostDialogProvider());
}
}

View File

@@ -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)
}
}

View File

@@ -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
) {

View File

@@ -103,8 +103,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;
@@ -212,7 +212,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;
@@ -3594,7 +3594,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;

View File

@@ -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;
@@ -669,7 +670,8 @@ public class StatusBar extends SystemUI implements
private final SysuiStatusBarStateController mStatusBarStateController;
private HeadsUpAppearanceController mHeadsUpAppearanceController;
private ActivityLaunchAnimator mActivityLaunchAnimator;
private final ActivityLaunchAnimator mActivityLaunchAnimator;
private final DialogLaunchAnimator mDialogLaunchAnimator;
private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
protected StatusBarNotificationPresenter mPresenter;
private NotificationActivityStarter mNotificationActivityStarter;
@@ -792,7 +794,9 @@ public class StatusBar extends SystemUI implements
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
Optional<StartingSurface> startingSurfaceOptional,
TunerService tunerService,
DumpManager dumpManager) {
DumpManager dumpManager,
ActivityLaunchAnimator activityLaunchAnimator,
DialogLaunchAnimator dialogLaunchAnimator) {
super(context);
mNotificationsController = notificationsController;
mLightBarController = lightBarController;
@@ -900,6 +904,8 @@ 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);
@@ -1467,7 +1473,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(),
@@ -2549,7 +2555,8 @@ public class StatusBar extends SystemUI implements
animationController != null && !willLaunchResolverActivity && shouldAnimateLaunch(
true /* isActivityIntent */);
ActivityLaunchAnimator.Controller animController =
animate ? wrapAnimationController(animationController, dismissShade) : null;
animationController != null ? wrapAnimationController(animationController,
dismissShade) : null;
// If we animate, we will dismiss the shade only once the animation is done. This is taken
// care of by the StatusBarLaunchAnimationController.
@@ -4426,6 +4433,8 @@ public class StatusBar extends SystemUI implements
&& !mBiometricUnlockController.isWakeAndUnlock()) {
mLightRevealScrim.setRevealAmount(1f - linear);
}
mDialogLaunchAnimator.onDozeAmountChanged(linear);
}
@Override

View File

@@ -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
) {

View File

@@ -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<DialogListener> 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);
}

View File

@@ -0,0 +1,36 @@
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,
theme: Int,
onCreateCallback: () -> Unit,
dismissOverride: (() -> Unit) -> Unit
): Dialog {
return SystemUIHostDialog(context, theme, onCreateCallback, dismissOverride)
}
private class SystemUIHostDialog(
context: Context,
theme: Int,
private val onCreateCallback: () -> Unit,
private val dismissOverride: (() -> Unit) -> Unit
) : SystemUIDialog(context, theme) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onCreateCallback()
}
override fun dismiss() {
dismissOverride {
super.dismiss()
}
}
}
}

View File

@@ -28,6 +28,8 @@ 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.animation.DialogLaunchAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingCollector;
@@ -235,7 +237,9 @@ public interface StatusBarPhoneModule {
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
Optional<StartingSurface> startingSurfaceOptional,
TunerService tunerService,
DumpManager dumpManager) {
DumpManager dumpManager,
ActivityLaunchAnimator activityLaunchAnimator,
DialogLaunchAnimator dialogLaunchAnimator) {
return new StatusBar(
context,
notificationsController,
@@ -333,6 +337,8 @@ public interface StatusBarPhoneModule {
unlockedScreenOffAnimationController,
startingSurfaceOptional,
tunerService,
dumpManager);
dumpManager,
activityLaunchAnimator,
dialogLaunchAnimator);
}
}

View File

@@ -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
) {

View File

@@ -0,0 +1,186 @@
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<ViewGroup>(android.R.id.content)
assertEquals(0, dialog.findViewById<ViewGroup>(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 <T : Any> 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,
theme: Int,
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<DialogListener>()
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()
}
}
}
}

View File

@@ -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)

View File

@@ -54,6 +54,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
// Mock
private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
private MediaOutputDialog mMediaOutputDialog = mock(MediaOutputDialog.class);
private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
private Icon mIcon = mock(Icon.class);
@@ -65,7 +66,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
@Before
public void setUp() {
mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController, mMediaOutputDialog);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);

View File

@@ -39,6 +39,7 @@ import com.android.internal.logging.UiEventLogger;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.phone.ShadeController;
@@ -63,6 +64,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
private NotificationEntryManager mNotificationEntryManager =
mock(NotificationEntryManager.class);
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
private MediaOutputController mMediaOutputController;
@@ -75,7 +77,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
public void setUp() {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger);
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext,
mMediaOutputController);
mMediaOutputBaseDialogImpl.onCreate(new Bundle());

View File

@@ -49,6 +49,7 @@ import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -91,6 +92,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
private NotificationEntryManager mNotificationEntryManager =
mock(NotificationEntryManager.class);
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
private Context mSpyContext;
private MediaOutputController mMediaOutputController;
@@ -113,7 +115,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger);
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
MediaDescription.Builder builder = new MediaDescription.Builder();
@@ -157,7 +159,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
public void start_withoutPackageName_verifyMediaControllerInit() {
mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger);
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mMediaOutputController.start(mCb);
@@ -178,7 +180,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
public void stop_withoutPackageName_verifyMediaControllerDeinit() {
mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger);
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mMediaOutputController.start(mCb);
@@ -449,7 +451,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
public void getNotificationLargeIcon_withoutPackageName_returnsNull() {
mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger);
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
assertThat(mMediaOutputController.getNotificationIcon()).isNull();
}

View File

@@ -36,6 +36,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.phone.ShadeController;
@@ -65,6 +66,7 @@ public class MediaOutputDialogTest extends SysuiTestCase {
private final NotificationEntryManager mNotificationEntryManager =
mock(NotificationEntryManager.class);
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
private MediaOutputDialog mMediaOutputDialog;
private MediaOutputController mMediaOutputController;
@@ -74,10 +76,11 @@ public class MediaOutputDialogTest extends SysuiTestCase {
public void setUp() {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger);
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
mMediaOutputDialog = new MediaOutputDialog(mContext, false,
mMediaOutputController, mUiEventLogger);
mMediaOutputDialog.show();
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice);
when(mMediaDevice.getFeatures()).thenReturn(mFeatures);
@@ -123,6 +126,7 @@ public class MediaOutputDialogTest extends SysuiTestCase {
public void onCreate_ShouldLogVisibility() {
MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false,
mMediaOutputController, mUiEventLogger);
testDialog.show();
testDialog.dismissDialog();

View File

@@ -34,6 +34,7 @@ import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.phone.ShadeController;
@@ -64,6 +65,7 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase {
private NotificationEntryManager mNotificationEntryManager =
mock(NotificationEntryManager.class);
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
private MediaOutputGroupDialog mMediaOutputGroupDialog;
private MediaOutputController mMediaOutputController;
@@ -73,10 +75,11 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase {
public void setUp() {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger);
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false,
mMediaOutputController);
mMediaOutputGroupDialog.show();
when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mMediaDevices);
}

View File

@@ -77,6 +77,8 @@ 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.animation.DialogLaunchAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingCollectorFake;
@@ -274,6 +276,8 @@ public class StatusBarTest extends SysuiTestCase {
@Mock private StartingSurface mStartingSurface;
@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);
@@ -452,7 +456,9 @@ public class StatusBarTest extends SysuiTestCase {
mUnlockedScreenOffAnimationController,
Optional.of(mStartingSurface),
mTunerService,
mock(DumpManager.class));
mock(DumpManager.class),
mActivityLaunchAnimator,
mDialogLaunchAnimator);
when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class),
any(NotificationPanelViewController.class), any(BiometricUnlockController.class),
any(ViewGroup.class), any(KeyguardBypassController.class)))