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:
committed by
Android (Google) Code Review
commit
3bf72bd3f9
@@ -165,6 +165,10 @@ public final class MathUtils {
|
|||||||
return start + (stop - start) * amount;
|
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
|
* Returns the interpolation scalar (s) that satisfies the equation: {@code value = }{@link
|
||||||
* #lerp}{@code (a, b, s)}
|
* #lerp}{@code (a, b, s)}
|
||||||
|
|||||||
@@ -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" />
|
||||||
@@ -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" />
|
||||||
19
packages/SystemUI/animation/res/values/ids.xml
Normal file
19
packages/SystemUI/animation/res/values/ids.xml
Normal 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>
|
||||||
29
packages/SystemUI/animation/res/values/styles.xml
Normal file
29
packages/SystemUI/animation/res/values/styles.xml
Normal 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>
|
||||||
@@ -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
|
package com.android.systemui.animation
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorListenerAdapter
|
|
||||||
import android.animation.ValueAnimator
|
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.app.ActivityTaskManager
|
import android.app.ActivityTaskManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.TaskInfo
|
import android.app.TaskInfo
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.graphics.PorterDuff
|
|
||||||
import android.graphics.PorterDuffXfermode
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.graphics.drawable.GradientDrawable
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.MathUtils
|
|
||||||
import android.view.IRemoteAnimationFinishedCallback
|
import android.view.IRemoteAnimationFinishedCallback
|
||||||
import android.view.IRemoteAnimationRunner
|
import android.view.IRemoteAnimationRunner
|
||||||
import android.view.RemoteAnimationAdapter
|
import android.view.RemoteAnimationAdapter
|
||||||
@@ -26,7 +34,6 @@ import android.view.SyncRtSurfaceTransactionApplier
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.animation.AnimationUtils
|
|
||||||
import android.view.animation.PathInterpolator
|
import android.view.animation.PathInterpolator
|
||||||
import com.android.internal.annotations.VisibleForTesting
|
import com.android.internal.annotations.VisibleForTesting
|
||||||
import com.android.internal.policy.ScreenDecorationsUtils
|
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
|
* A class that allows activities to be started in a seamless way from a view that is transforming
|
||||||
* nicely into the starting window.
|
* nicely into the starting window.
|
||||||
*/
|
*/
|
||||||
class ActivityLaunchAnimator(
|
class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) {
|
||||||
private val callback: Callback,
|
|
||||||
context: Context
|
|
||||||
) {
|
|
||||||
companion object {
|
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_IN = 266L
|
||||||
private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
|
private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
|
||||||
private const val ANIMATION_DELAY_NAV_FADE_IN =
|
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
|
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_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f)
|
||||||
private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 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,
|
* The callback of this animator. This should be set before any call to
|
||||||
R.interpolator.launch_animation_interpolator_y)
|
* [start(Pending)IntentWithAnimation].
|
||||||
|
*/
|
||||||
/** The interpolator used for the X position. */
|
var callback: Callback? = null
|
||||||
private val animationInterpolatorX = AnimationUtils.loadInterpolator(context,
|
|
||||||
R.interpolator.launch_animation_interpolator_x)
|
|
||||||
|
|
||||||
private val cornerRadii = FloatArray(8)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start an intent and animate the opening window. The intent will be started by running
|
* Start an intent and animate the opening window. The intent will be started by running
|
||||||
@@ -119,6 +97,8 @@ class ActivityLaunchAnimator(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val callback = this.callback ?: throw IllegalStateException(
|
||||||
|
"ActivityLaunchAnimator.callback must be set before using this animator")
|
||||||
val runner = Runner(controller)
|
val runner = Runner(controller)
|
||||||
val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen
|
val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen
|
||||||
|
|
||||||
@@ -126,9 +106,9 @@ class ActivityLaunchAnimator(
|
|||||||
// keyguard with the animation
|
// keyguard with the animation
|
||||||
val animationAdapter = if (!hideKeyguardWithAnimation) {
|
val animationAdapter = if (!hideKeyguardWithAnimation) {
|
||||||
RemoteAnimationAdapter(
|
RemoteAnimationAdapter(
|
||||||
runner,
|
runner,
|
||||||
ANIMATION_DURATION,
|
LaunchAnimator.ANIMATION_DURATION,
|
||||||
ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
|
LaunchAnimator.ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
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
|
// Only animate if the app is not already on top and will be opened, unless we are on the
|
||||||
// keyguard.
|
// keyguard.
|
||||||
val willAnimate =
|
val willAnimate =
|
||||||
launchResult == ActivityManager.START_TASK_TO_FRONT ||
|
launchResult == ActivityManager.START_TASK_TO_FRONT ||
|
||||||
launchResult == ActivityManager.START_SUCCESS ||
|
launchResult == ActivityManager.START_SUCCESS ||
|
||||||
(launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
|
(launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
|
||||||
hideKeyguardWithAnimation)
|
hideKeyguardWithAnimation)
|
||||||
|
|
||||||
Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " +
|
Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " +
|
||||||
"hideKeyguardWithAnimation=$hideKeyguardWithAnimation")
|
"hideKeyguardWithAnimation=$hideKeyguardWithAnimation")
|
||||||
@@ -234,7 +214,7 @@ class ActivityLaunchAnimator(
|
|||||||
*
|
*
|
||||||
* Note that all callbacks (onXXX methods) are all called on the main thread.
|
* Note that all callbacks (onXXX methods) are all called on the main thread.
|
||||||
*/
|
*/
|
||||||
interface Controller {
|
interface Controller : LaunchAnimator.Controller {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Return a [Controller] that will animate and expand [view] into the opening window.
|
* 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
|
* The intent was started. If [willAnimate] is false, nothing else will happen and the
|
||||||
* animation will not be started.
|
* animation will not be started.
|
||||||
*/
|
*/
|
||||||
fun onIntentStarted(willAnimate: Boolean) {}
|
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
|
* 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
|
* this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
|
||||||
@@ -313,66 +252,11 @@ class ActivityLaunchAnimator(
|
|||||||
fun onLaunchAnimationCancelled() {}
|
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
|
@VisibleForTesting
|
||||||
inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
|
inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
|
||||||
private val launchContainer = controller.launchContainer
|
private val launchContainer = controller.launchContainer
|
||||||
private val context = launchContainer.context
|
private val context = launchContainer.context
|
||||||
private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer)
|
private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer)
|
||||||
private var animator: ValueAnimator? = null
|
|
||||||
|
|
||||||
private val matrix = Matrix()
|
private val matrix = Matrix()
|
||||||
private val invertMatrix = Matrix()
|
private val invertMatrix = Matrix()
|
||||||
@@ -380,6 +264,7 @@ class ActivityLaunchAnimator(
|
|||||||
private var windowCropF = RectF()
|
private var windowCropF = RectF()
|
||||||
private var timedOut = false
|
private var timedOut = false
|
||||||
private var cancelled = 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
|
// A timeout to cancel the remote animation if it is not started within X milliseconds after
|
||||||
// the intent was started.
|
// the intent was started.
|
||||||
@@ -429,7 +314,7 @@ class ActivityLaunchAnimator(
|
|||||||
nonApps: Array<out RemoteAnimationTarget>?,
|
nonApps: Array<out RemoteAnimationTarget>?,
|
||||||
iCallback: IRemoteAnimationFinishedCallback?
|
iCallback: IRemoteAnimationFinishedCallback?
|
||||||
) {
|
) {
|
||||||
if (DEBUG) {
|
if (LaunchAnimator.DEBUG) {
|
||||||
Log.d(TAG, "Remote animation started")
|
Log.d(TAG, "Remote animation started")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,36 +334,20 @@ class ActivityLaunchAnimator(
|
|||||||
it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
|
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 windowBounds = window.screenSpaceBounds
|
||||||
val endTop = windowBounds.top
|
val endState = LaunchAnimator.State(
|
||||||
val endBottom = windowBounds.bottom
|
top = windowBounds.top,
|
||||||
val endLeft = windowBounds.left
|
bottom = windowBounds.bottom,
|
||||||
val endRight = windowBounds.right
|
left = windowBounds.left,
|
||||||
val endXCenter = (endLeft + endRight) / 2f
|
right = windowBounds.right
|
||||||
val endWidth = endRight - endLeft
|
)
|
||||||
|
val callback = this@ActivityLaunchAnimator.callback!!
|
||||||
|
val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)
|
||||||
|
|
||||||
// TODO(b/184121838): Ensure that we are launching on the same screen.
|
// TODO(b/184121838): We should somehow get the top and bottom radius of the window
|
||||||
val rootViewLocation = launchContainer.locationOnScreen
|
// instead of recomputing isExpandingFullyAbove here.
|
||||||
val isExpandingFullyAbove = endTop <= rootViewLocation[1] &&
|
val isExpandingFullyAbove =
|
||||||
endBottom >= rootViewLocation[1] + launchContainer.height &&
|
launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState)
|
||||||
endLeft <= rootViewLocation[0] &&
|
|
||||||
endRight >= rootViewLocation[0] + launchContainer.width
|
|
||||||
|
|
||||||
// TODO(b/184121838): We should somehow get the top and bottom radius of the window.
|
|
||||||
val endRadius = if (isExpandingFullyAbove) {
|
val endRadius = if (isExpandingFullyAbove) {
|
||||||
// Most of the time, expanding fully above the root view means expanding in full
|
// Most of the time, expanding fully above the root view means expanding in full
|
||||||
// screen.
|
// screen.
|
||||||
@@ -488,97 +357,40 @@ class ActivityLaunchAnimator(
|
|||||||
// a radius of 0.
|
// a radius of 0.
|
||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
|
endState.topCornerRadius = endRadius
|
||||||
|
endState.bottomCornerRadius = endRadius
|
||||||
|
|
||||||
// We add an extra layer with the same color as the app splash screen background color,
|
// We animate the opening window and delegate the view expansion to [this.controller].
|
||||||
// which is usually the same color of the app background. We first fade in this layer
|
val delegate = this.controller
|
||||||
// to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
|
val controller = object : LaunchAnimator.Controller by delegate {
|
||||||
// launch container and reveal the opening window.
|
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.setBlursDisabledForAppLaunch(true)
|
callback.setBlursDisabledForAppLaunch(true)
|
||||||
controller.onLaunchAnimationStart(isExpandingFullyAbove)
|
delegate.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?) {
|
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Animation ended")
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.setBlursDisabledForAppLaunch(false)
|
callback.setBlursDisabledForAppLaunch(false)
|
||||||
iCallback?.invoke()
|
iCallback?.invoke()
|
||||||
controller.onLaunchAnimationEnd(isExpandingFullyAbove)
|
delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
|
||||||
launchContainerOverlay.remove(windowBackgroundLayer)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
animator.addUpdateListener { animation ->
|
|
||||||
if (cancelled) {
|
|
||||||
return@addUpdateListener
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val linearProgress = animation.animatedFraction
|
override fun onLaunchAnimationProgress(
|
||||||
val progress = animationInterpolator.getInterpolation(linearProgress)
|
state: LaunchAnimator.State,
|
||||||
val xProgress = animationInterpolatorX.getInterpolation(linearProgress)
|
progress: Float,
|
||||||
val xCenter = MathUtils.lerp(startXCenter, endXCenter, xProgress)
|
linearProgress: Float
|
||||||
val halfWidth = lerp(startWidth, endWidth, progress) / 2
|
) {
|
||||||
|
applyStateToWindow(window, state)
|
||||||
state.top = lerp(startTop, endTop, progress).roundToInt()
|
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
|
||||||
state.bottom = lerp(startBottom, endBottom, progress).roundToInt()
|
delegate.onLaunchAnimationProgress(state, progress, linearProgress)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
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 screenBounds = window.screenSpaceBounds
|
||||||
val centerX = (screenBounds.left + screenBounds.right) / 2f
|
val centerX = (screenBounds.left + screenBounds.right) / 2f
|
||||||
val centerY = (screenBounds.top + screenBounds.bottom) / 2f
|
val centerY = (screenBounds.top + screenBounds.bottom) / 2f
|
||||||
@@ -632,48 +444,13 @@ class ActivityLaunchAnimator(
|
|||||||
transactionApplier.scheduleApply(params)
|
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(
|
private fun applyStateToNavigationBar(
|
||||||
navigationBar: RemoteAnimationTarget,
|
navigationBar: RemoteAnimationTarget,
|
||||||
state: State,
|
state: LaunchAnimator.State,
|
||||||
linearProgress: Float
|
linearProgress: Float
|
||||||
) {
|
) {
|
||||||
val fadeInProgress = getProgress(linearProgress, ANIMATION_DELAY_NAV_FADE_IN,
|
val fadeInProgress = LaunchAnimator.getProgress(linearProgress,
|
||||||
ANIMATION_DURATION_NAV_FADE_OUT)
|
ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)
|
||||||
|
|
||||||
val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
|
val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
|
||||||
if (fadeInProgress > 0) {
|
if (fadeInProgress > 0) {
|
||||||
@@ -682,13 +459,13 @@ class ActivityLaunchAnimator(
|
|||||||
0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat())
|
0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat())
|
||||||
windowCrop.set(state.left, 0, state.right, state.height)
|
windowCrop.set(state.left, 0, state.right, state.height)
|
||||||
params
|
params
|
||||||
.withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
|
.withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
|
||||||
.withMatrix(matrix)
|
.withMatrix(matrix)
|
||||||
.withWindowCrop(windowCrop)
|
.withWindowCrop(windowCrop)
|
||||||
.withVisibility(true)
|
.withVisibility(true)
|
||||||
} else {
|
} else {
|
||||||
val fadeOutProgress = getProgress(linearProgress, 0,
|
val fadeOutProgress = LaunchAnimator.getProgress(linearProgress, 0,
|
||||||
ANIMATION_DURATION_NAV_FADE_OUT)
|
ANIMATION_DURATION_NAV_FADE_OUT)
|
||||||
params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
|
params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,7 +491,7 @@ class ActivityLaunchAnimator(
|
|||||||
cancelled = true
|
cancelled = true
|
||||||
removeTimeout()
|
removeTimeout()
|
||||||
context.mainExecutor.execute {
|
context.mainExecutor.execute {
|
||||||
animator?.cancel()
|
animation?.cancel()
|
||||||
controller.onLaunchAnimationCancelled()
|
controller.onLaunchAnimationCancelled()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -726,9 +503,5 @@ class ActivityLaunchAnimator(
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun lerp(start: Int, stop: Int, amount: Float): Float {
|
|
||||||
return MathUtils.lerp(start.toFloat(), stop.toFloat(), amount)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
package com.android.systemui.animation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
package com.android.systemui.animation
|
||||||
|
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.ColorFilter
|
import android.graphics.ColorFilter
|
||||||
|
import android.graphics.Insets
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.graphics.PixelFormat
|
import android.graphics.PixelFormat
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
@@ -42,6 +59,7 @@ open class GhostedViewLaunchAnimatorController(
|
|||||||
override var launchContainer = ghostedView.rootView as ViewGroup
|
override var launchContainer = ghostedView.rootView as ViewGroup
|
||||||
private val launchContainerOverlay: ViewGroupOverlay
|
private val launchContainerOverlay: ViewGroupOverlay
|
||||||
get() = launchContainer.overlay
|
get() = launchContainer.overlay
|
||||||
|
private val launchContainerLocation = IntArray(2)
|
||||||
|
|
||||||
/** The ghost view that is drawn and animated instead of the ghosted view. */
|
/** The ghost view that is drawn and animated instead of the ghosted view. */
|
||||||
private var ghostView: GhostView? = null
|
private var ghostView: GhostView? = null
|
||||||
@@ -59,8 +77,12 @@ open class GhostedViewLaunchAnimatorController(
|
|||||||
* [backgroundView].
|
* [backgroundView].
|
||||||
*/
|
*/
|
||||||
private var backgroundDrawable: WrappedDrawable? = null
|
private var backgroundDrawable: WrappedDrawable? = null
|
||||||
|
private val backgroundInsets by lazy { getBackground()?.opticalInsets ?: Insets.NONE }
|
||||||
private var startBackgroundAlpha: Int = 0xFF
|
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
|
* 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
|
* 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
|
return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createAnimatorState(): ActivityLaunchAnimator.State {
|
override fun createAnimatorState(): LaunchAnimator.State {
|
||||||
val location = ghostedView.locationOnScreen
|
val state = LaunchAnimator.State(
|
||||||
return ActivityLaunchAnimator.State(
|
|
||||||
top = location[1],
|
|
||||||
bottom = location[1] + ghostedView.height,
|
|
||||||
left = location[0],
|
|
||||||
right = location[0] + ghostedView.width,
|
|
||||||
topCornerRadius = getCurrentTopCornerRadius(),
|
topCornerRadius = getCurrentTopCornerRadius(),
|
||||||
bottomCornerRadius = getCurrentBottomCornerRadius()
|
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) {
|
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
|
||||||
@@ -144,7 +174,7 @@ open class GhostedViewLaunchAnimatorController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onLaunchAnimationProgress(
|
override fun onLaunchAnimationProgress(
|
||||||
state: ActivityLaunchAnimator.State,
|
state: LaunchAnimator.State,
|
||||||
progress: Float,
|
progress: Float,
|
||||||
linearProgress: Float
|
linearProgress: Float
|
||||||
) {
|
) {
|
||||||
@@ -162,19 +192,47 @@ open class GhostedViewLaunchAnimatorController(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val scale = min(state.widthRatio, state.heightRatio)
|
// The ghost and backgrounds views were made invisible earlier. That can for instance happen
|
||||||
ghostViewMatrix.setValues(initialGhostViewMatrixValues)
|
// when animating a dialog into a view.
|
||||||
ghostViewMatrix.postScale(scale, scale, state.startCenterX, state.startCenterY)
|
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(
|
ghostViewMatrix.postTranslate(
|
||||||
(state.leftChange + state.rightChange) / 2f,
|
(leftChange + rightChange) / 2f,
|
||||||
(state.topChange + state.bottomChange) / 2f
|
(topChange + bottomChange) / 2f
|
||||||
)
|
)
|
||||||
ghostView.animationMatrix = ghostViewMatrix
|
ghostView.animationMatrix = ghostViewMatrix
|
||||||
|
|
||||||
backgroundView.top = state.top
|
// We need to take into account the background insets for the background position.
|
||||||
backgroundView.bottom = state.bottom
|
val insets = backgroundInsets
|
||||||
backgroundView.left = state.left
|
val topWithInsets = state.top - insets.top
|
||||||
backgroundView.right = state.right
|
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!!
|
val backgroundDrawable = backgroundDrawable!!
|
||||||
backgroundDrawable.wrapped?.let {
|
backgroundDrawable.wrapped?.let {
|
||||||
@@ -207,7 +265,7 @@ open class GhostedViewLaunchAnimatorController(
|
|||||||
* [drawable] is a [LayerDrawable], this will return the first layer that is a
|
* [drawable] is a [LayerDrawable], this will return the first layer that is a
|
||||||
* [GradientDrawable].
|
* [GradientDrawable].
|
||||||
*/
|
*/
|
||||||
private fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
|
fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
|
||||||
if (drawable is GradientDrawable) {
|
if (drawable is GradientDrawable) {
|
||||||
return drawable
|
return drawable
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||||
android:id="@+id/internet_connectivity_dialog"
|
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:layout_height="@dimen/internet_dialog_list_max_height"
|
||||||
android:background="@drawable/internet_dialog_rounded_top_corner_background"
|
android:background="@drawable/internet_dialog_rounded_top_corner_background"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/media_output_dialog"
|
android:id="@+id/media_output_dialog"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="@dimen/large_dialog_width"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,7 @@
|
|||||||
android:background="@drawable/qs_media_light_source"
|
android:background="@drawable/qs_media_light_source"
|
||||||
android:forceHasOverlappingRendering="false">
|
android:forceHasOverlappingRendering="false">
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/media_seamless_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minHeight="@dimen/qs_seamless_height"
|
android:minHeight="@dimen/qs_seamless_height"
|
||||||
|
|||||||
@@ -104,6 +104,6 @@
|
|||||||
<!-- When split shade is used, this panel should be aligned to the top -->
|
<!-- When split shade is used, this panel should be aligned to the top -->
|
||||||
<dimen name="qs_detail_margin_top">0dp</dimen>
|
<dimen name="qs_detail_margin_top">0dp</dimen>
|
||||||
|
|
||||||
<!-- Internet panel related dimensions -->
|
<!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) -->
|
||||||
<dimen name="internet_dialog_list_max_width">624dp</dimen>
|
<dimen name="large_dialog_width">624dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1596,7 +1596,9 @@
|
|||||||
<!-- Internet panel related dimensions -->
|
<!-- Internet panel related dimensions -->
|
||||||
<dimen name="internet_dialog_list_margin">12dp</dimen>
|
<dimen name="internet_dialog_list_margin">12dp</dimen>
|
||||||
<dimen name="internet_dialog_list_max_height">646dp</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 -->
|
<!-- Signal icon in internet dialog -->
|
||||||
<dimen name="signal_strength_icon_size">24dp</dimen>
|
<dimen name="signal_strength_icon_size">24dp</dimen>
|
||||||
|
|||||||
@@ -331,9 +331,6 @@
|
|||||||
<style name="Animation.ShutdownUi" parent="@android:style/Animation.Toast">
|
<style name="Animation.ShutdownUi" parent="@android:style/Animation.Toast">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Animation.MediaOutputDialog" parent="@android:style/Animation.InputMethod">
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- Standard animations for hiding and showing the status bar. -->
|
<!-- Standard animations for hiding and showing the status bar. -->
|
||||||
<style name="Animation.StatusBar">
|
<style name="Animation.StatusBar">
|
||||||
</style>
|
</style>
|
||||||
@@ -436,10 +433,6 @@
|
|||||||
<item name="android:windowCloseOnTouchOutside">true</item>
|
<item name="android:windowCloseOnTouchOutside">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.SystemUI.Dialog.MediaOutput">
|
|
||||||
<item name="android:windowBackground">@drawable/media_output_dialog_background</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="QSBorderlessButton">
|
<style name="QSBorderlessButton">
|
||||||
<item name="android:padding">12dp</item>
|
<item name="android:padding">12dp</item>
|
||||||
<item name="android:background">@drawable/qs_btn_borderless_rect</item>
|
<item name="android:background">@drawable/qs_btn_borderless_rect</item>
|
||||||
|
|||||||
@@ -364,9 +364,9 @@ public class MediaControlPanel {
|
|||||||
seamlessView.setVisibility(View.VISIBLE);
|
seamlessView.setVisibility(View.VISIBLE);
|
||||||
setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
|
setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
|
||||||
setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
|
setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
|
||||||
seamlessView.setOnClickListener(v -> {
|
seamlessView.setOnClickListener(
|
||||||
mMediaOutputDialogFactory.create(data.getPackageName(), true);
|
v -> mMediaOutputDialogFactory.create(data.getPackageName(), true,
|
||||||
});
|
mPlayerViewHolder.getSeamlessButton()));
|
||||||
|
|
||||||
ImageView iconView = mPlayerViewHolder.getSeamlessIcon();
|
ImageView iconView = mPlayerViewHolder.getSeamlessIcon();
|
||||||
TextView deviceName = mPlayerViewHolder.getSeamlessText();
|
TextView deviceName = mPlayerViewHolder.getSeamlessText();
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class PlayerViewHolder private constructor(itemView: View) {
|
|||||||
val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
|
val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
|
||||||
val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image)
|
val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image)
|
||||||
val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text)
|
val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text)
|
||||||
|
val seamlessButton = itemView.requireViewById<View>(R.id.media_seamless_button)
|
||||||
|
|
||||||
// Seek bar
|
// Seek bar
|
||||||
val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)
|
val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)
|
||||||
|
|||||||
@@ -45,11 +45,14 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
|
|||||||
private static final String TAG = "MediaOutputAdapter";
|
private static final String TAG = "MediaOutputAdapter";
|
||||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||||
|
|
||||||
|
private final MediaOutputDialog mMediaOutputDialog;
|
||||||
private ViewGroup mConnectedItem;
|
private ViewGroup mConnectedItem;
|
||||||
private boolean mIncludeDynamicGroup;
|
private boolean mIncludeDynamicGroup;
|
||||||
|
|
||||||
public MediaOutputAdapter(MediaOutputController controller) {
|
public MediaOutputAdapter(MediaOutputController controller,
|
||||||
|
MediaOutputDialog mediaOutputDialog) {
|
||||||
super(controller);
|
super(controller);
|
||||||
|
mMediaOutputDialog = mediaOutputDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -136,7 +139,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
|
|||||||
mDivider.setTransitionAlpha(1);
|
mDivider.setTransitionAlpha(1);
|
||||||
mAddIcon.setVisibility(View.VISIBLE);
|
mAddIcon.setVisibility(View.VISIBLE);
|
||||||
mAddIcon.setTransitionAlpha(1);
|
mAddIcon.setTransitionAlpha(1);
|
||||||
mAddIcon.setOnClickListener(v -> onEndItemClick());
|
mAddIcon.setOnClickListener(this::onEndItemClick);
|
||||||
} else {
|
} else {
|
||||||
// Init non-active device layout
|
// Init non-active device layout
|
||||||
mDivider.setVisibility(View.GONE);
|
mDivider.setVisibility(View.GONE);
|
||||||
@@ -197,7 +200,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
|
|||||||
mDivider.setTransitionAlpha(1);
|
mDivider.setTransitionAlpha(1);
|
||||||
mAddIcon.setVisibility(View.VISIBLE);
|
mAddIcon.setVisibility(View.VISIBLE);
|
||||||
mAddIcon.setTransitionAlpha(1);
|
mAddIcon.setTransitionAlpha(1);
|
||||||
mAddIcon.setOnClickListener(v -> onEndItemClick());
|
mAddIcon.setOnClickListener(this::onEndItemClick);
|
||||||
} else {
|
} else {
|
||||||
mDivider.setVisibility(View.GONE);
|
mDivider.setVisibility(View.GONE);
|
||||||
mAddIcon.setVisibility(View.GONE);
|
mAddIcon.setVisibility(View.GONE);
|
||||||
@@ -232,8 +235,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onEndItemClick() {
|
private void onEndItemClick(View view) {
|
||||||
mController.launchMediaOutputGroupDialog();
|
mController.launchMediaOutputGroupDialog(mMediaOutputDialog.getDialogView());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
|
|||||||
};
|
};
|
||||||
|
|
||||||
public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) {
|
public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) {
|
||||||
super(context, R.style.Theme_SystemUI_Dialog_MediaOutput);
|
super(context);
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mMediaOutputController = mediaOutputController;
|
mMediaOutputController = mediaOutputController;
|
||||||
mLayoutManager = new LinearLayoutManager(mContext);
|
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);
|
mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null);
|
||||||
final Window window = getWindow();
|
final Window window = getWindow();
|
||||||
final WindowManager.LayoutParams lp = window.getAttributes();
|
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
|
// Config insets to make sure the layout is above the navigation bar
|
||||||
lp.setFitInsetsTypes(statusBars() | navigationBars());
|
lp.setFitInsetsTypes(statusBars() | navigationBars());
|
||||||
lp.setFitInsetsSides(WindowInsets.Side.all());
|
lp.setFitInsetsSides(WindowInsets.Side.all());
|
||||||
lp.setFitInsetsIgnoringVisibility(true);
|
lp.setFitInsetsIgnoringVisibility(true);
|
||||||
window.setAttributes(lp);
|
window.setAttributes(lp);
|
||||||
window.setContentView(mDialogView);
|
window.setContentView(mDialogView);
|
||||||
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
window.setLayout(mContext.getResources().getDimensionPixelSize(R.dimen.large_dialog_width),
|
||||||
window.setWindowAnimations(R.style.Animation_MediaOutputDialog);
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
|
||||||
mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
|
mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
|
||||||
mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
|
mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
|
||||||
@@ -229,4 +229,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
|
|||||||
|
|
||||||
void onHeaderIconClick() {
|
void onHeaderIconClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
View getDialogView() {
|
||||||
|
return mDialogView;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import android.os.UserHandle;
|
|||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
@@ -48,6 +49,7 @@ import com.android.settingslib.media.MediaDevice;
|
|||||||
import com.android.settingslib.media.MediaOutputConstants;
|
import com.android.settingslib.media.MediaOutputConstants;
|
||||||
import com.android.settingslib.utils.ThreadUtils;
|
import com.android.settingslib.utils.ThreadUtils;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||||
import com.android.systemui.plugins.ActivityStarter;
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.statusbar.notification.NotificationEntryManager;
|
import com.android.systemui.statusbar.notification.NotificationEntryManager;
|
||||||
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
|
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
|
||||||
@@ -73,6 +75,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
|
|||||||
private final MediaSessionManager mMediaSessionManager;
|
private final MediaSessionManager mMediaSessionManager;
|
||||||
private final ShadeController mShadeController;
|
private final ShadeController mShadeController;
|
||||||
private final ActivityStarter mActivityStarter;
|
private final ActivityStarter mActivityStarter;
|
||||||
|
private final DialogLaunchAnimator mDialogLaunchAnimator;
|
||||||
private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
|
private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
|
||||||
private final boolean mAboveStatusbar;
|
private final boolean mAboveStatusbar;
|
||||||
private final NotificationEntryManager mNotificationEntryManager;
|
private final NotificationEntryManager mNotificationEntryManager;
|
||||||
@@ -82,6 +85,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
|
|||||||
private MediaController mMediaController;
|
private MediaController mMediaController;
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
Callback mCallback;
|
Callback mCallback;
|
||||||
|
Callback mPreviousCallback;
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
LocalMediaManager mLocalMediaManager;
|
LocalMediaManager mLocalMediaManager;
|
||||||
|
|
||||||
@@ -92,7 +96,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
|
|||||||
public MediaOutputController(@NonNull Context context, String packageName,
|
public MediaOutputController(@NonNull Context context, String packageName,
|
||||||
boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager
|
boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager
|
||||||
lbm, ShadeController shadeController, ActivityStarter starter,
|
lbm, ShadeController shadeController, ActivityStarter starter,
|
||||||
NotificationEntryManager notificationEntryManager, UiEventLogger uiEventLogger) {
|
NotificationEntryManager notificationEntryManager, UiEventLogger uiEventLogger,
|
||||||
|
DialogLaunchAnimator dialogLaunchAnimator) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mPackageName = packageName;
|
mPackageName = packageName;
|
||||||
mMediaSessionManager = mediaSessionManager;
|
mMediaSessionManager = mediaSessionManager;
|
||||||
@@ -104,6 +109,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
|
|||||||
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
|
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
|
||||||
mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
|
mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
|
||||||
mUiEventLogger = uiEventLogger;
|
mUiEventLogger = uiEventLogger;
|
||||||
|
mDialogLaunchAnimator = dialogLaunchAnimator;
|
||||||
}
|
}
|
||||||
|
|
||||||
void start(@NonNull Callback cb) {
|
void start(@NonNull Callback cb) {
|
||||||
@@ -129,7 +135,19 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
|
|||||||
}
|
}
|
||||||
return;
|
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;
|
mCallback = cb;
|
||||||
|
|
||||||
mLocalMediaManager.unregisterCallback(this);
|
mLocalMediaManager.unregisterCallback(this);
|
||||||
mLocalMediaManager.stopScan();
|
mLocalMediaManager.stopScan();
|
||||||
mLocalMediaManager.registerCallback(this);
|
mLocalMediaManager.registerCallback(this);
|
||||||
@@ -145,6 +163,15 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
|
|||||||
mLocalMediaManager.stopScan();
|
mLocalMediaManager.stopScan();
|
||||||
}
|
}
|
||||||
mMediaDevices.clear();
|
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
|
@Override
|
||||||
@@ -436,6 +463,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void launchBluetoothPairing() {
|
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();
|
mCallback.dismissDialog();
|
||||||
final ActivityStarter.OnDismissAction postKeyguardAction = () -> {
|
final ActivityStarter.OnDismissAction postKeyguardAction = () -> {
|
||||||
mContext.sendBroadcast(new Intent()
|
mContext.sendBroadcast(new Intent()
|
||||||
@@ -447,14 +478,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
|
|||||||
mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true);
|
mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void launchMediaOutputDialog() {
|
void launchMediaOutputGroupDialog(View mediaOutputDialog) {
|
||||||
mCallback.dismissDialog();
|
// We show the output group dialog from the output dialog.
|
||||||
new MediaOutputDialog(mContext, mAboveStatusbar, this, mUiEventLogger);
|
MediaOutputGroupDialog dialog = new MediaOutputGroupDialog(mContext, mAboveStatusbar, this);
|
||||||
}
|
mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog);
|
||||||
|
|
||||||
void launchMediaOutputGroupDialog() {
|
|
||||||
mCallback.dismissDialog();
|
|
||||||
new MediaOutputGroupDialog(mContext, mAboveStatusbar, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isActiveRemoteDevice(@NonNull MediaDevice device) {
|
boolean isActiveRemoteDevice(@NonNull MediaDevice device) {
|
||||||
|
|||||||
@@ -40,11 +40,10 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
|
|||||||
mediaOutputController, UiEventLogger uiEventLogger) {
|
mediaOutputController, UiEventLogger uiEventLogger) {
|
||||||
super(context, mediaOutputController);
|
super(context, mediaOutputController);
|
||||||
mUiEventLogger = uiEventLogger;
|
mUiEventLogger = uiEventLogger;
|
||||||
mAdapter = new MediaOutputAdapter(mMediaOutputController);
|
mAdapter = new MediaOutputAdapter(mMediaOutputController, this);
|
||||||
if (!aboveStatusbar) {
|
if (!aboveStatusbar) {
|
||||||
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
|
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
|
||||||
}
|
}
|
||||||
show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ package com.android.systemui.media.dialog
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.session.MediaSessionManager
|
import android.media.session.MediaSessionManager
|
||||||
|
import android.view.View
|
||||||
import com.android.internal.logging.UiEventLogger
|
import com.android.internal.logging.UiEventLogger
|
||||||
import com.android.settingslib.bluetooth.LocalBluetoothManager
|
import com.android.settingslib.bluetooth.LocalBluetoothManager
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator
|
||||||
import com.android.systemui.plugins.ActivityStarter
|
import com.android.systemui.plugins.ActivityStarter
|
||||||
import com.android.systemui.statusbar.notification.NotificationEntryManager
|
import com.android.systemui.statusbar.notification.NotificationEntryManager
|
||||||
import com.android.systemui.statusbar.phone.ShadeController
|
import com.android.systemui.statusbar.phone.ShadeController
|
||||||
@@ -35,19 +37,29 @@ class MediaOutputDialogFactory @Inject constructor(
|
|||||||
private val shadeController: ShadeController,
|
private val shadeController: ShadeController,
|
||||||
private val starter: ActivityStarter,
|
private val starter: ActivityStarter,
|
||||||
private val notificationEntryManager: NotificationEntryManager,
|
private val notificationEntryManager: NotificationEntryManager,
|
||||||
private val uiEventLogger: UiEventLogger
|
private val uiEventLogger: UiEventLogger,
|
||||||
|
private val dialogLaunchAnimator: DialogLaunchAnimator
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
var mediaOutputDialog: MediaOutputDialog? = null
|
var mediaOutputDialog: MediaOutputDialog? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates a [MediaOutputDialog] for the given package. */
|
/** 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?.dismiss()
|
||||||
mediaOutputDialog = MediaOutputController(context, packageName, aboveStatusBar,
|
|
||||||
mediaSessionManager, lbm, shadeController, starter, notificationEntryManager,
|
val controller = MediaOutputController(context, packageName, aboveStatusBar,
|
||||||
uiEventLogger).run {
|
mediaSessionManager, lbm, shadeController, starter, notificationEntryManager,
|
||||||
MediaOutputDialog(context, aboveStatusBar, this, uiEventLogger)
|
uiEventLogger, dialogLaunchAnimator)
|
||||||
|
val dialog = MediaOutputDialog(context, aboveStatusBar, controller, uiEventLogger)
|
||||||
|
mediaOutputDialog = dialog
|
||||||
|
|
||||||
|
// Show the dialog.
|
||||||
|
if (view != null) {
|
||||||
|
dialogLaunchAnimator.showFromView(dialog, view)
|
||||||
|
} else {
|
||||||
|
dialog.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ public class MediaOutputGroupDialog extends MediaOutputBaseDialog {
|
|||||||
if (!aboveStatusbar) {
|
if (!aboveStatusbar) {
|
||||||
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
|
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
|
||||||
}
|
}
|
||||||
show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -83,6 +82,8 @@ public class MediaOutputGroupDialog extends MediaOutputBaseDialog {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
void onHeaderIconClick() {
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ public class InternetDialog extends SystemUIDialog implements
|
|||||||
window.setContentView(mDialogView);
|
window.setContentView(mDialogView);
|
||||||
//Only fix the width for large screen or tablet.
|
//Only fix the width for large screen or tablet.
|
||||||
window.setLayout(mContext.getResources().getDimensionPixelSize(
|
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.setWindowAnimations(R.style.Animation_InternetDialog);
|
||||||
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||||
window.addFlags(FLAG_LAYOUT_NO_LIMITS);
|
window.addFlags(FLAG_LAYOUT_NO_LIMITS);
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import android.content.Context;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
||||||
import com.android.internal.statusbar.IStatusBarService;
|
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.SysUISingleton;
|
||||||
import com.android.systemui.dagger.qualifiers.Main;
|
import com.android.systemui.dagger.qualifiers.Main;
|
||||||
import com.android.systemui.dump.DumpManager;
|
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.StatusBarIconController;
|
||||||
import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
|
import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
|
||||||
import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
|
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.OngoingCallController;
|
||||||
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger;
|
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger;
|
||||||
import com.android.systemui.statusbar.policy.RemoteInputUriController;
|
import com.android.systemui.statusbar.policy.RemoteInputUriController;
|
||||||
@@ -261,4 +265,29 @@ public interface StatusBarDependenciesModule {
|
|||||||
@Binds
|
@Binds
|
||||||
QSCarrierGroupController.SlotIndexResolver provideSlotIndexResolver(
|
QSCarrierGroupController.SlotIndexResolver provideSlotIndexResolver(
|
||||||
QSCarrierGroupController.SubscriptionManagerSlotIndexResolver impl);
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package com.android.systemui.statusbar.notification
|
|||||||
|
|
||||||
import android.util.MathUtils
|
import android.util.MathUtils
|
||||||
import com.android.internal.annotations.VisibleForTesting
|
import com.android.internal.annotations.VisibleForTesting
|
||||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
|
||||||
import com.android.systemui.animation.Interpolators
|
import com.android.systemui.animation.Interpolators
|
||||||
|
import com.android.systemui.animation.LaunchAnimator
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/** Parameters for the notifications expand animations. */
|
/** Parameters for the notifications expand animations. */
|
||||||
@@ -15,7 +15,7 @@ class ExpandAnimationParameters(
|
|||||||
|
|
||||||
topCornerRadius: Float = 0f,
|
topCornerRadius: Float = 0f,
|
||||||
bottomCornerRadius: Float = 0f
|
bottomCornerRadius: Float = 0f
|
||||||
) : ActivityLaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
|
) : LaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
constructor() : this(
|
constructor() : this(
|
||||||
top = 0, bottom = 0, left = 0, right = 0, topCornerRadius = 0f, bottomCornerRadius = 0f
|
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 {
|
fun getProgress(delay: Long, duration: Long): Float {
|
||||||
return ActivityLaunchAnimator.getProgress(linearProgress, delay, duration)
|
return LaunchAnimator.getProgress(linearProgress, delay, duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.android.systemui.statusbar.notification
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.android.internal.jank.InteractionJankMonitor
|
import com.android.internal.jank.InteractionJankMonitor
|
||||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
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.row.ExpandableNotificationRow
|
||||||
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
|
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
|
||||||
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
|
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
|
||||||
@@ -54,7 +55,7 @@ class NotificationLaunchAnimatorController(
|
|||||||
// Do nothing. Notifications are always animated inside their rootView.
|
// 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.
|
// If the notification panel is collapsed, the clip may be larger than the height.
|
||||||
val height = max(0, notification.actualHeight - notification.clipBottomAmount)
|
val height = max(0, notification.actualHeight - notification.clipBottomAmount)
|
||||||
val location = notification.locationOnScreen
|
val location = notification.locationOnScreen
|
||||||
@@ -72,12 +73,12 @@ class NotificationLaunchAnimatorController(
|
|||||||
notification.currentBackgroundRadiusTop
|
notification.currentBackgroundRadiusTop
|
||||||
}
|
}
|
||||||
val params = ExpandAnimationParameters(
|
val params = ExpandAnimationParameters(
|
||||||
top = windowTop,
|
top = windowTop,
|
||||||
bottom = location[1] + height,
|
bottom = location[1] + height,
|
||||||
left = location[0],
|
left = location[0],
|
||||||
right = location[0] + notification.width,
|
right = location[0] + notification.width,
|
||||||
topCornerRadius = topCornerRadius,
|
topCornerRadius = topCornerRadius,
|
||||||
bottomCornerRadius = notification.currentBackgroundRadiusBottom
|
bottomCornerRadius = notification.currentBackgroundRadiusBottom
|
||||||
)
|
)
|
||||||
|
|
||||||
params.startTranslationZ = notification.translationZ
|
params.startTranslationZ = notification.translationZ
|
||||||
@@ -86,8 +87,8 @@ class NotificationLaunchAnimatorController(
|
|||||||
params.startClipTopAmount = notification.clipTopAmount
|
params.startClipTopAmount = notification.clipTopAmount
|
||||||
if (notification.isChildInGroup) {
|
if (notification.isChildInGroup) {
|
||||||
params.startNotificationTop += notification.notificationParent.translationY
|
params.startNotificationTop += notification.notificationParent.translationY
|
||||||
val parentRoundedClip = Math.max(clipStartLocation
|
val parentRoundedClip = Math.max(
|
||||||
- notification.notificationParent.locationOnScreen[1], 0)
|
clipStartLocation - notification.notificationParent.locationOnScreen[1], 0)
|
||||||
params.parentStartRoundedTopClipping = parentRoundedClip
|
params.parentStartRoundedTopClipping = parentRoundedClip
|
||||||
|
|
||||||
val parentClip = notification.notificationParent.clipTopAmount
|
val parentClip = notification.notificationParent.clipTopAmount
|
||||||
@@ -157,7 +158,7 @@ class NotificationLaunchAnimatorController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onLaunchAnimationProgress(
|
override fun onLaunchAnimationProgress(
|
||||||
state: ActivityLaunchAnimator.State,
|
state: LaunchAnimator.State,
|
||||||
progress: Float,
|
progress: Float,
|
||||||
linearProgress: Float
|
linearProgress: Float
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ import com.android.keyguard.dagger.KeyguardUserSwitcherComponent;
|
|||||||
import com.android.systemui.DejankUtils;
|
import com.android.systemui.DejankUtils;
|
||||||
import com.android.systemui.Dependency;
|
import com.android.systemui.Dependency;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
|
||||||
import com.android.systemui.animation.Interpolators;
|
import com.android.systemui.animation.Interpolators;
|
||||||
|
import com.android.systemui.animation.LaunchAnimator;
|
||||||
import com.android.systemui.biometrics.AuthController;
|
import com.android.systemui.biometrics.AuthController;
|
||||||
import com.android.systemui.classifier.Classifier;
|
import com.android.systemui.classifier.Classifier;
|
||||||
import com.android.systemui.classifier.FalsingCollector;
|
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 int FLING_HIDE = 2;
|
||||||
private static final long ANIMATION_DELAY_ICON_FADE_IN =
|
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;
|
- CollapsedStatusBarFragment.FADE_IN_DELAY - 48;
|
||||||
|
|
||||||
private final DozeParameters mDozeParameters;
|
private final DozeParameters mDozeParameters;
|
||||||
@@ -3594,7 +3594,7 @@ public class NotificationPanelViewController extends PanelViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void applyLaunchAnimationProgress(float linearProgress) {
|
public void applyLaunchAnimationProgress(float linearProgress) {
|
||||||
boolean hideIcons = ActivityLaunchAnimator.getProgress(linearProgress,
|
boolean hideIcons = LaunchAnimator.getProgress(linearProgress,
|
||||||
ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
|
ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
|
||||||
if (hideIcons != mHideIconsDuringLaunchAnimation) {
|
if (hideIcons != mHideIconsDuringLaunchAnimation) {
|
||||||
mHideIconsDuringLaunchAnimation = hideIcons;
|
mHideIconsDuringLaunchAnimation = hideIcons;
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ import com.android.systemui.R;
|
|||||||
import com.android.systemui.SystemUI;
|
import com.android.systemui.SystemUI;
|
||||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||||
import com.android.systemui.animation.DelegateLaunchAnimatorController;
|
import com.android.systemui.animation.DelegateLaunchAnimatorController;
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||||
import com.android.systemui.assist.AssistManager;
|
import com.android.systemui.assist.AssistManager;
|
||||||
import com.android.systemui.battery.BatteryMeterViewController;
|
import com.android.systemui.battery.BatteryMeterViewController;
|
||||||
import com.android.systemui.biometrics.AuthRippleController;
|
import com.android.systemui.biometrics.AuthRippleController;
|
||||||
@@ -669,7 +670,8 @@ public class StatusBar extends SystemUI implements
|
|||||||
private final SysuiStatusBarStateController mStatusBarStateController;
|
private final SysuiStatusBarStateController mStatusBarStateController;
|
||||||
|
|
||||||
private HeadsUpAppearanceController mHeadsUpAppearanceController;
|
private HeadsUpAppearanceController mHeadsUpAppearanceController;
|
||||||
private ActivityLaunchAnimator mActivityLaunchAnimator;
|
private final ActivityLaunchAnimator mActivityLaunchAnimator;
|
||||||
|
private final DialogLaunchAnimator mDialogLaunchAnimator;
|
||||||
private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
|
private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
|
||||||
protected StatusBarNotificationPresenter mPresenter;
|
protected StatusBarNotificationPresenter mPresenter;
|
||||||
private NotificationActivityStarter mNotificationActivityStarter;
|
private NotificationActivityStarter mNotificationActivityStarter;
|
||||||
@@ -792,7 +794,9 @@ public class StatusBar extends SystemUI implements
|
|||||||
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
|
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
|
||||||
Optional<StartingSurface> startingSurfaceOptional,
|
Optional<StartingSurface> startingSurfaceOptional,
|
||||||
TunerService tunerService,
|
TunerService tunerService,
|
||||||
DumpManager dumpManager) {
|
DumpManager dumpManager,
|
||||||
|
ActivityLaunchAnimator activityLaunchAnimator,
|
||||||
|
DialogLaunchAnimator dialogLaunchAnimator) {
|
||||||
super(context);
|
super(context);
|
||||||
mNotificationsController = notificationsController;
|
mNotificationsController = notificationsController;
|
||||||
mLightBarController = lightBarController;
|
mLightBarController = lightBarController;
|
||||||
@@ -900,6 +904,8 @@ public class StatusBar extends SystemUI implements
|
|||||||
});
|
});
|
||||||
|
|
||||||
mActivityIntentHelper = new ActivityIntentHelper(mContext);
|
mActivityIntentHelper = new ActivityIntentHelper(mContext);
|
||||||
|
mActivityLaunchAnimator = activityLaunchAnimator;
|
||||||
|
mDialogLaunchAnimator = dialogLaunchAnimator;
|
||||||
|
|
||||||
// TODO(b/190746471): Find a better home for this.
|
// TODO(b/190746471): Find a better home for this.
|
||||||
DateTimeView.setReceiverHandler(timeTickHandler);
|
DateTimeView.setReceiverHandler(timeTickHandler);
|
||||||
@@ -1467,7 +1473,7 @@ public class StatusBar extends SystemUI implements
|
|||||||
|
|
||||||
private void setUpPresenter() {
|
private void setUpPresenter() {
|
||||||
// Set up the initial notification state.
|
// Set up the initial notification state.
|
||||||
mActivityLaunchAnimator = new ActivityLaunchAnimator(mKeyguardHandler, mContext);
|
mActivityLaunchAnimator.setCallback(mKeyguardHandler);
|
||||||
mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider(
|
mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider(
|
||||||
mNotificationShadeWindowViewController,
|
mNotificationShadeWindowViewController,
|
||||||
mStackScrollerController.getNotificationListContainer(),
|
mStackScrollerController.getNotificationListContainer(),
|
||||||
@@ -2549,7 +2555,8 @@ public class StatusBar extends SystemUI implements
|
|||||||
animationController != null && !willLaunchResolverActivity && shouldAnimateLaunch(
|
animationController != null && !willLaunchResolverActivity && shouldAnimateLaunch(
|
||||||
true /* isActivityIntent */);
|
true /* isActivityIntent */);
|
||||||
ActivityLaunchAnimator.Controller animController =
|
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
|
// If we animate, we will dismiss the shade only once the animation is done. This is taken
|
||||||
// care of by the StatusBarLaunchAnimationController.
|
// care of by the StatusBarLaunchAnimationController.
|
||||||
@@ -4426,6 +4433,8 @@ public class StatusBar extends SystemUI implements
|
|||||||
&& !mBiometricUnlockController.isWakeAndUnlock()) {
|
&& !mBiometricUnlockController.isWakeAndUnlock()) {
|
||||||
mLightRevealScrim.setRevealAmount(1f - linear);
|
mLightRevealScrim.setRevealAmount(1f - linear);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mDialogLaunchAnimator.onDozeAmountChanged(linear);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.android.systemui.statusbar.phone
|
package com.android.systemui.statusbar.phone
|
||||||
|
|
||||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
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
|
* A [ActivityLaunchAnimator.Controller] that takes care of collapsing the status bar at the right
|
||||||
@@ -22,7 +23,7 @@ class StatusBarLaunchAnimatorController(
|
|||||||
delegate.onLaunchAnimationStart(isExpandingFullyAbove)
|
delegate.onLaunchAnimationStart(isExpandingFullyAbove)
|
||||||
statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true)
|
statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true)
|
||||||
if (!isExpandingFullyAbove) {
|
if (!isExpandingFullyAbove) {
|
||||||
statusBar.collapsePanelWithDuration(ActivityLaunchAnimator.ANIMATION_DURATION.toInt())
|
statusBar.collapsePanelWithDuration(LaunchAnimator.ANIMATION_DURATION.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ class StatusBarLaunchAnimatorController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onLaunchAnimationProgress(
|
override fun onLaunchAnimationProgress(
|
||||||
state: ActivityLaunchAnimator.State,
|
state: LaunchAnimator.State,
|
||||||
progress: Float,
|
progress: Float,
|
||||||
linearProgress: Float
|
linearProgress: Float
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -30,19 +30,24 @@ import android.view.WindowManager.LayoutParams;
|
|||||||
|
|
||||||
import com.android.systemui.Dependency;
|
import com.android.systemui.Dependency;
|
||||||
import com.android.systemui.R;
|
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.broadcast.BroadcastDispatcher;
|
||||||
import com.android.systemui.statusbar.policy.KeyguardStateController;
|
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.
|
* Base class for dialogs that should appear over panels and keyguard.
|
||||||
* The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast,
|
* The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast,
|
||||||
* and dismisses itself when it receives the 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 Context mContext;
|
||||||
private final DismissReceiver mDismissReceiver;
|
private final DismissReceiver mDismissReceiver;
|
||||||
|
private final Set<DialogListener> mDialogListeners = new LinkedHashSet<>();
|
||||||
|
|
||||||
public SystemUIDialog(Context context) {
|
public SystemUIDialog(Context context) {
|
||||||
this(context, R.style.Theme_SystemUI_Dialog);
|
this(context, R.style.Theme_SystemUI_Dialog);
|
||||||
@@ -72,6 +77,43 @@ public class SystemUIDialog extends AlertDialog {
|
|||||||
mDismissReceiver.unregister();
|
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) {
|
public void setShowForAllUsers(boolean show) {
|
||||||
setShowForAllUsers(this, show);
|
setShowForAllUsers(this, show);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ import com.android.internal.logging.MetricsLogger;
|
|||||||
import com.android.keyguard.KeyguardUpdateMonitor;
|
import com.android.keyguard.KeyguardUpdateMonitor;
|
||||||
import com.android.keyguard.ViewMediatorCallback;
|
import com.android.keyguard.ViewMediatorCallback;
|
||||||
import com.android.systemui.InitController;
|
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.assist.AssistManager;
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||||
import com.android.systemui.classifier.FalsingCollector;
|
import com.android.systemui.classifier.FalsingCollector;
|
||||||
@@ -235,7 +237,9 @@ public interface StatusBarPhoneModule {
|
|||||||
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
|
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
|
||||||
Optional<StartingSurface> startingSurfaceOptional,
|
Optional<StartingSurface> startingSurfaceOptional,
|
||||||
TunerService tunerService,
|
TunerService tunerService,
|
||||||
DumpManager dumpManager) {
|
DumpManager dumpManager,
|
||||||
|
ActivityLaunchAnimator activityLaunchAnimator,
|
||||||
|
DialogLaunchAnimator dialogLaunchAnimator) {
|
||||||
return new StatusBar(
|
return new StatusBar(
|
||||||
context,
|
context,
|
||||||
notificationsController,
|
notificationsController,
|
||||||
@@ -333,6 +337,8 @@ public interface StatusBarPhoneModule {
|
|||||||
unlockedScreenOffAnimationController,
|
unlockedScreenOffAnimationController,
|
||||||
startingSurfaceOptional,
|
startingSurfaceOptional,
|
||||||
tunerService,
|
tunerService,
|
||||||
dumpManager);
|
dumpManager,
|
||||||
|
activityLaunchAnimator,
|
||||||
|
dialogLaunchAnimator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import org.mockito.junit.MockitoJUnit
|
|||||||
@RunWithLooper
|
@RunWithLooper
|
||||||
class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||||
private val launchContainer = LinearLayout(mContext)
|
private val launchContainer = LinearLayout(mContext)
|
||||||
|
private val launchAnimator = LaunchAnimator(mContext, isForTesting = true)
|
||||||
@Mock lateinit var callback: ActivityLaunchAnimator.Callback
|
@Mock lateinit var callback: ActivityLaunchAnimator.Callback
|
||||||
@Spy private val controller = TestLaunchAnimatorController(launchContainer)
|
@Spy private val controller = TestLaunchAnimatorController(launchContainer)
|
||||||
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
|
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
|
||||||
@@ -56,7 +57,8 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
activityLaunchAnimator = ActivityLaunchAnimator(callback, mContext)
|
activityLaunchAnimator = ActivityLaunchAnimator(launchAnimator)
|
||||||
|
activityLaunchAnimator.callback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startIntentWithAnimation(
|
private fun startIntentWithAnimation(
|
||||||
@@ -120,7 +122,8 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() {
|
fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() {
|
||||||
`when`(callback.isOnKeyguard()).thenReturn(true)
|
`when`(callback.isOnKeyguard()).thenReturn(true)
|
||||||
val animator = ActivityLaunchAnimator(callback, context)
|
val animator = ActivityLaunchAnimator(launchAnimator)
|
||||||
|
animator.callback = callback
|
||||||
|
|
||||||
val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
|
val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
|
||||||
var animationAdapter: RemoteAnimationAdapter? = null
|
var animationAdapter: RemoteAnimationAdapter? = null
|
||||||
@@ -208,7 +211,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
|||||||
private class TestLaunchAnimatorController(
|
private class TestLaunchAnimatorController(
|
||||||
override var launchContainer: ViewGroup
|
override var launchContainer: ViewGroup
|
||||||
) : ActivityLaunchAnimator.Controller {
|
) : ActivityLaunchAnimator.Controller {
|
||||||
override fun createAnimatorState() = ActivityLaunchAnimator.State(
|
override fun createAnimatorState() = LaunchAnimator.State(
|
||||||
top = 100,
|
top = 100,
|
||||||
bottom = 200,
|
bottom = 200,
|
||||||
left = 300,
|
left = 300,
|
||||||
@@ -232,7 +235,7 @@ private class TestLaunchAnimatorController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onLaunchAnimationProgress(
|
override fun onLaunchAnimationProgress(
|
||||||
state: ActivityLaunchAnimator.State,
|
state: LaunchAnimator.State,
|
||||||
progress: Float,
|
progress: Float,
|
||||||
linearProgress: Float
|
linearProgress: Float
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() {
|
|||||||
fun animatingOrphanViewDoesNotCrash() {
|
fun animatingOrphanViewDoesNotCrash() {
|
||||||
val ghostedView = LinearLayout(mContext)
|
val ghostedView = LinearLayout(mContext)
|
||||||
val controller = GhostedViewLaunchAnimatorController(ghostedView)
|
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.onIntentStarted(willAnimate = true)
|
||||||
controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
|
controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
|
|||||||
|
|
||||||
// Mock
|
// Mock
|
||||||
private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
|
private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
|
||||||
|
private MediaOutputDialog mMediaOutputDialog = mock(MediaOutputDialog.class);
|
||||||
private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
|
private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
|
||||||
private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
|
private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
|
||||||
private Icon mIcon = mock(Icon.class);
|
private Icon mIcon = mock(Icon.class);
|
||||||
@@ -65,7 +66,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
|
mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController, mMediaOutputDialog);
|
||||||
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
|
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
|
||||||
.onCreateViewHolder(new LinearLayout(mContext), 0);
|
.onCreateViewHolder(new LinearLayout(mContext), 0);
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import com.android.internal.logging.UiEventLogger;
|
|||||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.SysuiTestCase;
|
import com.android.systemui.SysuiTestCase;
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||||
import com.android.systemui.plugins.ActivityStarter;
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.statusbar.notification.NotificationEntryManager;
|
import com.android.systemui.statusbar.notification.NotificationEntryManager;
|
||||||
import com.android.systemui.statusbar.phone.ShadeController;
|
import com.android.systemui.statusbar.phone.ShadeController;
|
||||||
@@ -63,6 +64,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
|
|||||||
private NotificationEntryManager mNotificationEntryManager =
|
private NotificationEntryManager mNotificationEntryManager =
|
||||||
mock(NotificationEntryManager.class);
|
mock(NotificationEntryManager.class);
|
||||||
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
|
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
|
||||||
|
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
|
||||||
|
|
||||||
private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
|
private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
|
||||||
private MediaOutputController mMediaOutputController;
|
private MediaOutputController mMediaOutputController;
|
||||||
@@ -75,7 +77,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
|
|||||||
public void setUp() {
|
public void setUp() {
|
||||||
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
|
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
|
||||||
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
||||||
mNotificationEntryManager, mUiEventLogger);
|
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
|
||||||
mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext,
|
mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext,
|
||||||
mMediaOutputController);
|
mMediaOutputController);
|
||||||
mMediaOutputBaseDialogImpl.onCreate(new Bundle());
|
mMediaOutputBaseDialogImpl.onCreate(new Bundle());
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import com.android.settingslib.media.LocalMediaManager;
|
|||||||
import com.android.settingslib.media.MediaDevice;
|
import com.android.settingslib.media.MediaDevice;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.SysuiTestCase;
|
import com.android.systemui.SysuiTestCase;
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||||
import com.android.systemui.plugins.ActivityStarter;
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.statusbar.notification.NotificationEntryManager;
|
import com.android.systemui.statusbar.notification.NotificationEntryManager;
|
||||||
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
|
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
|
||||||
@@ -91,6 +92,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
|
|||||||
private NotificationEntryManager mNotificationEntryManager =
|
private NotificationEntryManager mNotificationEntryManager =
|
||||||
mock(NotificationEntryManager.class);
|
mock(NotificationEntryManager.class);
|
||||||
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
|
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
|
||||||
|
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
|
||||||
|
|
||||||
private Context mSpyContext;
|
private Context mSpyContext;
|
||||||
private MediaOutputController mMediaOutputController;
|
private MediaOutputController mMediaOutputController;
|
||||||
@@ -113,7 +115,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
|
|||||||
|
|
||||||
mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, false,
|
mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, false,
|
||||||
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
||||||
mNotificationEntryManager, mUiEventLogger);
|
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
|
||||||
mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
|
mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
|
||||||
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
|
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
|
||||||
MediaDescription.Builder builder = new MediaDescription.Builder();
|
MediaDescription.Builder builder = new MediaDescription.Builder();
|
||||||
@@ -157,7 +159,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
|
|||||||
public void start_withoutPackageName_verifyMediaControllerInit() {
|
public void start_withoutPackageName_verifyMediaControllerInit() {
|
||||||
mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
|
mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
|
||||||
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
||||||
mNotificationEntryManager, mUiEventLogger);
|
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
|
||||||
|
|
||||||
mMediaOutputController.start(mCb);
|
mMediaOutputController.start(mCb);
|
||||||
|
|
||||||
@@ -178,7 +180,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
|
|||||||
public void stop_withoutPackageName_verifyMediaControllerDeinit() {
|
public void stop_withoutPackageName_verifyMediaControllerDeinit() {
|
||||||
mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
|
mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
|
||||||
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
||||||
mNotificationEntryManager, mUiEventLogger);
|
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
|
||||||
|
|
||||||
mMediaOutputController.start(mCb);
|
mMediaOutputController.start(mCb);
|
||||||
|
|
||||||
@@ -449,7 +451,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
|
|||||||
public void getNotificationLargeIcon_withoutPackageName_returnsNull() {
|
public void getNotificationLargeIcon_withoutPackageName_returnsNull() {
|
||||||
mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
|
mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
|
||||||
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
||||||
mNotificationEntryManager, mUiEventLogger);
|
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
|
||||||
|
|
||||||
assertThat(mMediaOutputController.getNotificationIcon()).isNull();
|
assertThat(mMediaOutputController.getNotificationIcon()).isNull();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
|||||||
import com.android.settingslib.media.LocalMediaManager;
|
import com.android.settingslib.media.LocalMediaManager;
|
||||||
import com.android.settingslib.media.MediaDevice;
|
import com.android.settingslib.media.MediaDevice;
|
||||||
import com.android.systemui.SysuiTestCase;
|
import com.android.systemui.SysuiTestCase;
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||||
import com.android.systemui.plugins.ActivityStarter;
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.statusbar.notification.NotificationEntryManager;
|
import com.android.systemui.statusbar.notification.NotificationEntryManager;
|
||||||
import com.android.systemui.statusbar.phone.ShadeController;
|
import com.android.systemui.statusbar.phone.ShadeController;
|
||||||
@@ -65,6 +66,7 @@ public class MediaOutputDialogTest extends SysuiTestCase {
|
|||||||
private final NotificationEntryManager mNotificationEntryManager =
|
private final NotificationEntryManager mNotificationEntryManager =
|
||||||
mock(NotificationEntryManager.class);
|
mock(NotificationEntryManager.class);
|
||||||
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
|
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
|
||||||
|
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
|
||||||
|
|
||||||
private MediaOutputDialog mMediaOutputDialog;
|
private MediaOutputDialog mMediaOutputDialog;
|
||||||
private MediaOutputController mMediaOutputController;
|
private MediaOutputController mMediaOutputController;
|
||||||
@@ -74,10 +76,11 @@ public class MediaOutputDialogTest extends SysuiTestCase {
|
|||||||
public void setUp() {
|
public void setUp() {
|
||||||
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
|
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
|
||||||
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
||||||
mNotificationEntryManager, mUiEventLogger);
|
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
|
||||||
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
|
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
|
||||||
mMediaOutputDialog = new MediaOutputDialog(mContext, false,
|
mMediaOutputDialog = new MediaOutputDialog(mContext, false,
|
||||||
mMediaOutputController, mUiEventLogger);
|
mMediaOutputController, mUiEventLogger);
|
||||||
|
mMediaOutputDialog.show();
|
||||||
|
|
||||||
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice);
|
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice);
|
||||||
when(mMediaDevice.getFeatures()).thenReturn(mFeatures);
|
when(mMediaDevice.getFeatures()).thenReturn(mFeatures);
|
||||||
@@ -123,6 +126,7 @@ public class MediaOutputDialogTest extends SysuiTestCase {
|
|||||||
public void onCreate_ShouldLogVisibility() {
|
public void onCreate_ShouldLogVisibility() {
|
||||||
MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false,
|
MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false,
|
||||||
mMediaOutputController, mUiEventLogger);
|
mMediaOutputController, mUiEventLogger);
|
||||||
|
testDialog.show();
|
||||||
|
|
||||||
testDialog.dismissDialog();
|
testDialog.dismissDialog();
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import com.android.settingslib.media.LocalMediaManager;
|
|||||||
import com.android.settingslib.media.MediaDevice;
|
import com.android.settingslib.media.MediaDevice;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.SysuiTestCase;
|
import com.android.systemui.SysuiTestCase;
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||||
import com.android.systemui.plugins.ActivityStarter;
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.statusbar.notification.NotificationEntryManager;
|
import com.android.systemui.statusbar.notification.NotificationEntryManager;
|
||||||
import com.android.systemui.statusbar.phone.ShadeController;
|
import com.android.systemui.statusbar.phone.ShadeController;
|
||||||
@@ -64,6 +65,7 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase {
|
|||||||
private NotificationEntryManager mNotificationEntryManager =
|
private NotificationEntryManager mNotificationEntryManager =
|
||||||
mock(NotificationEntryManager.class);
|
mock(NotificationEntryManager.class);
|
||||||
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
|
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
|
||||||
|
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
|
||||||
|
|
||||||
private MediaOutputGroupDialog mMediaOutputGroupDialog;
|
private MediaOutputGroupDialog mMediaOutputGroupDialog;
|
||||||
private MediaOutputController mMediaOutputController;
|
private MediaOutputController mMediaOutputController;
|
||||||
@@ -73,10 +75,11 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase {
|
|||||||
public void setUp() {
|
public void setUp() {
|
||||||
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
|
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
|
||||||
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
|
||||||
mNotificationEntryManager, mUiEventLogger);
|
mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
|
||||||
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
|
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
|
||||||
mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false,
|
mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false,
|
||||||
mMediaOutputController);
|
mMediaOutputController);
|
||||||
|
mMediaOutputGroupDialog.show();
|
||||||
when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mMediaDevices);
|
when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mMediaDevices);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ import com.android.keyguard.ViewMediatorCallback;
|
|||||||
import com.android.systemui.InitController;
|
import com.android.systemui.InitController;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.SysuiTestCase;
|
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.assist.AssistManager;
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||||
import com.android.systemui.classifier.FalsingCollectorFake;
|
import com.android.systemui.classifier.FalsingCollectorFake;
|
||||||
@@ -274,6 +276,8 @@ public class StatusBarTest extends SysuiTestCase {
|
|||||||
@Mock private StartingSurface mStartingSurface;
|
@Mock private StartingSurface mStartingSurface;
|
||||||
@Mock private OperatorNameViewController mOperatorNameViewController;
|
@Mock private OperatorNameViewController mOperatorNameViewController;
|
||||||
@Mock private OperatorNameViewController.Factory mOperatorNameViewControllerFactory;
|
@Mock private OperatorNameViewController.Factory mOperatorNameViewControllerFactory;
|
||||||
|
@Mock private ActivityLaunchAnimator mActivityLaunchAnimator;
|
||||||
|
@Mock private DialogLaunchAnimator mDialogLaunchAnimator;
|
||||||
private ShadeController mShadeController;
|
private ShadeController mShadeController;
|
||||||
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
|
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
|
||||||
private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
|
private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
|
||||||
@@ -452,7 +456,9 @@ public class StatusBarTest extends SysuiTestCase {
|
|||||||
mUnlockedScreenOffAnimationController,
|
mUnlockedScreenOffAnimationController,
|
||||||
Optional.of(mStartingSurface),
|
Optional.of(mStartingSurface),
|
||||||
mTunerService,
|
mTunerService,
|
||||||
mock(DumpManager.class));
|
mock(DumpManager.class),
|
||||||
|
mActivityLaunchAnimator,
|
||||||
|
mDialogLaunchAnimator);
|
||||||
when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class),
|
when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class),
|
||||||
any(NotificationPanelViewController.class), any(BiometricUnlockController.class),
|
any(NotificationPanelViewController.class), any(BiometricUnlockController.class),
|
||||||
any(ViewGroup.class), any(KeyguardBypassController.class)))
|
any(ViewGroup.class), any(KeyguardBypassController.class)))
|
||||||
|
|||||||
Reference in New Issue
Block a user