Merge changes I49c6095f,Id14bee4b into tm-qpr-dev am: eb6c71b9eb
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/18799657 Change-Id: I0ca6a76d2fa00e59a396e7196b7c95a4c0dd0198 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
committed by
Automerger Merge Worker
commit
a411565494
@@ -59,7 +59,8 @@ class ActivityLaunchAnimator(
|
|||||||
companion object {
|
companion object {
|
||||||
/** The timings when animating a View into an app. */
|
/** The timings when animating a View into an app. */
|
||||||
@JvmField
|
@JvmField
|
||||||
val TIMINGS = LaunchAnimator.Timings(
|
val TIMINGS =
|
||||||
|
LaunchAnimator.Timings(
|
||||||
totalDuration = 500L,
|
totalDuration = 500L,
|
||||||
contentBeforeFadeOutDelay = 0L,
|
contentBeforeFadeOutDelay = 0L,
|
||||||
contentBeforeFadeOutDuration = 150L,
|
contentBeforeFadeOutDuration = 150L,
|
||||||
@@ -72,13 +73,12 @@ class ActivityLaunchAnimator(
|
|||||||
* showing the app (which is under the dialog window) so that the dialog window dim is fully
|
* showing the app (which is under the dialog window) so that the dialog window dim is fully
|
||||||
* faded out, to avoid flicker.
|
* faded out, to avoid flicker.
|
||||||
*/
|
*/
|
||||||
val DIALOG_TIMINGS = TIMINGS.copy(
|
val DIALOG_TIMINGS =
|
||||||
contentBeforeFadeOutDuration = 200L,
|
TIMINGS.copy(contentBeforeFadeOutDuration = 200L, contentAfterFadeInDelay = 200L)
|
||||||
contentAfterFadeInDelay = 200L
|
|
||||||
)
|
|
||||||
|
|
||||||
/** The interpolators when animating a View or a dialog into an app. */
|
/** The interpolators when animating a View or a dialog into an app. */
|
||||||
val INTERPOLATORS = LaunchAnimator.Interpolators(
|
val INTERPOLATORS =
|
||||||
|
LaunchAnimator.Interpolators(
|
||||||
positionInterpolator = Interpolators.EMPHASIZED,
|
positionInterpolator = Interpolators.EMPHASIZED,
|
||||||
positionXInterpolator = createPositionXInterpolator(),
|
positionXInterpolator = createPositionXInterpolator(),
|
||||||
contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN,
|
contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN,
|
||||||
@@ -98,7 +98,8 @@ class ActivityLaunchAnimator(
|
|||||||
private const val LAUNCH_TIMEOUT = 1000L
|
private const val LAUNCH_TIMEOUT = 1000L
|
||||||
|
|
||||||
private fun createPositionXInterpolator(): Interpolator {
|
private fun createPositionXInterpolator(): Interpolator {
|
||||||
val path = Path().apply {
|
val path =
|
||||||
|
Path().apply {
|
||||||
moveTo(0f, 0f)
|
moveTo(0f, 0f)
|
||||||
cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f)
|
cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f)
|
||||||
cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f)
|
cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f)
|
||||||
@@ -150,14 +151,18 @@ class ActivityLaunchAnimator(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val callback = this.callback ?: throw IllegalStateException(
|
val callback =
|
||||||
"ActivityLaunchAnimator.callback must be set before using this animator")
|
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
|
||||||
|
|
||||||
// Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the
|
// Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the
|
||||||
// keyguard with the animation
|
// keyguard with the animation
|
||||||
val animationAdapter = if (!hideKeyguardWithAnimation) {
|
val animationAdapter =
|
||||||
|
if (!hideKeyguardWithAnimation) {
|
||||||
RemoteAnimationAdapter(
|
RemoteAnimationAdapter(
|
||||||
runner,
|
runner,
|
||||||
TIMINGS.totalDuration,
|
TIMINGS.totalDuration,
|
||||||
@@ -171,8 +176,12 @@ class ActivityLaunchAnimator(
|
|||||||
// activity launches.
|
// activity launches.
|
||||||
if (packageName != null && animationAdapter != null) {
|
if (packageName != null && animationAdapter != null) {
|
||||||
try {
|
try {
|
||||||
ActivityTaskManager.getService().registerRemoteAnimationForNextActivityStart(
|
ActivityTaskManager.getService()
|
||||||
packageName, animationAdapter, null /* launchCookie */)
|
.registerRemoteAnimationForNextActivityStart(
|
||||||
|
packageName,
|
||||||
|
animationAdapter,
|
||||||
|
null /* launchCookie */
|
||||||
|
)
|
||||||
} catch (e: RemoteException) {
|
} catch (e: RemoteException) {
|
||||||
Log.w(TAG, "Unable to register the remote animation", e)
|
Log.w(TAG, "Unable to register the remote animation", e)
|
||||||
}
|
}
|
||||||
@@ -188,8 +197,11 @@ class ActivityLaunchAnimator(
|
|||||||
(launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
|
(launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
|
||||||
hideKeyguardWithAnimation)
|
hideKeyguardWithAnimation)
|
||||||
|
|
||||||
Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " +
|
Log.i(
|
||||||
"hideKeyguardWithAnimation=$hideKeyguardWithAnimation")
|
TAG,
|
||||||
|
"launchResult=$launchResult willAnimate=$willAnimate " +
|
||||||
|
"hideKeyguardWithAnimation=$hideKeyguardWithAnimation"
|
||||||
|
)
|
||||||
controller.callOnIntentStartedOnMainThread(willAnimate)
|
controller.callOnIntentStartedOnMainThread(willAnimate)
|
||||||
|
|
||||||
// If we expect an animation, post a timeout to cancel it in case the remote animation is
|
// If we expect an animation, post a timeout to cancel it in case the remote animation is
|
||||||
@@ -206,9 +218,7 @@ class ActivityLaunchAnimator(
|
|||||||
|
|
||||||
private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) {
|
private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) {
|
||||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||||
this.launchContainer.context.mainExecutor.execute {
|
this.launchContainer.context.mainExecutor.execute { this.onIntentStarted(willAnimate) }
|
||||||
this.onIntentStarted(willAnimate)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.onIntentStarted(willAnimate)
|
this.onIntentStarted(willAnimate)
|
||||||
}
|
}
|
||||||
@@ -246,8 +256,7 @@ class ActivityLaunchAnimator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Create a new animation [Runner] controlled by [controller]. */
|
/** Create a new animation [Runner] controlled by [controller]. */
|
||||||
@VisibleForTesting
|
@VisibleForTesting fun createRunner(controller: Controller): Runner = Runner(controller)
|
||||||
fun createRunner(controller: Controller): Runner = Runner(controller)
|
|
||||||
|
|
||||||
interface PendingIntentStarter {
|
interface PendingIntentStarter {
|
||||||
/**
|
/**
|
||||||
@@ -271,19 +280,16 @@ class ActivityLaunchAnimator(
|
|||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/** Called when an activity launch animation started. */
|
/** Called when an activity launch animation started. */
|
||||||
@JvmDefault
|
@JvmDefault fun onLaunchAnimationStart() {}
|
||||||
fun onLaunchAnimationStart() {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an activity launch animation is finished. This will be called if and only if
|
* Called when an activity launch animation is finished. This will be called if and only if
|
||||||
* [onLaunchAnimationStart] was called earlier.
|
* [onLaunchAnimationStart] was called earlier.
|
||||||
*/
|
*/
|
||||||
@JvmDefault
|
@JvmDefault fun onLaunchAnimationEnd() {}
|
||||||
fun onLaunchAnimationEnd() {}
|
|
||||||
|
|
||||||
/** Called when an activity launch animation made progress. */
|
/** Called when an activity launch animation made progress. */
|
||||||
@JvmDefault
|
@JvmDefault fun onLaunchAnimationProgress(linearProgress: Float) {}
|
||||||
fun onLaunchAnimationProgress(linearProgress: Float) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -326,6 +332,17 @@ class ActivityLaunchAnimator(
|
|||||||
val isDialogLaunch: Boolean
|
val isDialogLaunch: Boolean
|
||||||
get() = false
|
get() = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the expandable controller by this [Controller] is below the launching window that
|
||||||
|
* is going to be animated.
|
||||||
|
*
|
||||||
|
* This should be `false` when launching an app from the shade or status bar, given that
|
||||||
|
* they are drawn above all apps. This is usually `true` when using this launcher in a
|
||||||
|
* normal app or a launcher, that are drawn below the animating activity/window.
|
||||||
|
*/
|
||||||
|
val isBelowAnimatingWindow: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -394,9 +411,7 @@ class ActivityLaunchAnimator(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
context.mainExecutor.execute {
|
context.mainExecutor.execute { startAnimation(apps, nonApps, iCallback) }
|
||||||
startAnimation(apps, nonApps, iCallback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startAnimation(
|
private fun startAnimation(
|
||||||
@@ -408,9 +423,7 @@ class ActivityLaunchAnimator(
|
|||||||
Log.d(TAG, "Remote animation started")
|
Log.d(TAG, "Remote animation started")
|
||||||
}
|
}
|
||||||
|
|
||||||
val window = apps?.firstOrNull {
|
val window = apps?.firstOrNull { it.mode == RemoteAnimationTarget.MODE_OPENING }
|
||||||
it.mode == RemoteAnimationTarget.MODE_OPENING
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window == null) {
|
if (window == null) {
|
||||||
Log.i(TAG, "Aborting the animation as no window is opening")
|
Log.i(TAG, "Aborting the animation as no window is opening")
|
||||||
@@ -420,23 +433,26 @@ class ActivityLaunchAnimator(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val navigationBar = nonApps?.firstOrNull {
|
val navigationBar =
|
||||||
|
nonApps?.firstOrNull {
|
||||||
it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
|
it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
|
||||||
}
|
}
|
||||||
|
|
||||||
val windowBounds = window.screenSpaceBounds
|
val windowBounds = window.screenSpaceBounds
|
||||||
val endState = LaunchAnimator.State(
|
val endState =
|
||||||
|
LaunchAnimator.State(
|
||||||
top = windowBounds.top,
|
top = windowBounds.top,
|
||||||
bottom = windowBounds.bottom,
|
bottom = windowBounds.bottom,
|
||||||
left = windowBounds.left,
|
left = windowBounds.left,
|
||||||
right = windowBounds.right
|
right = windowBounds.right
|
||||||
)
|
)
|
||||||
val callback = this@ActivityLaunchAnimator.callback!!
|
val callback = this@ActivityLaunchAnimator.callback!!
|
||||||
val windowBackgroundColor = window.taskInfo?.let { callback.getBackgroundColor(it) }
|
val windowBackgroundColor =
|
||||||
?: window.backgroundColor
|
window.taskInfo?.let { callback.getBackgroundColor(it) } ?: window.backgroundColor
|
||||||
|
|
||||||
// Make sure we use the modified timings when animating a dialog into an app.
|
// Make sure we use the modified timings when animating a dialog into an app.
|
||||||
val launchAnimator = if (controller.isDialogLaunch) {
|
val launchAnimator =
|
||||||
|
if (controller.isDialogLaunch) {
|
||||||
dialogToAppAnimator
|
dialogToAppAnimator
|
||||||
} else {
|
} else {
|
||||||
launchAnimator
|
launchAnimator
|
||||||
@@ -446,12 +462,14 @@ class ActivityLaunchAnimator(
|
|||||||
// instead of recomputing isExpandingFullyAbove here.
|
// instead of recomputing isExpandingFullyAbove here.
|
||||||
val isExpandingFullyAbove =
|
val isExpandingFullyAbove =
|
||||||
launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState)
|
launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState)
|
||||||
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.
|
||||||
ScreenDecorationsUtils.getWindowCornerRadius(context)
|
ScreenDecorationsUtils.getWindowCornerRadius(context)
|
||||||
} else {
|
} else {
|
||||||
// This usually means we are in split screen mode, so 2 out of 4 corners will have
|
// This usually means we are in split screen mode, so 2 out of 4 corners will
|
||||||
|
// have
|
||||||
// a radius of 0.
|
// a radius of 0.
|
||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
@@ -460,7 +478,8 @@ class ActivityLaunchAnimator(
|
|||||||
|
|
||||||
// We animate the opening window and delegate the view expansion to [this.controller].
|
// We animate the opening window and delegate the view expansion to [this.controller].
|
||||||
val delegate = this.controller
|
val delegate = this.controller
|
||||||
val controller = object : LaunchAnimator.Controller by delegate {
|
val controller =
|
||||||
|
object : Controller by delegate {
|
||||||
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
|
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
|
||||||
listeners.forEach { it.onLaunchAnimationStart() }
|
listeners.forEach { it.onLaunchAnimationStart() }
|
||||||
delegate.onLaunchAnimationStart(isExpandingFullyAbove)
|
delegate.onLaunchAnimationStart(isExpandingFullyAbove)
|
||||||
@@ -477,10 +496,10 @@ class ActivityLaunchAnimator(
|
|||||||
progress: Float,
|
progress: Float,
|
||||||
linearProgress: Float
|
linearProgress: Float
|
||||||
) {
|
) {
|
||||||
// Apply the state to the window only if it is visible, i.e. when the expanding
|
// Apply the state to the window only if it is visible, i.e. when the
|
||||||
// view is *not* visible.
|
// expanding view is *not* visible.
|
||||||
if (!state.visible) {
|
if (!state.visible) {
|
||||||
applyStateToWindow(window, state)
|
applyStateToWindow(window, state, linearProgress)
|
||||||
}
|
}
|
||||||
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
|
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
|
||||||
|
|
||||||
@@ -489,11 +508,21 @@ class ActivityLaunchAnimator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
animation = launchAnimator.startAnimation(
|
animation =
|
||||||
controller, endState, windowBackgroundColor, drawHole = true)
|
launchAnimator.startAnimation(
|
||||||
|
controller,
|
||||||
|
endState,
|
||||||
|
windowBackgroundColor,
|
||||||
|
fadeOutWindowBackgroundLayer = !controller.isBelowAnimatingWindow,
|
||||||
|
drawHole = !controller.isBelowAnimatingWindow,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) {
|
private fun applyStateToWindow(
|
||||||
|
window: RemoteAnimationTarget,
|
||||||
|
state: LaunchAnimator.State,
|
||||||
|
linearProgress: Float,
|
||||||
|
) {
|
||||||
if (transactionApplierView.viewRootImpl == null) {
|
if (transactionApplierView.viewRootImpl == null) {
|
||||||
// If the view root we synchronize with was detached, don't apply any transaction
|
// If the view root we synchronize with was detached, don't apply any transaction
|
||||||
// (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw).
|
// (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw).
|
||||||
@@ -535,14 +564,33 @@ class ActivityLaunchAnimator(
|
|||||||
windowCropF.bottom.roundToInt()
|
windowCropF.bottom.roundToInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The alpha of the opening window. If it opens above the expandable, then it should
|
||||||
|
// fade in progressively. Otherwise, it should be fully opaque and will be progressively
|
||||||
|
// revealed as the window background color layer above the window fades out.
|
||||||
|
val alpha =
|
||||||
|
if (controller.isBelowAnimatingWindow) {
|
||||||
|
val windowProgress =
|
||||||
|
LaunchAnimator.getProgress(
|
||||||
|
TIMINGS,
|
||||||
|
linearProgress,
|
||||||
|
TIMINGS.contentAfterFadeInDelay,
|
||||||
|
TIMINGS.contentAfterFadeInDuration
|
||||||
|
)
|
||||||
|
|
||||||
|
INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation(windowProgress)
|
||||||
|
} else {
|
||||||
|
1f
|
||||||
|
}
|
||||||
|
|
||||||
// The scale will also be applied to the corner radius, so we divide by the scale to
|
// The scale will also be applied to the corner radius, so we divide by the scale to
|
||||||
// keep the original radius. We use the max of (topCornerRadius, bottomCornerRadius) to
|
// keep the original radius. We use the max of (topCornerRadius, bottomCornerRadius) to
|
||||||
// make sure that the window does not draw itself behind the expanding view. This is
|
// make sure that the window does not draw itself behind the expanding view. This is
|
||||||
// especially important for lock screen animations, where the window is not clipped by
|
// especially important for lock screen animations, where the window is not clipped by
|
||||||
// the shade.
|
// the shade.
|
||||||
val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale
|
val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale
|
||||||
val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash)
|
val params =
|
||||||
.withAlpha(1f)
|
SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash)
|
||||||
|
.withAlpha(alpha)
|
||||||
.withMatrix(matrix)
|
.withMatrix(matrix)
|
||||||
.withWindowCrop(windowCrop)
|
.withWindowCrop(windowCrop)
|
||||||
.withCornerRadius(cornerRadius)
|
.withCornerRadius(cornerRadius)
|
||||||
@@ -563,14 +611,21 @@ class ActivityLaunchAnimator(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress,
|
val fadeInProgress =
|
||||||
ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)
|
LaunchAnimator.getProgress(
|
||||||
|
TIMINGS,
|
||||||
|
linearProgress,
|
||||||
|
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) {
|
||||||
matrix.reset()
|
matrix.reset()
|
||||||
matrix.setTranslate(
|
matrix.setTranslate(
|
||||||
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))
|
||||||
@@ -578,8 +633,13 @@ class ActivityLaunchAnimator(
|
|||||||
.withWindowCrop(windowCrop)
|
.withWindowCrop(windowCrop)
|
||||||
.withVisibility(true)
|
.withVisibility(true)
|
||||||
} else {
|
} else {
|
||||||
val fadeOutProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress, 0,
|
val fadeOutProgress =
|
||||||
ANIMATION_DURATION_NAV_FADE_OUT)
|
LaunchAnimator.getProgress(
|
||||||
|
TIMINGS,
|
||||||
|
linearProgress,
|
||||||
|
0,
|
||||||
|
ANIMATION_DURATION_NAV_FADE_OUT
|
||||||
|
)
|
||||||
params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
|
params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ private const val TAG = "DialogLaunchAnimator"
|
|||||||
* @see showFromDialog
|
* @see showFromDialog
|
||||||
* @see createActivityLaunchController
|
* @see createActivityLaunchController
|
||||||
*/
|
*/
|
||||||
class DialogLaunchAnimator @JvmOverloads constructor(
|
class DialogLaunchAnimator
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
private val dreamManager: IDreamManager,
|
private val dreamManager: IDreamManager,
|
||||||
private val interactionJankMonitor: InteractionJankMonitor,
|
private val interactionJankMonitor: InteractionJankMonitor,
|
||||||
private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
|
private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
|
||||||
@@ -62,7 +64,8 @@ class DialogLaunchAnimator @JvmOverloads constructor(
|
|||||||
|
|
||||||
// We use the same interpolator for X and Y axis to make sure the dialog does not move out
|
// We use the same interpolator for X and Y axis to make sure the dialog does not move out
|
||||||
// of the screen bounds during the animation.
|
// of the screen bounds during the animation.
|
||||||
private val INTERPOLATORS = ActivityLaunchAnimator.INTERPOLATORS.copy(
|
private val INTERPOLATORS =
|
||||||
|
ActivityLaunchAnimator.INTERPOLATORS.copy(
|
||||||
positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
|
positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,8 +108,10 @@ class DialogLaunchAnimator @JvmOverloads constructor(
|
|||||||
|
|
||||||
// If the view we are launching from belongs to another dialog, then this means the caller
|
// If the view we are launching from belongs to another dialog, then this means the caller
|
||||||
// intent is to launch a dialog from another dialog.
|
// intent is to launch a dialog from another dialog.
|
||||||
val animatedParent = openedDialogs
|
val animatedParent =
|
||||||
.firstOrNull { it.dialog.window.decorView.viewRootImpl == view.viewRootImpl }
|
openedDialogs.firstOrNull {
|
||||||
|
it.dialog.window.decorView.viewRootImpl == view.viewRootImpl
|
||||||
|
}
|
||||||
val animateFrom = animatedParent?.dialogContentWithBackground ?: view
|
val animateFrom = animatedParent?.dialogContentWithBackground ?: view
|
||||||
|
|
||||||
// Make sure we don't run the launch animation from the same view twice at the same time.
|
// Make sure we don't run the launch animation from the same view twice at the same time.
|
||||||
@@ -118,7 +123,8 @@ class DialogLaunchAnimator @JvmOverloads constructor(
|
|||||||
|
|
||||||
animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
|
animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
|
||||||
|
|
||||||
val animatedDialog = AnimatedDialog(
|
val animatedDialog =
|
||||||
|
AnimatedDialog(
|
||||||
launchAnimator,
|
launchAnimator,
|
||||||
dreamManager,
|
dreamManager,
|
||||||
interactionJankMonitor,
|
interactionJankMonitor,
|
||||||
@@ -146,9 +152,8 @@ class DialogLaunchAnimator @JvmOverloads constructor(
|
|||||||
animateFrom: Dialog,
|
animateFrom: Dialog,
|
||||||
animateBackgroundBoundsChange: Boolean = false
|
animateBackgroundBoundsChange: Boolean = false
|
||||||
) {
|
) {
|
||||||
val view = openedDialogs
|
val view =
|
||||||
.firstOrNull { it.dialog == animateFrom }
|
openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground
|
||||||
?.dialogContentWithBackground
|
|
||||||
?: throw IllegalStateException(
|
?: throw IllegalStateException(
|
||||||
"The animateFrom dialog was not animated using " +
|
"The animateFrom dialog was not animated using " +
|
||||||
"DialogLaunchAnimator.showFrom(View|Dialog)"
|
"DialogLaunchAnimator.showFrom(View|Dialog)"
|
||||||
@@ -175,8 +180,10 @@ class DialogLaunchAnimator @JvmOverloads constructor(
|
|||||||
view: View,
|
view: View,
|
||||||
cujType: Int? = null
|
cujType: Int? = null
|
||||||
): ActivityLaunchAnimator.Controller? {
|
): ActivityLaunchAnimator.Controller? {
|
||||||
val animatedDialog = openedDialogs
|
val animatedDialog =
|
||||||
.firstOrNull { it.dialog.window.decorView.viewRootImpl == view.viewRootImpl }
|
openedDialogs.firstOrNull {
|
||||||
|
it.dialog.window.decorView.viewRootImpl == view.viewRootImpl
|
||||||
|
}
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
// At this point, we know that the intent of the caller is to dismiss the dialog to show
|
// At this point, we know that the intent of the caller is to dismiss the dialog to show
|
||||||
@@ -257,9 +264,9 @@ class DialogLaunchAnimator @JvmOverloads constructor(
|
|||||||
* Ensure that all dialogs currently shown won't animate into their touch surface when
|
* Ensure that all dialogs currently shown won't animate into their touch surface when
|
||||||
* dismissed.
|
* dismissed.
|
||||||
*
|
*
|
||||||
* This is a temporary API meant to be called right before we both dismiss a dialog and start
|
* This is a temporary API meant to be called right before we both dismiss a dialog and start an
|
||||||
* an activity, which currently does not look good if we animate the dialog into the touch
|
* activity, which currently does not look good if we animate the dialog into the touch surface
|
||||||
* surface at the same time as the activity starts.
|
* at the same time as the activity starts.
|
||||||
*
|
*
|
||||||
* TODO(b/193634619): Remove this function and animate dialog into opening activity instead.
|
* TODO(b/193634619): Remove this function and animate dialog into opening activity instead.
|
||||||
*/
|
*/
|
||||||
@@ -295,8 +302,8 @@ private class AnimatedDialog(
|
|||||||
var touchSurface: View,
|
var touchSurface: View,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A callback that will be called with this [AnimatedDialog] after the dialog was
|
* A callback that will be called with this [AnimatedDialog] after the dialog was dismissed and
|
||||||
* dismissed and the exit animation is done.
|
* the exit animation is done.
|
||||||
*/
|
*/
|
||||||
private val onDialogDismissed: (AnimatedDialog) -> Unit,
|
private val onDialogDismissed: (AnimatedDialog) -> Unit,
|
||||||
|
|
||||||
@@ -333,9 +340,7 @@ private class AnimatedDialog(
|
|||||||
*/
|
*/
|
||||||
var dialogContentWithBackground: ViewGroup? = null
|
var dialogContentWithBackground: ViewGroup? = null
|
||||||
|
|
||||||
/**
|
/** The background color of [dialog], taking into consideration its window background color. */
|
||||||
* The background color of [dialog], taking into consideration its window background color.
|
|
||||||
*/
|
|
||||||
private var originalDialogBackgroundColor = Color.BLACK
|
private var originalDialogBackgroundColor = Color.BLACK
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -353,7 +358,8 @@ private class AnimatedDialog(
|
|||||||
private var isOriginalDialogViewLaidOut = false
|
private var isOriginalDialogViewLaidOut = false
|
||||||
|
|
||||||
/** A layout listener to animate the dialog height change. */
|
/** A layout listener to animate the dialog height change. */
|
||||||
private val backgroundLayoutListener = if (animateBackgroundBoundsChange) {
|
private val backgroundLayoutListener =
|
||||||
|
if (animateBackgroundBoundsChange) {
|
||||||
AnimatedBoundsLayoutListener()
|
AnimatedBoundsLayoutListener()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -381,37 +387,41 @@ private class AnimatedDialog(
|
|||||||
val window = dialog.window!!
|
val window = dialog.window!!
|
||||||
val isWindowFullScreen =
|
val isWindowFullScreen =
|
||||||
window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT
|
window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT
|
||||||
val dialogContentWithBackground = if (isWindowFullScreen) {
|
val dialogContentWithBackground =
|
||||||
// If the dialog window is already fullscreen, then we look for the first ViewGroup that
|
if (isWindowFullScreen) {
|
||||||
// has a background (and is not the DecorView, which always has a background) and
|
// If the dialog window is already fullscreen, then we look for the first ViewGroup
|
||||||
// animate towards that ViewGroup given that this is probably what represents the actual
|
// that has a background (and is not the DecorView, which always has a background)
|
||||||
// dialog view.
|
// and animate towards that ViewGroup given that this is probably what represents
|
||||||
|
// the actual dialog view.
|
||||||
var viewGroupWithBackground: ViewGroup? = null
|
var viewGroupWithBackground: ViewGroup? = null
|
||||||
for (i in 0 until decorView.childCount) {
|
for (i in 0 until decorView.childCount) {
|
||||||
viewGroupWithBackground = findFirstViewGroupWithBackground(decorView.getChildAt(i))
|
viewGroupWithBackground =
|
||||||
|
findFirstViewGroupWithBackground(decorView.getChildAt(i))
|
||||||
if (viewGroupWithBackground != null) {
|
if (viewGroupWithBackground != null) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animate that view with the background. Throw if we didn't find one, because otherwise
|
// Animate that view with the background. Throw if we didn't find one, because
|
||||||
|
// otherwise
|
||||||
// it's not clear what we should animate.
|
// it's not clear what we should animate.
|
||||||
viewGroupWithBackground
|
viewGroupWithBackground
|
||||||
?: throw IllegalStateException("Unable to find ViewGroup with background")
|
?: throw IllegalStateException("Unable to find ViewGroup with background")
|
||||||
} else {
|
} else {
|
||||||
// We will make the dialog window (and therefore its DecorView) fullscreen to make it
|
// We will make the dialog window (and therefore its DecorView) fullscreen to make
|
||||||
// possible to animate outside its bounds.
|
// it possible to animate outside its bounds.
|
||||||
//
|
//
|
||||||
// Before that, we add a new View as a child of the DecorView with the same size and
|
// Before that, we add a new View as a child of the DecorView with the same size and
|
||||||
// gravity as that DecorView, then we add all original children of the DecorView to that
|
// gravity as that DecorView, then we add all original children of the DecorView to
|
||||||
// new View. Finally we remove the background of the DecorView and add it to the new
|
// that new View. Finally we remove the background of the DecorView and add it to
|
||||||
// View, then we make the DecorView fullscreen. This new View now acts as a fake (non
|
// the new View, then we make the DecorView fullscreen. This new View now acts as a
|
||||||
// fullscreen) window.
|
// fake (non fullscreen) window.
|
||||||
//
|
//
|
||||||
// On top of that, we also add a fullscreen transparent background between the DecorView
|
// On top of that, we also add a fullscreen transparent background between the
|
||||||
// and the view that we added so that we can dismiss the dialog when this view is
|
// DecorView and the view that we added so that we can dismiss the dialog when this
|
||||||
// clicked. This is necessary because DecorView overrides onTouchEvent and therefore we
|
// view is clicked. This is necessary because DecorView overrides onTouchEvent and
|
||||||
// can't set the click listener directly on the (now fullscreen) DecorView.
|
// therefore we can't set the click listener directly on the (now fullscreen)
|
||||||
|
// DecorView.
|
||||||
val fullscreenTransparentBackground = FrameLayout(dialog.context)
|
val fullscreenTransparentBackground = FrameLayout(dialog.context)
|
||||||
decorView.addView(
|
decorView.addView(
|
||||||
fullscreenTransparentBackground,
|
fullscreenTransparentBackground,
|
||||||
@@ -422,17 +432,19 @@ private class AnimatedDialog(
|
|||||||
val dialogContentWithBackground = FrameLayout(dialog.context)
|
val dialogContentWithBackground = FrameLayout(dialog.context)
|
||||||
dialogContentWithBackground.background = decorView.background
|
dialogContentWithBackground.background = decorView.background
|
||||||
|
|
||||||
// Make the window background transparent. Note that setting the window (or DecorView)
|
// Make the window background transparent. Note that setting the window (or
|
||||||
// background drawable to null leads to issues with background color (not being
|
// DecorView) background drawable to null leads to issues with background color (not
|
||||||
// transparent) or with insets that are not refreshed. Therefore we need to set it to
|
// being transparent) or with insets that are not refreshed. Therefore we need to
|
||||||
// something not null, hence we are using android.R.color.transparent here.
|
// set it to something not null, hence we are using android.R.color.transparent
|
||||||
|
// here.
|
||||||
window.setBackgroundDrawableResource(android.R.color.transparent)
|
window.setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
|
|
||||||
// Close the dialog when clicking outside of it.
|
// Close the dialog when clicking outside of it.
|
||||||
fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
|
fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
|
||||||
dialogContentWithBackground.isClickable = true
|
dialogContentWithBackground.isClickable = true
|
||||||
|
|
||||||
// Make sure the transparent and dialog backgrounds are not focusable by accessibility
|
// Make sure the transparent and dialog backgrounds are not focusable by
|
||||||
|
// accessibility
|
||||||
// features.
|
// features.
|
||||||
fullscreenTransparentBackground.importantForAccessibility =
|
fullscreenTransparentBackground.importantForAccessibility =
|
||||||
View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||||
@@ -455,15 +467,26 @@ private class AnimatedDialog(
|
|||||||
dialogContentWithBackground.addView(view)
|
dialogContentWithBackground.addView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the window fullscreen and add a layout listener to ensure it stays fullscreen.
|
// Make the window fullscreen and add a layout listener to ensure it stays
|
||||||
|
// fullscreen.
|
||||||
window.setLayout(MATCH_PARENT, MATCH_PARENT)
|
window.setLayout(MATCH_PARENT, MATCH_PARENT)
|
||||||
decorViewLayoutListener = View.OnLayoutChangeListener {
|
decorViewLayoutListener =
|
||||||
v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
View.OnLayoutChangeListener {
|
||||||
if (window.attributes.width != MATCH_PARENT ||
|
v,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
oldLeft,
|
||||||
|
oldTop,
|
||||||
|
oldRight,
|
||||||
|
oldBottom ->
|
||||||
|
if (
|
||||||
|
window.attributes.width != MATCH_PARENT ||
|
||||||
window.attributes.height != MATCH_PARENT
|
window.attributes.height != MATCH_PARENT
|
||||||
) {
|
) {
|
||||||
// The dialog size changed, copy its size to dialogContentWithBackground and
|
// The dialog size changed, copy its size to dialogContentWithBackground
|
||||||
// make the dialog window full screen again.
|
// and make the dialog window full screen again.
|
||||||
val layoutParams = dialogContentWithBackground.layoutParams
|
val layoutParams = dialogContentWithBackground.layoutParams
|
||||||
layoutParams.width = window.attributes.width
|
layoutParams.width = window.attributes.width
|
||||||
layoutParams.height = window.attributes.height
|
layoutParams.height = window.attributes.height
|
||||||
@@ -482,7 +505,8 @@ private class AnimatedDialog(
|
|||||||
originalDialogBackgroundColor =
|
originalDialogBackgroundColor =
|
||||||
GhostedViewLaunchAnimatorController.findGradientDrawable(background)
|
GhostedViewLaunchAnimatorController.findGradientDrawable(background)
|
||||||
?.color
|
?.color
|
||||||
?.defaultColor ?: Color.BLACK
|
?.defaultColor
|
||||||
|
?: Color.BLACK
|
||||||
|
|
||||||
// Make the background view invisible until we start the animation. We use the transition
|
// Make the background view invisible until we start the animation. We use the transition
|
||||||
// visibility like GhostView does so that we don't mess up with the accessibility tree (see
|
// visibility like GhostView does so that we don't mess up with the accessibility tree (see
|
||||||
@@ -508,7 +532,8 @@ private class AnimatedDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start the animation once the background view is properly laid out.
|
// Start the animation once the background view is properly laid out.
|
||||||
dialogContentWithBackground.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
|
dialogContentWithBackground.addOnLayoutChangeListener(
|
||||||
|
object : View.OnLayoutChangeListener {
|
||||||
override fun onLayoutChange(
|
override fun onLayoutChange(
|
||||||
v: View,
|
v: View,
|
||||||
left: Int,
|
left: Int,
|
||||||
@@ -525,7 +550,8 @@ private class AnimatedDialog(
|
|||||||
isOriginalDialogViewLaidOut = true
|
isOriginalDialogViewLaidOut = true
|
||||||
maybeStartLaunchAnimation()
|
maybeStartLaunchAnimation()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Disable the dim. We will enable it once we start the animation.
|
// Disable the dim. We will enable it once we start the animation.
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
@@ -551,10 +577,12 @@ private class AnimatedDialog(
|
|||||||
// Create a ghost of the touch surface (which will make the touch surface invisible) and add
|
// Create a ghost of the touch surface (which will make the touch surface invisible) and add
|
||||||
// it to the host dialog. We trigger a one off synchronization to make sure that this is
|
// it to the host dialog. We trigger a one off synchronization to make sure that this is
|
||||||
// done in sync between the two different windows.
|
// done in sync between the two different windows.
|
||||||
synchronizeNextDraw(then = {
|
synchronizeNextDraw(
|
||||||
|
then = {
|
||||||
isTouchSurfaceGhostDrawn = true
|
isTouchSurfaceGhostDrawn = true
|
||||||
maybeStartLaunchAnimation()
|
maybeStartLaunchAnimation()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
GhostView.addGhost(touchSurface, decorView)
|
GhostView.addGhost(touchSurface, decorView)
|
||||||
|
|
||||||
// The ghost of the touch surface was just created, so the touch surface is currently
|
// The ghost of the touch surface was just created, so the touch surface is currently
|
||||||
@@ -616,14 +644,13 @@ private class AnimatedDialog(
|
|||||||
onLaunchAnimationEnd = {
|
onLaunchAnimationEnd = {
|
||||||
touchSurface.setTag(R.id.tag_launch_animation_running, null)
|
touchSurface.setTag(R.id.tag_launch_animation_running, null)
|
||||||
|
|
||||||
// We hide the touch surface when the dialog is showing. We will make this
|
// We hide the touch surface when the dialog is showing. We will make this view
|
||||||
// view visible again when dismissing the dialog.
|
// visible again when dismissing the dialog.
|
||||||
touchSurface.visibility = View.INVISIBLE
|
touchSurface.visibility = View.INVISIBLE
|
||||||
|
|
||||||
isLaunching = false
|
isLaunching = false
|
||||||
|
|
||||||
// dismiss was called during the animation, dismiss again now to actually
|
// dismiss was called during the animation, dismiss again now to actually dismiss.
|
||||||
// dismiss.
|
|
||||||
if (dismissRequested) {
|
if (dismissRequested) {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
@@ -632,8 +659,9 @@ private class AnimatedDialog(
|
|||||||
// at the end of the launch animation, because the lauch animation already correctly
|
// at the end of the launch animation, because the lauch animation already correctly
|
||||||
// handles bounds changes.
|
// handles bounds changes.
|
||||||
if (backgroundLayoutListener != null) {
|
if (backgroundLayoutListener != null) {
|
||||||
dialogContentWithBackground!!
|
dialogContentWithBackground!!.addOnLayoutChangeListener(
|
||||||
.addOnLayoutChangeListener(backgroundLayoutListener)
|
backgroundLayoutListener
|
||||||
|
)
|
||||||
}
|
}
|
||||||
cuj?.run { interactionJankMonitor.end(cujType) }
|
cuj?.run { interactionJankMonitor.end(cujType) }
|
||||||
}
|
}
|
||||||
@@ -711,16 +739,19 @@ private class AnimatedDialog(
|
|||||||
dialogContentWithBackground.visibility = View.INVISIBLE
|
dialogContentWithBackground.visibility = View.INVISIBLE
|
||||||
|
|
||||||
if (backgroundLayoutListener != null) {
|
if (backgroundLayoutListener != null) {
|
||||||
dialogContentWithBackground
|
dialogContentWithBackground.removeOnLayoutChangeListener(
|
||||||
.removeOnLayoutChangeListener(backgroundLayoutListener)
|
backgroundLayoutListener
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure that the removal of the ghost and making the touch surface visible is
|
// Make sure that the removal of the ghost and making the touch surface visible is
|
||||||
// done at the same time.
|
// done at the same time.
|
||||||
synchronizeNextDraw(then = {
|
synchronizeNextDraw(
|
||||||
|
then = {
|
||||||
onAnimationFinished(true /* instantDismiss */)
|
onAnimationFinished(true /* instantDismiss */)
|
||||||
onDialogDismissed(this@AnimatedDialog)
|
onDialogDismissed(this@AnimatedDialog)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -740,7 +771,8 @@ private class AnimatedDialog(
|
|||||||
endViewController.launchContainer = decorView
|
endViewController.launchContainer = decorView
|
||||||
|
|
||||||
val endState = endViewController.createAnimatorState()
|
val endState = endViewController.createAnimatorState()
|
||||||
val controller = object : LaunchAnimator.Controller {
|
val controller =
|
||||||
|
object : LaunchAnimator.Controller {
|
||||||
override var launchContainer: ViewGroup
|
override var launchContainer: ViewGroup
|
||||||
get() = startViewController.launchContainer
|
get() = startViewController.launchContainer
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -753,8 +785,8 @@ private class AnimatedDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
|
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
|
||||||
// During launch, onLaunchAnimationStart will be used to remove the temporary touch
|
// During launch, onLaunchAnimationStart will be used to remove the temporary
|
||||||
// surface ghost so it is important to call this before calling
|
// touch surface ghost so it is important to call this before calling
|
||||||
// onLaunchAnimationStart on the controller (which will create its own ghost).
|
// onLaunchAnimationStart on the controller (which will create its own ghost).
|
||||||
onLaunchAnimationStart()
|
onLaunchAnimationStart()
|
||||||
|
|
||||||
@@ -780,12 +812,12 @@ private class AnimatedDialog(
|
|||||||
state.visible = !state.visible
|
state.visible = !state.visible
|
||||||
endViewController.onLaunchAnimationProgress(state, progress, linearProgress)
|
endViewController.onLaunchAnimationProgress(state, progress, linearProgress)
|
||||||
|
|
||||||
// If the dialog content is complex, its dimension might change during the launch
|
// If the dialog content is complex, its dimension might change during the
|
||||||
// animation. The animation end position might also change during the exit
|
// launch animation. The animation end position might also change during the
|
||||||
// animation, for instance when locking the phone when the dialog is open. Therefore
|
// exit animation, for instance when locking the phone when the dialog is open.
|
||||||
// we update the end state to the new position/size. Usually the dialog dimension or
|
// Therefore we update the end state to the new position/size. Usually the
|
||||||
// position will change in the early frames, so changing the end state shouldn't
|
// dialog dimension or position will change in the early frames, so changing the
|
||||||
// really be noticeable.
|
// end state shouldn't really be noticeable.
|
||||||
endViewController.fillGhostedViewState(endState)
|
endViewController.fillGhostedViewState(endState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -866,15 +898,18 @@ private class AnimatedDialog(
|
|||||||
currentAnimator?.cancel()
|
currentAnimator?.cancel()
|
||||||
currentAnimator = null
|
currentAnimator = null
|
||||||
|
|
||||||
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
|
val animator =
|
||||||
|
ValueAnimator.ofFloat(0f, 1f).apply {
|
||||||
duration = ANIMATION_DURATION
|
duration = ANIMATION_DURATION
|
||||||
interpolator = Interpolators.STANDARD
|
interpolator = Interpolators.STANDARD
|
||||||
|
|
||||||
addListener(object : AnimatorListenerAdapter() {
|
addListener(
|
||||||
|
object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
currentAnimator = null
|
currentAnimator = null
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
addUpdateListener { animatedValue ->
|
addUpdateListener { animatedValue ->
|
||||||
val progress = animatedValue.animatedFraction
|
val progress = animatedValue.animatedFraction
|
||||||
|
|||||||
@@ -105,9 +105,7 @@ open class GhostedViewLaunchAnimatorController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform a BFS to find the largest View with background.
|
// Perform a BFS to find the largest View with background.
|
||||||
val views = LinkedList<View>().apply {
|
val views = LinkedList<View>().apply { add(view) }
|
||||||
add(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
while (views.isNotEmpty()) {
|
while (views.isNotEmpty()) {
|
||||||
val v = views.removeFirst()
|
val v = views.removeFirst()
|
||||||
@@ -161,7 +159,8 @@ open class GhostedViewLaunchAnimatorController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createAnimatorState(): LaunchAnimator.State {
|
override fun createAnimatorState(): LaunchAnimator.State {
|
||||||
val state = LaunchAnimator.State(
|
val state =
|
||||||
|
LaunchAnimator.State(
|
||||||
topCornerRadius = getCurrentTopCornerRadius(),
|
topCornerRadius = getCurrentTopCornerRadius(),
|
||||||
bottomCornerRadius = getCurrentBottomCornerRadius()
|
bottomCornerRadius = getCurrentBottomCornerRadius()
|
||||||
)
|
)
|
||||||
@@ -255,7 +254,8 @@ open class GhostedViewLaunchAnimatorController(
|
|||||||
|
|
||||||
launchContainer.getLocationOnScreen(launchContainerLocation)
|
launchContainer.getLocationOnScreen(launchContainerLocation)
|
||||||
ghostViewMatrix.postScale(
|
ghostViewMatrix.postScale(
|
||||||
scale, scale,
|
scale,
|
||||||
|
scale,
|
||||||
ghostedViewState.centerX - launchContainerLocation[0],
|
ghostedViewState.centerX - launchContainerLocation[0],
|
||||||
ghostedViewState.centerY - launchContainerLocation[1]
|
ghostedViewState.centerY - launchContainerLocation[1]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ import kotlin.math.roundToInt
|
|||||||
private const val TAG = "LaunchAnimator"
|
private const val TAG = "LaunchAnimator"
|
||||||
|
|
||||||
/** A base class to animate a window launch (activity or dialog) from a view . */
|
/** A base class to animate a window launch (activity or dialog) from a view . */
|
||||||
class LaunchAnimator(
|
class LaunchAnimator(private val timings: Timings, private val interpolators: Interpolators) {
|
||||||
private val timings: Timings,
|
|
||||||
private val interpolators: Interpolators
|
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
internal const val DEBUG = false
|
internal const val DEBUG = false
|
||||||
private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
|
private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
|
||||||
@@ -77,8 +74,8 @@ class LaunchAnimator(
|
|||||||
* This will be used to:
|
* This will be used to:
|
||||||
* - Get the associated [Context].
|
* - Get the associated [Context].
|
||||||
* - Compute whether we are expanding fully above the launch container.
|
* - Compute whether we are expanding fully above the launch container.
|
||||||
* - Get to overlay to which we initially put the window background layer, until the
|
* - Get to overlay to which we initially put the window background layer, until the opening
|
||||||
* opening window is made visible (see [openingWindowSyncView]).
|
* window is made visible (see [openingWindowSyncView]).
|
||||||
*
|
*
|
||||||
* This container can be changed to force this [Controller] to animate the expanding view
|
* 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
|
* inside a different location, for instance to ensure correct layering during the
|
||||||
@@ -132,7 +129,6 @@ class LaunchAnimator(
|
|||||||
var bottom: Int = 0,
|
var bottom: Int = 0,
|
||||||
var left: Int = 0,
|
var left: Int = 0,
|
||||||
var right: Int = 0,
|
var right: Int = 0,
|
||||||
|
|
||||||
var topCornerRadius: Float = 0f,
|
var topCornerRadius: Float = 0f,
|
||||||
var bottomCornerRadius: Float = 0f
|
var bottomCornerRadius: Float = 0f
|
||||||
) {
|
) {
|
||||||
@@ -202,18 +198,20 @@ class LaunchAnimator(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a launch animation controlled by [controller] towards [endState]. An intermediary
|
* Start a launch animation controlled by [controller] towards [endState]. An intermediary layer
|
||||||
* layer with [windowBackgroundColor] will fade in then fade out above the expanding view, and
|
* with [windowBackgroundColor] will fade in then (optionally) fade out above the expanding
|
||||||
* should be the same background color as the opening (or closing) window. If [drawHole] is
|
* view, and should be the same background color as the opening (or closing) window.
|
||||||
* 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.
|
* If [fadeOutWindowBackgroundLayer] is true, then this intermediary layer will fade out during
|
||||||
|
* the second half of the animation, and will have SRC blending mode (ultimately punching a hole
|
||||||
|
* in the [launch container][Controller.launchContainer]) iff [drawHole] is true.
|
||||||
*/
|
*/
|
||||||
fun startAnimation(
|
fun startAnimation(
|
||||||
controller: Controller,
|
controller: Controller,
|
||||||
endState: State,
|
endState: State,
|
||||||
windowBackgroundColor: Int,
|
windowBackgroundColor: Int,
|
||||||
drawHole: Boolean = false
|
fadeOutWindowBackgroundLayer: Boolean = true,
|
||||||
|
drawHole: Boolean = false,
|
||||||
): Animation {
|
): Animation {
|
||||||
val state = controller.createAnimatorState()
|
val state = controller.createAnimatorState()
|
||||||
|
|
||||||
@@ -238,8 +236,12 @@ class LaunchAnimator(
|
|||||||
val endBottomCornerRadius = endState.bottomCornerRadius
|
val endBottomCornerRadius = endState.bottomCornerRadius
|
||||||
|
|
||||||
fun maybeUpdateEndState() {
|
fun maybeUpdateEndState() {
|
||||||
if (endTop != endState.top || endBottom != endState.bottom ||
|
if (
|
||||||
endLeft != endState.left || endRight != endState.right) {
|
endTop != endState.top ||
|
||||||
|
endBottom != endState.bottom ||
|
||||||
|
endLeft != endState.left ||
|
||||||
|
endRight != endState.right
|
||||||
|
) {
|
||||||
endTop = endState.top
|
endTop = endState.top
|
||||||
endBottom = endState.bottom
|
endBottom = endState.bottom
|
||||||
endLeft = endState.left
|
endLeft = endState.left
|
||||||
@@ -256,7 +258,8 @@ class LaunchAnimator(
|
|||||||
// color, which is usually the same color of the app background. We first fade in this layer
|
// 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
|
// 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.
|
// launch container and reveal the opening window.
|
||||||
val windowBackgroundLayer = GradientDrawable().apply {
|
val windowBackgroundLayer =
|
||||||
|
GradientDrawable().apply {
|
||||||
setColor(windowBackgroundColor)
|
setColor(windowBackgroundColor)
|
||||||
alpha = 0
|
alpha = 0
|
||||||
}
|
}
|
||||||
@@ -270,14 +273,16 @@ class LaunchAnimator(
|
|||||||
// [Controller.openingWindowSyncView] once the opening app window starts to be visible.
|
// [Controller.openingWindowSyncView] once the opening app window starts to be visible.
|
||||||
val openingWindowSyncView = controller.openingWindowSyncView
|
val openingWindowSyncView = controller.openingWindowSyncView
|
||||||
val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
|
val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
|
||||||
val moveBackgroundLayerWhenAppIsVisible = openingWindowSyncView != null &&
|
val moveBackgroundLayerWhenAppIsVisible =
|
||||||
|
openingWindowSyncView != null &&
|
||||||
openingWindowSyncView.viewRootImpl != controller.launchContainer.viewRootImpl
|
openingWindowSyncView.viewRootImpl != controller.launchContainer.viewRootImpl
|
||||||
|
|
||||||
val launchContainerOverlay = launchContainer.overlay
|
val launchContainerOverlay = launchContainer.overlay
|
||||||
var cancelled = false
|
var cancelled = false
|
||||||
var movedBackgroundLayer = false
|
var movedBackgroundLayer = false
|
||||||
|
|
||||||
animator.addListener(object : AnimatorListenerAdapter() {
|
animator.addListener(
|
||||||
|
object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
|
override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Animation started")
|
Log.d(TAG, "Animation started")
|
||||||
@@ -301,7 +306,8 @@ class LaunchAnimator(
|
|||||||
openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
|
openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
animator.addUpdateListener { animation ->
|
animator.addUpdateListener { animation ->
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
@@ -333,7 +339,8 @@ class LaunchAnimator(
|
|||||||
|
|
||||||
// The expanding view can/should be hidden once it is completely covered by the opening
|
// The expanding view can/should be hidden once it is completely covered by the opening
|
||||||
// window.
|
// window.
|
||||||
state.visible = getProgress(
|
state.visible =
|
||||||
|
getProgress(
|
||||||
timings,
|
timings,
|
||||||
linearProgress,
|
linearProgress,
|
||||||
timings.contentBeforeFadeOutDelay,
|
timings.contentBeforeFadeOutDelay,
|
||||||
@@ -352,7 +359,8 @@ class LaunchAnimator(
|
|||||||
ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {})
|
ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {})
|
||||||
}
|
}
|
||||||
|
|
||||||
val container = if (movedBackgroundLayer) {
|
val container =
|
||||||
|
if (movedBackgroundLayer) {
|
||||||
openingWindowSyncView!!
|
openingWindowSyncView!!
|
||||||
} else {
|
} else {
|
||||||
controller.launchContainer
|
controller.launchContainer
|
||||||
@@ -363,6 +371,7 @@ class LaunchAnimator(
|
|||||||
state,
|
state,
|
||||||
linearProgress,
|
linearProgress,
|
||||||
container,
|
container,
|
||||||
|
fadeOutWindowBackgroundLayer,
|
||||||
drawHole
|
drawHole
|
||||||
)
|
)
|
||||||
controller.onLaunchAnimationProgress(state, progress, linearProgress)
|
controller.onLaunchAnimationProgress(state, progress, linearProgress)
|
||||||
@@ -391,6 +400,7 @@ class LaunchAnimator(
|
|||||||
state: State,
|
state: State,
|
||||||
linearProgress: Float,
|
linearProgress: Float,
|
||||||
launchContainer: View,
|
launchContainer: View,
|
||||||
|
fadeOutWindowBackgroundLayer: Boolean,
|
||||||
drawHole: Boolean
|
drawHole: Boolean
|
||||||
) {
|
) {
|
||||||
// Update position.
|
// Update position.
|
||||||
@@ -415,7 +425,8 @@ class LaunchAnimator(
|
|||||||
|
|
||||||
// We first fade in the background layer to hide the expanding view, then fade it out
|
// 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.
|
// with SRC mode to draw a hole punch in the status bar and reveal the opening window.
|
||||||
val fadeInProgress = getProgress(
|
val fadeInProgress =
|
||||||
|
getProgress(
|
||||||
timings,
|
timings,
|
||||||
linearProgress,
|
linearProgress,
|
||||||
timings.contentBeforeFadeOutDelay,
|
timings.contentBeforeFadeOutDelay,
|
||||||
@@ -425,8 +436,9 @@ class LaunchAnimator(
|
|||||||
val alpha =
|
val alpha =
|
||||||
interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress)
|
interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress)
|
||||||
drawable.alpha = (alpha * 0xFF).roundToInt()
|
drawable.alpha = (alpha * 0xFF).roundToInt()
|
||||||
} else {
|
} else if (fadeOutWindowBackgroundLayer) {
|
||||||
val fadeOutProgress = getProgress(
|
val fadeOutProgress =
|
||||||
|
getProgress(
|
||||||
timings,
|
timings,
|
||||||
linearProgress,
|
linearProgress,
|
||||||
timings.contentAfterFadeInDelay,
|
timings.contentAfterFadeInDelay,
|
||||||
@@ -439,6 +451,8 @@ class LaunchAnimator(
|
|||||||
if (drawHole) {
|
if (drawHole) {
|
||||||
drawable.setXfermode(SRC_MODE)
|
drawable.setXfermode(SRC_MODE)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
drawable.alpha = 0xFF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class RemoteTransitionAdapter {
|
|||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Almost a copy of Transitions#setupStartState.
|
* Almost a copy of Transitions#setupStartState.
|
||||||
|
*
|
||||||
* TODO: remove when there is proper cross-process transaction sync.
|
* TODO: remove when there is proper cross-process transaction sync.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
@@ -50,7 +51,8 @@ class RemoteTransitionAdapter {
|
|||||||
info: TransitionInfo,
|
info: TransitionInfo,
|
||||||
t: SurfaceControl.Transaction
|
t: SurfaceControl.Transaction
|
||||||
) {
|
) {
|
||||||
val isOpening = info.type == WindowManager.TRANSIT_OPEN ||
|
val isOpening =
|
||||||
|
info.type == WindowManager.TRANSIT_OPEN ||
|
||||||
info.type == WindowManager.TRANSIT_TO_FRONT
|
info.type == WindowManager.TRANSIT_TO_FRONT
|
||||||
// Put animating stuff above this line and put static stuff below it.
|
// Put animating stuff above this line and put static stuff below it.
|
||||||
val zSplitLine = info.changes.size
|
val zSplitLine = info.changes.size
|
||||||
@@ -59,15 +61,19 @@ class RemoteTransitionAdapter {
|
|||||||
|
|
||||||
// Launcher animates leaf tasks directly, so always reparent all task leashes to root.
|
// Launcher animates leaf tasks directly, so always reparent all task leashes to root.
|
||||||
t.reparent(leash, info.rootLeash)
|
t.reparent(leash, info.rootLeash)
|
||||||
t.setPosition(leash, (change.startAbsBounds.left - info.rootOffset.x).toFloat(), (
|
t.setPosition(
|
||||||
change.startAbsBounds.top - info.rootOffset.y).toFloat())
|
leash,
|
||||||
|
(change.startAbsBounds.left - info.rootOffset.x).toFloat(),
|
||||||
|
(change.startAbsBounds.top - info.rootOffset.y).toFloat()
|
||||||
|
)
|
||||||
t.show(leash)
|
t.show(leash)
|
||||||
// Put all the OPEN/SHOW on top
|
// Put all the OPEN/SHOW on top
|
||||||
if (mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT) {
|
if (mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT) {
|
||||||
if (isOpening) {
|
if (isOpening) {
|
||||||
t.setLayer(leash, zSplitLine + info.changes.size - layer)
|
t.setLayer(leash, zSplitLine + info.changes.size - layer)
|
||||||
if (change.flags
|
if (
|
||||||
and TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT == 0) {
|
change.flags and TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT == 0
|
||||||
|
) {
|
||||||
// if transferred, it should be left visible.
|
// if transferred, it should be left visible.
|
||||||
t.setAlpha(leash, 0f)
|
t.setAlpha(leash, 0f)
|
||||||
}
|
}
|
||||||
@@ -75,8 +81,9 @@ class RemoteTransitionAdapter {
|
|||||||
// put on bottom and leave it visible
|
// put on bottom and leave it visible
|
||||||
t.setLayer(leash, zSplitLine - layer)
|
t.setLayer(leash, zSplitLine - layer)
|
||||||
}
|
}
|
||||||
} else if (mode == WindowManager.TRANSIT_CLOSE ||
|
} else if (
|
||||||
mode == WindowManager.TRANSIT_TO_BACK) {
|
mode == WindowManager.TRANSIT_CLOSE || mode == WindowManager.TRANSIT_TO_BACK
|
||||||
|
) {
|
||||||
if (isOpening) {
|
if (isOpening) {
|
||||||
// put on bottom and leave visible
|
// put on bottom and leave visible
|
||||||
t.setLayer(leash, zSplitLine - layer)
|
t.setLayer(leash, zSplitLine - layer)
|
||||||
@@ -102,10 +109,15 @@ class RemoteTransitionAdapter {
|
|||||||
// making leashes means we have to handle them specially.
|
// making leashes means we have to handle them specially.
|
||||||
return change.leash
|
return change.leash
|
||||||
}
|
}
|
||||||
val leashSurface = SurfaceControl.Builder()
|
val leashSurface =
|
||||||
|
SurfaceControl.Builder()
|
||||||
.setName(change.leash.toString() + "_transition-leash")
|
.setName(change.leash.toString() + "_transition-leash")
|
||||||
.setContainerLayer().setParent(if (change.parent == null)
|
.setContainerLayer()
|
||||||
info.rootLeash else info.getChange(change.parent!!)!!.leash).build()
|
.setParent(
|
||||||
|
if (change.parent == null) info.rootLeash
|
||||||
|
else info.getChange(change.parent!!)!!.leash
|
||||||
|
)
|
||||||
|
.build()
|
||||||
// Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
|
// Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
|
||||||
setupLeash(leashSurface, change, info.changes.size - order, info, t)
|
setupLeash(leashSurface, change, info.changes.size - order, info, t)
|
||||||
t.reparent(change.leash, leashSurface)
|
t.reparent(change.leash, leashSurface)
|
||||||
@@ -118,10 +130,10 @@ class RemoteTransitionAdapter {
|
|||||||
|
|
||||||
private fun newModeToLegacyMode(newMode: Int): Int {
|
private fun newModeToLegacyMode(newMode: Int): Int {
|
||||||
return when (newMode) {
|
return when (newMode) {
|
||||||
WindowManager.TRANSIT_OPEN, WindowManager.TRANSIT_TO_FRONT
|
WindowManager.TRANSIT_OPEN,
|
||||||
-> RemoteAnimationTarget.MODE_OPENING
|
WindowManager.TRANSIT_TO_FRONT -> RemoteAnimationTarget.MODE_OPENING
|
||||||
WindowManager.TRANSIT_CLOSE, WindowManager.TRANSIT_TO_BACK
|
WindowManager.TRANSIT_CLOSE,
|
||||||
-> RemoteAnimationTarget.MODE_CLOSING
|
WindowManager.TRANSIT_TO_BACK -> RemoteAnimationTarget.MODE_CLOSING
|
||||||
else -> RemoteAnimationTarget.MODE_CHANGING
|
else -> RemoteAnimationTarget.MODE_CHANGING
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +150,8 @@ class RemoteTransitionAdapter {
|
|||||||
info: TransitionInfo,
|
info: TransitionInfo,
|
||||||
t: SurfaceControl.Transaction
|
t: SurfaceControl.Transaction
|
||||||
): RemoteAnimationTarget {
|
): RemoteAnimationTarget {
|
||||||
val target = RemoteAnimationTarget(
|
val target =
|
||||||
|
RemoteAnimationTarget(
|
||||||
/* taskId */ if (change.taskInfo != null) change.taskInfo!!.taskId else -1,
|
/* taskId */ if (change.taskInfo != null) change.taskInfo!!.taskId else -1,
|
||||||
/* mode */ newModeToLegacyMode(change.mode),
|
/* mode */ newModeToLegacyMode(change.mode),
|
||||||
/* leash */ createLeash(info, change, order, t),
|
/* leash */ createLeash(info, change, order, t),
|
||||||
@@ -151,15 +164,16 @@ class RemoteTransitionAdapter {
|
|||||||
/* localBounds */ rectOffsetTo(change.endAbsBounds, change.endRelOffset),
|
/* localBounds */ rectOffsetTo(change.endAbsBounds, change.endRelOffset),
|
||||||
/* screenSpaceBounds */ Rect(change.endAbsBounds),
|
/* screenSpaceBounds */ Rect(change.endAbsBounds),
|
||||||
/* windowConfig */ if (change.taskInfo != null)
|
/* windowConfig */ if (change.taskInfo != null)
|
||||||
change.taskInfo!!.configuration.windowConfiguration else
|
change.taskInfo!!.configuration.windowConfiguration
|
||||||
WindowConfiguration(),
|
else WindowConfiguration(),
|
||||||
/* isNotInRecents */ if (change.taskInfo != null)
|
/* isNotInRecents */ if (change.taskInfo != null) !change.taskInfo!!.isRunning
|
||||||
!change.taskInfo!!.isRunning else true,
|
else true,
|
||||||
/* startLeash */ null,
|
/* startLeash */ null,
|
||||||
/* startBounds */ Rect(change.startAbsBounds),
|
/* startBounds */ Rect(change.startAbsBounds),
|
||||||
/* taskInfo */ change.taskInfo,
|
/* taskInfo */ change.taskInfo,
|
||||||
/* allowEnterPip */ change.allowEnterPip,
|
/* allowEnterPip */ change.allowEnterPip,
|
||||||
/* windowType */ WindowManager.LayoutParams.INVALID_WINDOW_TYPE)
|
/* windowType */ WindowManager.LayoutParams.INVALID_WINDOW_TYPE
|
||||||
|
)
|
||||||
target.backgroundColor = change.backgroundColor
|
target.backgroundColor = change.backgroundColor
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
@@ -192,9 +206,7 @@ class RemoteTransitionAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun adaptRemoteRunner(
|
fun adaptRemoteRunner(runner: IRemoteAnimationRunner): IRemoteTransition.Stub {
|
||||||
runner: IRemoteAnimationRunner
|
|
||||||
): IRemoteTransition.Stub {
|
|
||||||
return object : IRemoteTransition.Stub() {
|
return object : IRemoteTransition.Stub() {
|
||||||
override fun startAnimation(
|
override fun startAnimation(
|
||||||
token: IBinder,
|
token: IBinder,
|
||||||
@@ -218,18 +230,24 @@ class RemoteTransitionAdapter {
|
|||||||
var displayH = 0f
|
var displayH = 0f
|
||||||
for (i in info.changes.indices.reversed()) {
|
for (i in info.changes.indices.reversed()) {
|
||||||
val change = info.changes[i]
|
val change = info.changes[i]
|
||||||
if (change.taskInfo != null &&
|
if (
|
||||||
change.taskInfo!!.activityType
|
change.taskInfo != null &&
|
||||||
== WindowConfiguration.ACTIVITY_TYPE_HOME) {
|
change.taskInfo!!.activityType ==
|
||||||
isReturnToHome = (change.mode == WindowManager.TRANSIT_OPEN ||
|
WindowConfiguration.ACTIVITY_TYPE_HOME
|
||||||
|
) {
|
||||||
|
isReturnToHome =
|
||||||
|
(change.mode == WindowManager.TRANSIT_OPEN ||
|
||||||
change.mode == WindowManager.TRANSIT_TO_FRONT)
|
change.mode == WindowManager.TRANSIT_TO_FRONT)
|
||||||
launcherTask = change
|
launcherTask = change
|
||||||
launcherLayer = info.changes.size - i
|
launcherLayer = info.changes.size - i
|
||||||
} else if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) {
|
} else if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) {
|
||||||
wallpaper = change
|
wallpaper = change
|
||||||
}
|
}
|
||||||
if (change.parent == null && change.endRotation >= 0 &&
|
if (
|
||||||
change.endRotation != change.startRotation) {
|
change.parent == null &&
|
||||||
|
change.endRotation >= 0 &&
|
||||||
|
change.endRotation != change.startRotation
|
||||||
|
) {
|
||||||
rotateDelta = change.endRotation - change.startRotation
|
rotateDelta = change.endRotation - change.startRotation
|
||||||
displayW = change.endAbsBounds.width().toFloat()
|
displayW = change.endAbsBounds.width().toFloat()
|
||||||
displayH = change.endAbsBounds.height().toFloat()
|
displayH = change.endAbsBounds.height().toFloat()
|
||||||
@@ -240,8 +258,13 @@ class RemoteTransitionAdapter {
|
|||||||
val counterLauncher = CounterRotator()
|
val counterLauncher = CounterRotator()
|
||||||
val counterWallpaper = CounterRotator()
|
val counterWallpaper = CounterRotator()
|
||||||
if (launcherTask != null && rotateDelta != 0 && launcherTask.parent != null) {
|
if (launcherTask != null && rotateDelta != 0 && launcherTask.parent != null) {
|
||||||
counterLauncher.setup(t, info.getChange(launcherTask.parent!!)!!.leash,
|
counterLauncher.setup(
|
||||||
rotateDelta, displayW, displayH)
|
t,
|
||||||
|
info.getChange(launcherTask.parent!!)!!.leash,
|
||||||
|
rotateDelta,
|
||||||
|
displayW,
|
||||||
|
displayH
|
||||||
|
)
|
||||||
if (counterLauncher.surface != null) {
|
if (counterLauncher.surface != null) {
|
||||||
t.setLayer(counterLauncher.surface!!, launcherLayer)
|
t.setLayer(counterLauncher.surface!!, launcherLayer)
|
||||||
}
|
}
|
||||||
@@ -257,8 +280,10 @@ class RemoteTransitionAdapter {
|
|||||||
val mode = info.changes[i].mode
|
val mode = info.changes[i].mode
|
||||||
// Only deal with independent layers
|
// Only deal with independent layers
|
||||||
if (!TransitionInfo.isIndependent(change, info)) continue
|
if (!TransitionInfo.isIndependent(change, info)) continue
|
||||||
if (mode == WindowManager.TRANSIT_CLOSE ||
|
if (
|
||||||
mode == WindowManager.TRANSIT_TO_BACK) {
|
mode == WindowManager.TRANSIT_CLOSE ||
|
||||||
|
mode == WindowManager.TRANSIT_TO_BACK
|
||||||
|
) {
|
||||||
t.setLayer(leash!!, info.changes.size * 3 - i)
|
t.setLayer(leash!!, info.changes.size * 3 - i)
|
||||||
counterLauncher.addChild(t, leash)
|
counterLauncher.addChild(t, leash)
|
||||||
}
|
}
|
||||||
@@ -273,8 +298,13 @@ class RemoteTransitionAdapter {
|
|||||||
counterLauncher.addChild(t, leashMap[launcherTask.leash])
|
counterLauncher.addChild(t, leashMap[launcherTask.leash])
|
||||||
}
|
}
|
||||||
if (wallpaper != null && rotateDelta != 0 && wallpaper.parent != null) {
|
if (wallpaper != null && rotateDelta != 0 && wallpaper.parent != null) {
|
||||||
counterWallpaper.setup(t, info.getChange(wallpaper.parent!!)!!.leash,
|
counterWallpaper.setup(
|
||||||
rotateDelta, displayW, displayH)
|
t,
|
||||||
|
info.getChange(wallpaper.parent!!)!!.leash,
|
||||||
|
rotateDelta,
|
||||||
|
displayW,
|
||||||
|
displayH
|
||||||
|
)
|
||||||
if (counterWallpaper.surface != null) {
|
if (counterWallpaper.surface != null) {
|
||||||
t.setLayer(counterWallpaper.surface!!, -1)
|
t.setLayer(counterWallpaper.surface!!, -1)
|
||||||
counterWallpaper.addChild(t, leashMap[wallpaper.leash])
|
counterWallpaper.addChild(t, leashMap[wallpaper.leash])
|
||||||
@@ -282,13 +312,14 @@ class RemoteTransitionAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.apply()
|
t.apply()
|
||||||
val animationFinishedCallback = object : IRemoteAnimationFinishedCallback {
|
val animationFinishedCallback =
|
||||||
|
object : IRemoteAnimationFinishedCallback {
|
||||||
override fun onAnimationFinished() {
|
override fun onAnimationFinished() {
|
||||||
val finishTransaction = SurfaceControl.Transaction()
|
val finishTransaction = SurfaceControl.Transaction()
|
||||||
counterLauncher.cleanUp(finishTransaction)
|
counterLauncher.cleanUp(finishTransaction)
|
||||||
counterWallpaper.cleanUp(finishTransaction)
|
counterWallpaper.cleanUp(finishTransaction)
|
||||||
// Release surface references now. This is apparently to free GPU memory
|
// Release surface references now. This is apparently to free GPU
|
||||||
// while doing quick operations (eg. during CTS).
|
// memory while doing quick operations (eg. during CTS).
|
||||||
for (i in info.changes.indices.reversed()) {
|
for (i in info.changes.indices.reversed()) {
|
||||||
info.changes[i].leash.release()
|
info.changes[i].leash.release()
|
||||||
}
|
}
|
||||||
@@ -296,11 +327,17 @@ class RemoteTransitionAdapter {
|
|||||||
leashMap.valueAt(i).release()
|
leashMap.valueAt(i).release()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
finishCallback.onTransitionFinished(null /* wct */,
|
finishCallback.onTransitionFinished(
|
||||||
finishTransaction)
|
null /* wct */,
|
||||||
|
finishTransaction
|
||||||
|
)
|
||||||
} catch (e: RemoteException) {
|
} catch (e: RemoteException) {
|
||||||
Log.e("ActivityOptionsCompat", "Failed to call app controlled" +
|
Log.e(
|
||||||
" animation finished callback", e)
|
"ActivityOptionsCompat",
|
||||||
|
"Failed to call app controlled" +
|
||||||
|
" animation finished callback",
|
||||||
|
e
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,8 +348,11 @@ class RemoteTransitionAdapter {
|
|||||||
// TODO(bc-unlcok): Pass correct transit type.
|
// TODO(bc-unlcok): Pass correct transit type.
|
||||||
runner.onAnimationStart(
|
runner.onAnimationStart(
|
||||||
WindowManager.TRANSIT_OLD_NONE,
|
WindowManager.TRANSIT_OLD_NONE,
|
||||||
appsCompat, wallpapersCompat, nonAppsCompat,
|
appsCompat,
|
||||||
animationFinishedCallback)
|
wallpapersCompat,
|
||||||
|
nonAppsCompat,
|
||||||
|
animationFinishedCallback
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mergeAnimation(
|
override fun mergeAnimation(
|
||||||
@@ -329,16 +369,12 @@ class RemoteTransitionAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun adaptRemoteAnimation(
|
fun adaptRemoteAnimation(adapter: RemoteAnimationAdapter): RemoteTransition {
|
||||||
adapter: RemoteAnimationAdapter
|
|
||||||
): RemoteTransition {
|
|
||||||
return RemoteTransition(adaptRemoteRunner(adapter.runner), adapter.callingApplication)
|
return RemoteTransition(adaptRemoteRunner(adapter.runner), adapter.callingApplication)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Utility class that takes care of counter-rotating surfaces during a transition animation. */
|
||||||
* Utility class that takes care of counter-rotating surfaces during a transition animation.
|
|
||||||
*/
|
|
||||||
class CounterRotator {
|
class CounterRotator {
|
||||||
/** Gets the surface with the counter-rotation. */
|
/** Gets the surface with the counter-rotation. */
|
||||||
var surface: SurfaceControl? = null
|
var surface: SurfaceControl? = null
|
||||||
@@ -358,7 +394,8 @@ class RemoteTransitionAdapter {
|
|||||||
parentH: Float
|
parentH: Float
|
||||||
) {
|
) {
|
||||||
if (rotateDelta == 0) return
|
if (rotateDelta == 0) return
|
||||||
val surface = SurfaceControl.Builder()
|
val surface =
|
||||||
|
SurfaceControl.Builder()
|
||||||
.setName("Transition Unrotate")
|
.setName("Transition Unrotate")
|
||||||
.setContainerLayer()
|
.setContainerLayer()
|
||||||
.setParent(parent)
|
.setParent(parent)
|
||||||
@@ -378,17 +415,15 @@ class RemoteTransitionAdapter {
|
|||||||
t.show(surface)
|
t.show(surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Adds a surface that needs to be counter-rotate. */
|
||||||
* Adds a surface that needs to be counter-rotate.
|
|
||||||
*/
|
|
||||||
fun addChild(t: SurfaceControl.Transaction, child: SurfaceControl?) {
|
fun addChild(t: SurfaceControl.Transaction, child: SurfaceControl?) {
|
||||||
if (surface == null) return
|
if (surface == null) return
|
||||||
t.reparent(child!!, surface)
|
t.reparent(child!!, surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean-up. Since finishTransaction should reset all change leashes, we only need to remove the
|
* Clean-up. Since finishTransaction should reset all change leashes, we only need to remove
|
||||||
* counter rotation surface.
|
* the counter rotation surface.
|
||||||
*/
|
*/
|
||||||
fun cleanUp(finishTransaction: SurfaceControl.Transaction) {
|
fun cleanUp(finishTransaction: SurfaceControl.Transaction) {
|
||||||
if (surface == null) return
|
if (surface == null) return
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ class ViewHierarchyAnimator {
|
|||||||
private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE
|
private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE
|
||||||
|
|
||||||
/** The properties used to animate the view bounds. */
|
/** The properties used to animate the view bounds. */
|
||||||
private val PROPERTIES = mapOf(
|
private val PROPERTIES =
|
||||||
|
mapOf(
|
||||||
Bound.LEFT to createViewProperty(Bound.LEFT),
|
Bound.LEFT to createViewProperty(Bound.LEFT),
|
||||||
Bound.TOP to createViewProperty(Bound.TOP),
|
Bound.TOP to createViewProperty(Bound.TOP),
|
||||||
Bound.RIGHT to createViewProperty(Bound.RIGHT),
|
Bound.RIGHT to createViewProperty(Bound.RIGHT),
|
||||||
@@ -103,7 +104,8 @@ class ViewHierarchyAnimator {
|
|||||||
duration: Long,
|
duration: Long,
|
||||||
ephemeral: Boolean
|
ephemeral: Boolean
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (!isVisible(
|
if (
|
||||||
|
!isVisible(
|
||||||
rootView.visibility,
|
rootView.visibility,
|
||||||
rootView.left,
|
rootView.left,
|
||||||
rootView.top,
|
rootView.top,
|
||||||
@@ -131,11 +133,7 @@ class ViewHierarchyAnimator {
|
|||||||
duration: Long,
|
duration: Long,
|
||||||
ephemeral: Boolean
|
ephemeral: Boolean
|
||||||
): View.OnLayoutChangeListener {
|
): View.OnLayoutChangeListener {
|
||||||
return createListener(
|
return createListener(interpolator, duration, ephemeral)
|
||||||
interpolator,
|
|
||||||
duration,
|
|
||||||
ephemeral
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,7 +169,8 @@ class ViewHierarchyAnimator {
|
|||||||
duration: Long = DEFAULT_DURATION,
|
duration: Long = DEFAULT_DURATION,
|
||||||
includeMargins: Boolean = false
|
includeMargins: Boolean = false
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (isVisible(
|
if (
|
||||||
|
isVisible(
|
||||||
rootView.visibility,
|
rootView.visibility,
|
||||||
rootView.left,
|
rootView.left,
|
||||||
rootView.top,
|
rootView.top,
|
||||||
@@ -182,8 +181,12 @@ class ViewHierarchyAnimator {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val listener = createAdditionListener(
|
val listener =
|
||||||
origin, interpolator, duration, ignorePreviousValues = !includeMargins
|
createAdditionListener(
|
||||||
|
origin,
|
||||||
|
interpolator,
|
||||||
|
duration,
|
||||||
|
ignorePreviousValues = !includeMargins
|
||||||
)
|
)
|
||||||
addListener(rootView, listener, recursive = true)
|
addListener(rootView, listener, recursive = true)
|
||||||
return true
|
return true
|
||||||
@@ -257,7 +260,8 @@ class ViewHierarchyAnimator {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val startValues = processStartValues(
|
val startValues =
|
||||||
|
processStartValues(
|
||||||
origin,
|
origin,
|
||||||
left,
|
left,
|
||||||
top,
|
top,
|
||||||
@@ -269,7 +273,8 @@ class ViewHierarchyAnimator {
|
|||||||
startBottom,
|
startBottom,
|
||||||
ignorePreviousValues
|
ignorePreviousValues
|
||||||
)
|
)
|
||||||
val endValues = mapOf(
|
val endValues =
|
||||||
|
mapOf(
|
||||||
Bound.LEFT to left,
|
Bound.LEFT to left,
|
||||||
Bound.TOP to top,
|
Bound.TOP to top,
|
||||||
Bound.RIGHT to right,
|
Bound.RIGHT to right,
|
||||||
@@ -313,7 +318,8 @@ class ViewHierarchyAnimator {
|
|||||||
interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR,
|
interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR,
|
||||||
duration: Long = DEFAULT_DURATION
|
duration: Long = DEFAULT_DURATION
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (!isVisible(
|
if (
|
||||||
|
!isVisible(
|
||||||
rootView.visibility,
|
rootView.visibility,
|
||||||
rootView.left,
|
rootView.left,
|
||||||
rootView.top,
|
rootView.top,
|
||||||
@@ -327,11 +333,7 @@ class ViewHierarchyAnimator {
|
|||||||
val parent = rootView.parent as ViewGroup
|
val parent = rootView.parent as ViewGroup
|
||||||
|
|
||||||
// Ensure that rootView's siblings animate nicely around the removal.
|
// Ensure that rootView's siblings animate nicely around the removal.
|
||||||
val listener = createUpdateListener(
|
val listener = createUpdateListener(interpolator, duration, ephemeral = true)
|
||||||
interpolator,
|
|
||||||
duration,
|
|
||||||
ephemeral = true
|
|
||||||
)
|
|
||||||
for (i in 0 until parent.childCount) {
|
for (i in 0 until parent.childCount) {
|
||||||
val child = parent.getChildAt(i)
|
val child = parent.getChildAt(i)
|
||||||
if (child == rootView) continue
|
if (child == rootView) continue
|
||||||
@@ -346,13 +348,15 @@ class ViewHierarchyAnimator {
|
|||||||
// them manually during the animation.
|
// them manually during the animation.
|
||||||
parent.overlay.add(rootView)
|
parent.overlay.add(rootView)
|
||||||
|
|
||||||
val startValues = mapOf(
|
val startValues =
|
||||||
|
mapOf(
|
||||||
Bound.LEFT to rootView.left,
|
Bound.LEFT to rootView.left,
|
||||||
Bound.TOP to rootView.top,
|
Bound.TOP to rootView.top,
|
||||||
Bound.RIGHT to rootView.right,
|
Bound.RIGHT to rootView.right,
|
||||||
Bound.BOTTOM to rootView.bottom
|
Bound.BOTTOM to rootView.bottom
|
||||||
)
|
)
|
||||||
val endValues = processEndValuesForRemoval(
|
val endValues =
|
||||||
|
processEndValuesForRemoval(
|
||||||
destination,
|
destination,
|
||||||
rootView.left,
|
rootView.left,
|
||||||
rootView.top,
|
rootView.top,
|
||||||
@@ -400,20 +404,24 @@ class ViewHierarchyAnimator {
|
|||||||
(animation.animatedValue as Float) * startAlphas[i]
|
(animation.animatedValue as Float) * startAlphas[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
animator.addListener(object : AnimatorListenerAdapter() {
|
animator.addListener(
|
||||||
|
object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
rootView.animate()
|
rootView
|
||||||
|
.animate()
|
||||||
.alpha(0f)
|
.alpha(0f)
|
||||||
.setInterpolator(Interpolators.ALPHA_OUT)
|
.setInterpolator(Interpolators.ALPHA_OUT)
|
||||||
.setDuration(duration / 2)
|
.setDuration(duration / 2)
|
||||||
.withEndAction { parent.overlay.remove(rootView) }
|
.withEndAction { parent.overlay.remove(rootView) }
|
||||||
.start()
|
.start()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
animator.start()
|
animator.start()
|
||||||
} else {
|
} else {
|
||||||
// Fade out the view during the second half of the removal.
|
// Fade out the view during the second half of the removal.
|
||||||
rootView.animate()
|
rootView
|
||||||
|
.animate()
|
||||||
.alpha(0f)
|
.alpha(0f)
|
||||||
.setInterpolator(Interpolators.ALPHA_OUT)
|
.setInterpolator(Interpolators.ALPHA_OUT)
|
||||||
.setDuration(duration / 2)
|
.setDuration(duration / 2)
|
||||||
@@ -440,13 +448,15 @@ class ViewHierarchyAnimator {
|
|||||||
) {
|
) {
|
||||||
for (i in 0 until rootView.childCount) {
|
for (i in 0 until rootView.childCount) {
|
||||||
val child = rootView.getChildAt(i)
|
val child = rootView.getChildAt(i)
|
||||||
val childStartValues = mapOf(
|
val childStartValues =
|
||||||
|
mapOf(
|
||||||
Bound.LEFT to child.left,
|
Bound.LEFT to child.left,
|
||||||
Bound.TOP to child.top,
|
Bound.TOP to child.top,
|
||||||
Bound.RIGHT to child.right,
|
Bound.RIGHT to child.right,
|
||||||
Bound.BOTTOM to child.bottom
|
Bound.BOTTOM to child.bottom
|
||||||
)
|
)
|
||||||
val childEndValues = processChildEndValuesForRemoval(
|
val childEndValues =
|
||||||
|
processChildEndValuesForRemoval(
|
||||||
destination,
|
destination,
|
||||||
child.left,
|
child.left,
|
||||||
child.top,
|
child.top,
|
||||||
@@ -500,6 +510,7 @@ class ViewHierarchyAnimator {
|
|||||||
* not newly introduced margins are included.
|
* not newly introduced margins are included.
|
||||||
*
|
*
|
||||||
* Base case
|
* Base case
|
||||||
|
* ```
|
||||||
* 1) origin=TOP
|
* 1) origin=TOP
|
||||||
* x---------x x---------x x---------x x---------x x---------x
|
* x---------x x---------x x---------x x---------x x---------x
|
||||||
* x---------x | | | | | |
|
* x---------x | | | | | |
|
||||||
@@ -518,11 +529,11 @@ class ViewHierarchyAnimator {
|
|||||||
* x -> x---x -> | | -> | | -> | |
|
* x -> x---x -> | | -> | | -> | |
|
||||||
* x-----x x-------x | |
|
* x-----x x-------x | |
|
||||||
* x---------x
|
* x---------x
|
||||||
*
|
* ```
|
||||||
* In case the start and end values differ in the direction of the origin, and
|
* In case the start and end values differ in the direction of the origin, and
|
||||||
* [ignorePreviousValues] is false, the previous values are used and a translation is
|
* [ignorePreviousValues] is false, the previous values are used and a translation is
|
||||||
* included in addition to the view expansion.
|
* included in addition to the view expansion.
|
||||||
*
|
* ```
|
||||||
* origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70)
|
* origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70)
|
||||||
* x
|
* x
|
||||||
* x--x
|
* x--x
|
||||||
@@ -531,6 +542,7 @@ class ViewHierarchyAnimator {
|
|||||||
* x----x | |
|
* x----x | |
|
||||||
* | |
|
* | |
|
||||||
* x------x
|
* x------x
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
private fun processStartValues(
|
private fun processStartValues(
|
||||||
origin: Hotspot?,
|
origin: Hotspot?,
|
||||||
@@ -555,41 +567,53 @@ class ViewHierarchyAnimator {
|
|||||||
var bottom = startBottom
|
var bottom = startBottom
|
||||||
|
|
||||||
if (origin != null) {
|
if (origin != null) {
|
||||||
left = when (origin) {
|
left =
|
||||||
|
when (origin) {
|
||||||
Hotspot.CENTER -> (newLeft + newRight) / 2
|
Hotspot.CENTER -> (newLeft + newRight) / 2
|
||||||
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft)
|
Hotspot.BOTTOM_LEFT,
|
||||||
Hotspot.TOP, Hotspot.BOTTOM -> newLeft
|
Hotspot.LEFT,
|
||||||
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max(
|
Hotspot.TOP_LEFT -> min(startLeft, newLeft)
|
||||||
startRight,
|
Hotspot.TOP,
|
||||||
newRight
|
Hotspot.BOTTOM -> newLeft
|
||||||
)
|
Hotspot.TOP_RIGHT,
|
||||||
|
Hotspot.RIGHT,
|
||||||
|
Hotspot.BOTTOM_RIGHT -> max(startRight, newRight)
|
||||||
}
|
}
|
||||||
top = when (origin) {
|
top =
|
||||||
|
when (origin) {
|
||||||
Hotspot.CENTER -> (newTop + newBottom) / 2
|
Hotspot.CENTER -> (newTop + newBottom) / 2
|
||||||
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop)
|
Hotspot.TOP_LEFT,
|
||||||
Hotspot.LEFT, Hotspot.RIGHT -> newTop
|
Hotspot.TOP,
|
||||||
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max(
|
Hotspot.TOP_RIGHT -> min(startTop, newTop)
|
||||||
startBottom,
|
Hotspot.LEFT,
|
||||||
newBottom
|
Hotspot.RIGHT -> newTop
|
||||||
)
|
Hotspot.BOTTOM_RIGHT,
|
||||||
|
Hotspot.BOTTOM,
|
||||||
|
Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom)
|
||||||
}
|
}
|
||||||
right = when (origin) {
|
right =
|
||||||
|
when (origin) {
|
||||||
Hotspot.CENTER -> (newLeft + newRight) / 2
|
Hotspot.CENTER -> (newLeft + newRight) / 2
|
||||||
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max(
|
Hotspot.TOP_RIGHT,
|
||||||
startRight,
|
Hotspot.RIGHT,
|
||||||
newRight
|
Hotspot.BOTTOM_RIGHT -> max(startRight, newRight)
|
||||||
)
|
Hotspot.TOP,
|
||||||
Hotspot.TOP, Hotspot.BOTTOM -> newRight
|
Hotspot.BOTTOM -> newRight
|
||||||
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft)
|
Hotspot.BOTTOM_LEFT,
|
||||||
|
Hotspot.LEFT,
|
||||||
|
Hotspot.TOP_LEFT -> min(startLeft, newLeft)
|
||||||
}
|
}
|
||||||
bottom = when (origin) {
|
bottom =
|
||||||
|
when (origin) {
|
||||||
Hotspot.CENTER -> (newTop + newBottom) / 2
|
Hotspot.CENTER -> (newTop + newBottom) / 2
|
||||||
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max(
|
Hotspot.BOTTOM_RIGHT,
|
||||||
startBottom,
|
Hotspot.BOTTOM,
|
||||||
newBottom
|
Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom)
|
||||||
)
|
Hotspot.LEFT,
|
||||||
Hotspot.LEFT, Hotspot.RIGHT -> newBottom
|
Hotspot.RIGHT -> newBottom
|
||||||
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop)
|
Hotspot.TOP_LEFT,
|
||||||
|
Hotspot.TOP,
|
||||||
|
Hotspot.TOP_RIGHT -> min(startTop, newTop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,6 +630,7 @@ class ViewHierarchyAnimator {
|
|||||||
* view's starting bounds.
|
* view's starting bounds.
|
||||||
*
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
|
* ```
|
||||||
* 1) destination=TOP
|
* 1) destination=TOP
|
||||||
* x---------x x---------x x---------x x---------x x---------x
|
* x---------x x---------x x---------x x---------x x---------x
|
||||||
* | | | | | | x---------x
|
* | | | | | | x---------x
|
||||||
@@ -624,6 +649,7 @@ class ViewHierarchyAnimator {
|
|||||||
* | | -> | | -> | | -> x---x -> x
|
* | | -> | | -> | | -> x---x -> x
|
||||||
* | | x-------x x-----x
|
* | | x-------x x-----x
|
||||||
* x---------x
|
* x---------x
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
private fun processEndValuesForRemoval(
|
private fun processEndValuesForRemoval(
|
||||||
destination: Hotspot,
|
destination: Hotspot,
|
||||||
@@ -632,31 +658,53 @@ class ViewHierarchyAnimator {
|
|||||||
right: Int,
|
right: Int,
|
||||||
bottom: Int
|
bottom: Int
|
||||||
): Map<Bound, Int> {
|
): Map<Bound, Int> {
|
||||||
val endLeft = when (destination) {
|
val endLeft =
|
||||||
|
when (destination) {
|
||||||
Hotspot.CENTER -> (left + right) / 2
|
Hotspot.CENTER -> (left + right) / 2
|
||||||
Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP ->
|
Hotspot.BOTTOM,
|
||||||
left
|
Hotspot.BOTTOM_LEFT,
|
||||||
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> right
|
Hotspot.LEFT,
|
||||||
|
Hotspot.TOP_LEFT,
|
||||||
|
Hotspot.TOP -> left
|
||||||
|
Hotspot.TOP_RIGHT,
|
||||||
|
Hotspot.RIGHT,
|
||||||
|
Hotspot.BOTTOM_RIGHT -> right
|
||||||
}
|
}
|
||||||
val endTop = when (destination) {
|
val endTop =
|
||||||
|
when (destination) {
|
||||||
Hotspot.CENTER -> (top + bottom) / 2
|
Hotspot.CENTER -> (top + bottom) / 2
|
||||||
Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT ->
|
Hotspot.LEFT,
|
||||||
top
|
Hotspot.TOP_LEFT,
|
||||||
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> bottom
|
Hotspot.TOP,
|
||||||
|
Hotspot.TOP_RIGHT,
|
||||||
|
Hotspot.RIGHT -> top
|
||||||
|
Hotspot.BOTTOM_RIGHT,
|
||||||
|
Hotspot.BOTTOM,
|
||||||
|
Hotspot.BOTTOM_LEFT -> bottom
|
||||||
}
|
}
|
||||||
val endRight = when (destination) {
|
val endRight =
|
||||||
|
when (destination) {
|
||||||
Hotspot.CENTER -> (left + right) / 2
|
Hotspot.CENTER -> (left + right) / 2
|
||||||
Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT,
|
Hotspot.TOP,
|
||||||
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM ->
|
Hotspot.TOP_RIGHT,
|
||||||
right
|
Hotspot.RIGHT,
|
||||||
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> left
|
Hotspot.BOTTOM_RIGHT,
|
||||||
|
Hotspot.BOTTOM -> right
|
||||||
|
Hotspot.BOTTOM_LEFT,
|
||||||
|
Hotspot.LEFT,
|
||||||
|
Hotspot.TOP_LEFT -> left
|
||||||
}
|
}
|
||||||
val endBottom = when (destination) {
|
val endBottom =
|
||||||
|
when (destination) {
|
||||||
Hotspot.CENTER -> (top + bottom) / 2
|
Hotspot.CENTER -> (top + bottom) / 2
|
||||||
Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM,
|
Hotspot.RIGHT,
|
||||||
Hotspot.BOTTOM_LEFT, Hotspot.LEFT ->
|
Hotspot.BOTTOM_RIGHT,
|
||||||
bottom
|
Hotspot.BOTTOM,
|
||||||
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> top
|
Hotspot.BOTTOM_LEFT,
|
||||||
|
Hotspot.LEFT -> bottom
|
||||||
|
Hotspot.TOP_LEFT,
|
||||||
|
Hotspot.TOP,
|
||||||
|
Hotspot.TOP_RIGHT -> top
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapOf(
|
return mapOf(
|
||||||
@@ -675,6 +723,7 @@ class ViewHierarchyAnimator {
|
|||||||
* its center is at the [destination].
|
* its center is at the [destination].
|
||||||
*
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
|
* ```
|
||||||
* 1) destination=TOP
|
* 1) destination=TOP
|
||||||
* The child maintains its left and right positions, but is shifted up so that its
|
* The child maintains its left and right positions, but is shifted up so that its
|
||||||
* center is on the parent's end top edge.
|
* center is on the parent's end top edge.
|
||||||
@@ -682,6 +731,7 @@ class ViewHierarchyAnimator {
|
|||||||
* The child shifts so that its center is on the parent's end bottom left corner.
|
* The child shifts so that its center is on the parent's end bottom left corner.
|
||||||
* 3) destination=CENTER
|
* 3) destination=CENTER
|
||||||
* The child shifts so that its own center is on the parent's end center.
|
* The child shifts so that its own center is on the parent's end center.
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
private fun processChildEndValuesForRemoval(
|
private fun processChildEndValuesForRemoval(
|
||||||
destination: Hotspot,
|
destination: Hotspot,
|
||||||
@@ -695,31 +745,53 @@ class ViewHierarchyAnimator {
|
|||||||
val halfWidth = (right - left) / 2
|
val halfWidth = (right - left) / 2
|
||||||
val halfHeight = (bottom - top) / 2
|
val halfHeight = (bottom - top) / 2
|
||||||
|
|
||||||
val endLeft = when (destination) {
|
val endLeft =
|
||||||
|
when (destination) {
|
||||||
Hotspot.CENTER -> (parentWidth / 2) - halfWidth
|
Hotspot.CENTER -> (parentWidth / 2) - halfWidth
|
||||||
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> -halfWidth
|
Hotspot.BOTTOM_LEFT,
|
||||||
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth
|
Hotspot.LEFT,
|
||||||
Hotspot.TOP, Hotspot.BOTTOM -> left
|
Hotspot.TOP_LEFT -> -halfWidth
|
||||||
|
Hotspot.TOP_RIGHT,
|
||||||
|
Hotspot.RIGHT,
|
||||||
|
Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth
|
||||||
|
Hotspot.TOP,
|
||||||
|
Hotspot.BOTTOM -> left
|
||||||
}
|
}
|
||||||
val endTop = when (destination) {
|
val endTop =
|
||||||
|
when (destination) {
|
||||||
Hotspot.CENTER -> (parentHeight / 2) - halfHeight
|
Hotspot.CENTER -> (parentHeight / 2) - halfHeight
|
||||||
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> -halfHeight
|
Hotspot.TOP_LEFT,
|
||||||
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT ->
|
Hotspot.TOP,
|
||||||
parentHeight - halfHeight
|
Hotspot.TOP_RIGHT -> -halfHeight
|
||||||
Hotspot.LEFT, Hotspot.RIGHT -> top
|
Hotspot.BOTTOM_RIGHT,
|
||||||
|
Hotspot.BOTTOM,
|
||||||
|
Hotspot.BOTTOM_LEFT -> parentHeight - halfHeight
|
||||||
|
Hotspot.LEFT,
|
||||||
|
Hotspot.RIGHT -> top
|
||||||
}
|
}
|
||||||
val endRight = when (destination) {
|
val endRight =
|
||||||
|
when (destination) {
|
||||||
Hotspot.CENTER -> (parentWidth / 2) + halfWidth
|
Hotspot.CENTER -> (parentWidth / 2) + halfWidth
|
||||||
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth
|
Hotspot.TOP_RIGHT,
|
||||||
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> halfWidth
|
Hotspot.RIGHT,
|
||||||
Hotspot.TOP, Hotspot.BOTTOM -> right
|
Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth
|
||||||
|
Hotspot.BOTTOM_LEFT,
|
||||||
|
Hotspot.LEFT,
|
||||||
|
Hotspot.TOP_LEFT -> halfWidth
|
||||||
|
Hotspot.TOP,
|
||||||
|
Hotspot.BOTTOM -> right
|
||||||
}
|
}
|
||||||
val endBottom = when (destination) {
|
val endBottom =
|
||||||
|
when (destination) {
|
||||||
Hotspot.CENTER -> (parentHeight / 2) + halfHeight
|
Hotspot.CENTER -> (parentHeight / 2) + halfHeight
|
||||||
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT ->
|
Hotspot.BOTTOM_RIGHT,
|
||||||
parentHeight + halfHeight
|
Hotspot.BOTTOM,
|
||||||
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> halfHeight
|
Hotspot.BOTTOM_LEFT -> parentHeight + halfHeight
|
||||||
Hotspot.LEFT, Hotspot.RIGHT -> bottom
|
Hotspot.TOP_LEFT,
|
||||||
|
Hotspot.TOP,
|
||||||
|
Hotspot.TOP_RIGHT -> halfHeight
|
||||||
|
Hotspot.LEFT,
|
||||||
|
Hotspot.RIGHT -> bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapOf(
|
return mapOf(
|
||||||
@@ -790,7 +862,8 @@ class ViewHierarchyAnimator {
|
|||||||
duration: Long,
|
duration: Long,
|
||||||
ephemeral: Boolean
|
ephemeral: Boolean
|
||||||
) {
|
) {
|
||||||
val propertyValuesHolders = buildList {
|
val propertyValuesHolders =
|
||||||
|
buildList {
|
||||||
bounds.forEach { bound ->
|
bounds.forEach { bound ->
|
||||||
add(
|
add(
|
||||||
PropertyValuesHolder.ofInt(
|
PropertyValuesHolder.ofInt(
|
||||||
@@ -800,26 +873,29 @@ class ViewHierarchyAnimator {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.toTypedArray()
|
}
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
(view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()
|
(view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()
|
||||||
|
|
||||||
val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders)
|
val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders)
|
||||||
animator.interpolator = interpolator
|
animator.interpolator = interpolator
|
||||||
animator.duration = duration
|
animator.duration = duration
|
||||||
animator.addListener(object : AnimatorListenerAdapter() {
|
animator.addListener(
|
||||||
|
object : AnimatorListenerAdapter() {
|
||||||
var cancelled = false
|
var cancelled = false
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
view.setTag(R.id.tag_animator, null /* tag */)
|
view.setTag(R.id.tag_animator, null /* tag */)
|
||||||
bounds.forEach { view.setTag(it.overrideTag, null /* tag */) }
|
bounds.forEach { view.setTag(it.overrideTag, null /* tag */) }
|
||||||
|
|
||||||
// When an animation is cancelled, a new one might be taking over. We shouldn't
|
// When an animation is cancelled, a new one might be taking over. We
|
||||||
// unregister the listener yet.
|
// shouldn't unregister the listener yet.
|
||||||
if (ephemeral && !cancelled) {
|
if (ephemeral && !cancelled) {
|
||||||
// The duration is the same for the whole hierarchy, so it's safe to remove
|
// The duration is the same for the whole hierarchy, so it's safe to
|
||||||
// the listener recursively. We do this because some descendant views might
|
// remove the listener recursively. We do this because some descendant
|
||||||
// not change bounds, and therefore not animate and leak the listener.
|
// views might not change bounds, and therefore not animate and leak the
|
||||||
|
// listener.
|
||||||
recursivelyRemoveListener(view)
|
recursivelyRemoveListener(view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -827,7 +903,8 @@ class ViewHierarchyAnimator {
|
|||||||
override fun onAnimationCancel(animation: Animator?) {
|
override fun onAnimationCancel(animation: Animator?) {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
bounds.forEach { bound -> setBound(view, bound, startValues.getValue(bound)) }
|
bounds.forEach { bound -> setBound(view, bound, startValues.getValue(bound)) }
|
||||||
|
|
||||||
@@ -838,7 +915,15 @@ class ViewHierarchyAnimator {
|
|||||||
|
|
||||||
/** An enum used to determine the origin of addition animations. */
|
/** An enum used to determine the origin of addition animations. */
|
||||||
enum class Hotspot {
|
enum class Hotspot {
|
||||||
CENTER, LEFT, TOP_LEFT, TOP, TOP_RIGHT, RIGHT, BOTTOM_RIGHT, BOTTOM, BOTTOM_LEFT
|
CENTER,
|
||||||
|
LEFT,
|
||||||
|
TOP_LEFT,
|
||||||
|
TOP,
|
||||||
|
TOP_RIGHT,
|
||||||
|
RIGHT,
|
||||||
|
BOTTOM_RIGHT,
|
||||||
|
BOTTOM,
|
||||||
|
BOTTOM_LEFT
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class Bound(val label: String, val overrideTag: Int) {
|
private enum class Bound(val label: String, val overrideTag: Int) {
|
||||||
|
|||||||
@@ -11,25 +11,26 @@ object ViewRootSync {
|
|||||||
/**
|
/**
|
||||||
* Synchronize the next draw between the view roots of [view] and [otherView], then run [then].
|
* Synchronize the next draw between the view roots of [view] and [otherView], then run [then].
|
||||||
*
|
*
|
||||||
* Note that in some cases, the synchronization might not be possible (e.g. WM consumed the
|
* Note that in some cases, the synchronization might not be possible (e.g. WM consumed the next
|
||||||
* next transactions) or disabled (temporarily, on low ram devices). In this case, [then] will
|
* transactions) or disabled (temporarily, on low ram devices). In this case, [then] will be
|
||||||
* be called without synchronizing.
|
* called without synchronizing.
|
||||||
*/
|
*/
|
||||||
fun synchronizeNextDraw(
|
fun synchronizeNextDraw(view: View, otherView: View, then: () -> Unit) {
|
||||||
view: View,
|
if (
|
||||||
otherView: View,
|
!view.isAttachedToWindow ||
|
||||||
then: () -> Unit
|
view.viewRootImpl == null ||
|
||||||
|
!otherView.isAttachedToWindow ||
|
||||||
|
otherView.viewRootImpl == null ||
|
||||||
|
view.viewRootImpl == otherView.viewRootImpl
|
||||||
) {
|
) {
|
||||||
if (!view.isAttachedToWindow || view.viewRootImpl == null ||
|
|
||||||
!otherView.isAttachedToWindow || otherView.viewRootImpl == null ||
|
|
||||||
view.viewRootImpl == otherView.viewRootImpl) {
|
|
||||||
// No need to synchronize if either the touch surface or dialog view is not attached
|
// No need to synchronize if either the touch surface or dialog view is not attached
|
||||||
// to a window.
|
// to a window.
|
||||||
then()
|
then()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
surfaceSyncer = SurfaceSyncer().apply {
|
surfaceSyncer =
|
||||||
|
SurfaceSyncer().apply {
|
||||||
val syncId = setupSync(Runnable { then() })
|
val syncId = setupSync(Runnable { then() })
|
||||||
addToSync(syncId, view)
|
addToSync(syncId, view)
|
||||||
addToSync(syncId, otherView)
|
addToSync(syncId, otherView)
|
||||||
@@ -37,9 +38,7 @@ object ViewRootSync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A Java-friendly API for [synchronizeNextDraw]. */
|
||||||
* A Java-friendly API for [synchronizeNextDraw].
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun synchronizeNextDraw(view: View, otherView: View, then: Runnable) {
|
fun synchronizeNextDraw(view: View, otherView: View, then: Runnable) {
|
||||||
synchronizeNextDraw(view, otherView, then::run)
|
synchronizeNextDraw(view, otherView, then::run)
|
||||||
|
|||||||
Reference in New Issue
Block a user