Merge changes I49c6095f,Id14bee4b into tm-qpr-dev

* changes:
  Support Activity launches above the expandable
  Format Kotlin code in SysUI Animation lib using ktfmt
This commit is contained in:
Jordan Demeulenaere
2022-06-13 07:07:01 +00:00
committed by Android (Google) Code Review
10 changed files with 957 additions and 729 deletions

View File

@@ -59,31 +59,31 @@ 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 =
totalDuration = 500L, LaunchAnimator.Timings(
contentBeforeFadeOutDelay = 0L, totalDuration = 500L,
contentBeforeFadeOutDuration = 150L, contentBeforeFadeOutDelay = 0L,
contentAfterFadeInDelay = 150L, contentBeforeFadeOutDuration = 150L,
contentAfterFadeInDuration = 183L contentAfterFadeInDelay = 150L,
) contentAfterFadeInDuration = 183L
)
/** /**
* The timings when animating a Dialog into an app. We need to wait at least 200ms before * The timings when animating a Dialog into an app. We need to wait at least 200ms before
* 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 =
positionInterpolator = Interpolators.EMPHASIZED, LaunchAnimator.Interpolators(
positionXInterpolator = createPositionXInterpolator(), positionInterpolator = Interpolators.EMPHASIZED,
contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN, positionXInterpolator = createPositionXInterpolator(),
contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f) contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN,
) contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f)
)
/** Durations & interpolators for the navigation bar fading in & out. */ /** Durations & interpolators for the navigation bar fading in & out. */
private const val ANIMATION_DURATION_NAV_FADE_IN = 266L private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
@@ -98,11 +98,12 @@ 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 =
moveTo(0f, 0f) Path().apply {
cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f) moveTo(0f, 0f)
cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f) cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f)
} cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f)
}
return PathInterpolator(path) return PathInterpolator(path)
} }
} }
@@ -150,29 +151,37 @@ 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 =
RemoteAnimationAdapter( if (!hideKeyguardWithAnimation) {
runner, RemoteAnimationAdapter(
TIMINGS.totalDuration, runner,
TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */ TIMINGS.totalDuration,
) TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */
} else { )
null } else {
} null
}
// Register the remote animation for the given package to also animate trampoline // Register the remote animation for the given package to also animate trampoline
// 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)
} }
@@ -184,12 +193,15 @@ class ActivityLaunchAnimator(
// keyguard. // keyguard.
val willAnimate = val willAnimate =
launchResult == ActivityManager.START_TASK_TO_FRONT || launchResult == ActivityManager.START_TASK_TO_FRONT ||
launchResult == ActivityManager.START_SUCCESS || launchResult == ActivityManager.START_SUCCESS ||
(launchResult == ActivityManager.START_DELIVERED_TO_TOP && (launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
hideKeyguardWithAnimation) hideKeyguardWithAnimation)
Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " + Log.i(
"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,80 +433,96 @@ class ActivityLaunchAnimator(
return return
} }
val navigationBar = nonApps?.firstOrNull { val navigationBar =
it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR nonApps?.firstOrNull {
} it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
}
val windowBounds = window.screenSpaceBounds val windowBounds = window.screenSpaceBounds
val endState = LaunchAnimator.State( val endState =
top = windowBounds.top, LaunchAnimator.State(
bottom = windowBounds.bottom, top = windowBounds.top,
left = windowBounds.left, bottom = windowBounds.bottom,
right = windowBounds.right left = windowBounds.left,
) 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 =
dialogToAppAnimator if (controller.isDialogLaunch) {
} else { dialogToAppAnimator
launchAnimator } else {
} launchAnimator
}
// TODO(b/184121838): We should somehow get the top and bottom radius of the window // TODO(b/184121838): We should somehow get the top and bottom radius of the window
// instead of recomputing isExpandingFullyAbove here. // 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 =
// Most of the time, expanding fully above the root view means expanding in full if (isExpandingFullyAbove) {
// screen. // Most of the time, expanding fully above the root view means expanding in full
ScreenDecorationsUtils.getWindowCornerRadius(context) // screen.
} else { ScreenDecorationsUtils.getWindowCornerRadius(context)
// This usually means we are in split screen mode, so 2 out of 4 corners will have } else {
// a radius of 0. // This usually means we are in split screen mode, so 2 out of 4 corners will
0f // have
} // a radius of 0.
0f
}
endState.topCornerRadius = endRadius endState.topCornerRadius = endRadius
endState.bottomCornerRadius = endRadius endState.bottomCornerRadius = endRadius
// 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 =
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { object : Controller by delegate {
listeners.forEach { it.onLaunchAnimationStart() } override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
delegate.onLaunchAnimationStart(isExpandingFullyAbove) listeners.forEach { it.onLaunchAnimationStart() }
} delegate.onLaunchAnimationStart(isExpandingFullyAbove)
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
listeners.forEach { it.onLaunchAnimationEnd() }
iCallback?.invoke()
delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
}
override fun onLaunchAnimationProgress(
state: LaunchAnimator.State,
progress: Float,
linearProgress: Float
) {
// Apply the state to the window only if it is visible, i.e. when the expanding
// view is *not* visible.
if (!state.visible) {
applyStateToWindow(window, state)
} }
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
listeners.forEach { it.onLaunchAnimationProgress(linearProgress) } override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
delegate.onLaunchAnimationProgress(state, progress, linearProgress) listeners.forEach { it.onLaunchAnimationEnd() }
iCallback?.invoke()
delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
}
override fun onLaunchAnimationProgress(
state: LaunchAnimator.State,
progress: Float,
linearProgress: Float
) {
// Apply the state to the window only if it is visible, i.e. when the
// expanding view is *not* visible.
if (!state.visible) {
applyStateToWindow(window, state, linearProgress)
}
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
listeners.forEach { it.onLaunchAnimationProgress(linearProgress) }
delegate.onLaunchAnimationProgress(state, progress, linearProgress)
}
} }
}
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,19 +564,38 @@ 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)
.withMatrix(matrix) .withAlpha(alpha)
.withWindowCrop(windowCrop) .withMatrix(matrix)
.withCornerRadius(cornerRadius) .withWindowCrop(windowCrop)
.withVisibility(true) .withCornerRadius(cornerRadius)
.build() .withVisibility(true)
.build()
transactionApplier.scheduleApply(params) transactionApplier.scheduleApply(params)
} }
@@ -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))
} }

View File

@@ -23,4 +23,4 @@ package com.android.systemui.animation
*/ */
open class DelegateLaunchAnimatorController( open class DelegateLaunchAnimatorController(
protected val delegate: ActivityLaunchAnimator.Controller protected val delegate: ActivityLaunchAnimator.Controller
) : ActivityLaunchAnimator.Controller by delegate ) : ActivityLaunchAnimator.Controller by delegate

View File

@@ -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,9 +64,10 @@ 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 =
positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator ActivityLaunchAnimator.INTERPOLATORS.copy(
) positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
)
private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.tag_launch_animation_running private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.tag_launch_animation_running
} }
@@ -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,18 +123,19 @@ class DialogLaunchAnimator @JvmOverloads constructor(
animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true) animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
val animatedDialog = AnimatedDialog( val animatedDialog =
launchAnimator, AnimatedDialog(
dreamManager, launchAnimator,
interactionJankMonitor, dreamManager,
animateFrom, interactionJankMonitor,
onDialogDismissed = { openedDialogs.remove(it) }, animateFrom,
dialog = dialog, onDialogDismissed = { openedDialogs.remove(it) },
animateBackgroundBoundsChange, dialog = dialog,
animatedParent, animateBackgroundBoundsChange,
isForTesting, animatedParent,
cuj isForTesting,
) cuj
)
openedDialogs.add(animatedDialog) openedDialogs.add(animatedDialog)
animatedDialog.start() animatedDialog.start()
@@ -146,13 +152,12 @@ 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)" )
)
showFromView(dialog, view, animateBackgroundBoundsChange = animateBackgroundBoundsChange) showFromView(dialog, view, animateBackgroundBoundsChange = animateBackgroundBoundsChange)
} }
@@ -175,9 +180,11 @@ 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 {
?: return null it.dialog.window.decorView.viewRootImpl == view.viewRootImpl
}
?: 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
// an app, so we disable the exit animation into the touch surface because we will never // an app, so we disable the exit animation into the touch surface because we will never
@@ -240,7 +247,7 @@ class DialogLaunchAnimator @JvmOverloads constructor(
} }
private fun disableDialogDismiss() { private fun disableDialogDismiss() {
dialog.setDismissOverride { /* Do nothing */ } dialog.setDismissOverride { /* Do nothing */}
} }
private fun enableDialogDismiss() { private fun enableDialogDismiss() {
@@ -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,11 +358,12 @@ 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 =
AnimatedBoundsLayoutListener() if (animateBackgroundBoundsChange) {
} else { AnimatedBoundsLayoutListener()
null } else {
} null
}
/* /*
* A layout listener in case the dialog (window) size changes (for instance because of a * A layout listener in case the dialog (window) size changes (for instance because of a
@@ -381,100 +387,117 @@ 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
var viewGroupWithBackground: ViewGroup? = null // the actual dialog view.
for (i in 0 until decorView.childCount) { var viewGroupWithBackground: ViewGroup? = null
viewGroupWithBackground = findFirstViewGroupWithBackground(decorView.getChildAt(i)) for (i in 0 until decorView.childCount) {
if (viewGroupWithBackground != null) { viewGroupWithBackground =
break findFirstViewGroupWithBackground(decorView.getChildAt(i))
if (viewGroupWithBackground != null) {
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
// it's not clear what we should animate. // otherwise
viewGroupWithBackground // it's not clear what we should animate.
?: throw IllegalStateException("Unable to find ViewGroup with background") viewGroupWithBackground
} else { ?: throw IllegalStateException("Unable to find ViewGroup with background")
// We will make the dialog window (and therefore its DecorView) fullscreen to make it } else {
// possible to animate outside its bounds. // We will make the dialog window (and therefore its DecorView) fullscreen to make
// // 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 //
// gravity as that DecorView, then we add all original children of the DecorView to that // Before that, we add a new View as a child of the DecorView with the same size and
// new View. Finally we remove the background of the DecorView and add it to the new // gravity as that DecorView, then we add all original children of the DecorView to
// View, then we make the DecorView fullscreen. This new View now acts as a fake (non // that new View. Finally we remove the background of the DecorView and add it to
// fullscreen) window. // the new View, then we make the DecorView fullscreen. This new View now acts as a
// // fake (non fullscreen) window.
// On top of that, we also add a fullscreen transparent background between the DecorView //
// and the view that we added so that we can dismiss the dialog when this view is // On top of that, we also add a fullscreen transparent background between the
// clicked. This is necessary because DecorView overrides onTouchEvent and therefore we // DecorView and the view that we added so that we can dismiss the dialog when this
// can't set the click listener directly on the (now fullscreen) DecorView. // view is clicked. This is necessary because DecorView overrides onTouchEvent and
val fullscreenTransparentBackground = FrameLayout(dialog.context) // therefore we can't set the click listener directly on the (now fullscreen)
decorView.addView( // DecorView.
fullscreenTransparentBackground, val fullscreenTransparentBackground = FrameLayout(dialog.context)
0 /* index */, decorView.addView(
FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) fullscreenTransparentBackground,
) 0 /* index */,
FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
val dialogContentWithBackground = FrameLayout(dialog.context)
dialogContentWithBackground.background = decorView.background
// Make the window background transparent. Note that setting the window (or DecorView)
// background drawable to null leads to issues with background color (not being
// transparent) or with insets that are not refreshed. Therefore we need to set it to
// something not null, hence we are using android.R.color.transparent here.
window.setBackgroundDrawableResource(android.R.color.transparent)
// Close the dialog when clicking outside of it.
fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
dialogContentWithBackground.isClickable = true
// Make sure the transparent and dialog backgrounds are not focusable by accessibility
// features.
fullscreenTransparentBackground.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO
dialogContentWithBackground.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO
fullscreenTransparentBackground.addView(
dialogContentWithBackground,
FrameLayout.LayoutParams(
window.attributes.width,
window.attributes.height,
window.attributes.gravity
) )
)
// Move all original children of the DecorView to the new View we just added. val dialogContentWithBackground = FrameLayout(dialog.context)
for (i in 1 until decorView.childCount) { dialogContentWithBackground.background = decorView.background
val view = decorView.getChildAt(1)
decorView.removeViewAt(1)
dialogContentWithBackground.addView(view)
}
// Make the window fullscreen and add a layout listener to ensure it stays fullscreen. // Make the window background transparent. Note that setting the window (or
window.setLayout(MATCH_PARENT, MATCH_PARENT) // DecorView) background drawable to null leads to issues with background color (not
decorViewLayoutListener = View.OnLayoutChangeListener { // being transparent) or with insets that are not refreshed. Therefore we need to
v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> // set it to something not null, hence we are using android.R.color.transparent
if (window.attributes.width != MATCH_PARENT || // here.
window.attributes.height != MATCH_PARENT window.setBackgroundDrawableResource(android.R.color.transparent)
) {
// The dialog size changed, copy its size to dialogContentWithBackground and // Close the dialog when clicking outside of it.
// make the dialog window full screen again. fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
val layoutParams = dialogContentWithBackground.layoutParams dialogContentWithBackground.isClickable = true
layoutParams.width = window.attributes.width
layoutParams.height = window.attributes.height // Make sure the transparent and dialog backgrounds are not focusable by
dialogContentWithBackground.layoutParams = layoutParams // accessibility
window.setLayout(MATCH_PARENT, MATCH_PARENT) // features.
fullscreenTransparentBackground.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO
dialogContentWithBackground.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO
fullscreenTransparentBackground.addView(
dialogContentWithBackground,
FrameLayout.LayoutParams(
window.attributes.width,
window.attributes.height,
window.attributes.gravity
)
)
// Move all original children of the DecorView to the new View we just added.
for (i in 1 until decorView.childCount) {
val view = decorView.getChildAt(1)
decorView.removeViewAt(1)
dialogContentWithBackground.addView(view)
} }
}
decorView.addOnLayoutChangeListener(decorViewLayoutListener)
dialogContentWithBackground // Make the window fullscreen and add a layout listener to ensure it stays
} // fullscreen.
window.setLayout(MATCH_PARENT, MATCH_PARENT)
decorViewLayoutListener =
View.OnLayoutChangeListener {
v,
left,
top,
right,
bottom,
oldLeft,
oldTop,
oldRight,
oldBottom ->
if (
window.attributes.width != MATCH_PARENT ||
window.attributes.height != MATCH_PARENT
) {
// The dialog size changed, copy its size to dialogContentWithBackground
// and make the dialog window full screen again.
val layoutParams = dialogContentWithBackground.layoutParams
layoutParams.width = window.attributes.width
layoutParams.height = window.attributes.height
dialogContentWithBackground.layoutParams = layoutParams
window.setLayout(MATCH_PARENT, MATCH_PARENT)
}
}
decorView.addOnLayoutChangeListener(decorViewLayoutListener)
dialogContentWithBackground
}
this.dialogContentWithBackground = dialogContentWithBackground this.dialogContentWithBackground = dialogContentWithBackground
dialogContentWithBackground.setTag(R.id.tag_dialog_background, true) dialogContentWithBackground.setTag(R.id.tag_dialog_background, true)
@@ -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,24 +532,26 @@ 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(
override fun onLayoutChange( object : View.OnLayoutChangeListener {
v: View, override fun onLayoutChange(
left: Int, v: View,
top: Int, left: Int,
right: Int, top: Int,
bottom: Int, right: Int,
oldLeft: Int, bottom: Int,
oldTop: Int, oldLeft: Int,
oldRight: Int, oldTop: Int,
oldBottom: Int oldRight: Int,
) { oldBottom: Int
dialogContentWithBackground.removeOnLayoutChangeListener(this) ) {
dialogContentWithBackground.removeOnLayoutChangeListener(this)
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(
isTouchSurfaceGhostDrawn = true then = {
maybeStartLaunchAnimation() isTouchSurfaceGhostDrawn = true
}) 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(
onAnimationFinished(true /* instantDismiss */) then = {
onDialogDismissed(this@AnimatedDialog) onAnimationFinished(true /* instantDismiss */)
}) onDialogDismissed(this@AnimatedDialog)
}
)
} }
) )
} }
@@ -740,56 +771,57 @@ private class AnimatedDialog(
endViewController.launchContainer = decorView endViewController.launchContainer = decorView
val endState = endViewController.createAnimatorState() val endState = endViewController.createAnimatorState()
val controller = object : LaunchAnimator.Controller { val controller =
override var launchContainer: ViewGroup object : LaunchAnimator.Controller {
get() = startViewController.launchContainer override var launchContainer: ViewGroup
set(value) { get() = startViewController.launchContainer
startViewController.launchContainer = value set(value) {
endViewController.launchContainer = value startViewController.launchContainer = value
endViewController.launchContainer = value
}
override fun createAnimatorState(): LaunchAnimator.State {
return startViewController.createAnimatorState()
} }
override fun createAnimatorState(): LaunchAnimator.State { override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
return startViewController.createAnimatorState() // During launch, onLaunchAnimationStart will be used to remove the temporary
// touch surface ghost so it is important to call this before calling
// onLaunchAnimationStart on the controller (which will create its own ghost).
onLaunchAnimationStart()
startViewController.onLaunchAnimationStart(isExpandingFullyAbove)
endViewController.onLaunchAnimationStart(isExpandingFullyAbove)
}
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
startViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
endViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
onLaunchAnimationEnd()
}
override fun onLaunchAnimationProgress(
state: LaunchAnimator.State,
progress: Float,
linearProgress: Float
) {
startViewController.onLaunchAnimationProgress(state, progress, linearProgress)
// The end view is visible only iff the starting view is not visible.
state.visible = !state.visible
endViewController.onLaunchAnimationProgress(state, progress, linearProgress)
// If the dialog content is complex, its dimension might change during the
// launch animation. The animation end position might also change during the
// exit animation, for instance when locking the phone when the dialog is open.
// Therefore we update the end state to the new position/size. Usually the
// dialog dimension or position will change in the early frames, so changing the
// end state shouldn't really be noticeable.
endViewController.fillGhostedViewState(endState)
}
} }
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
// During launch, onLaunchAnimationStart will be used to remove the temporary touch
// surface ghost so it is important to call this before calling
// onLaunchAnimationStart on the controller (which will create its own ghost).
onLaunchAnimationStart()
startViewController.onLaunchAnimationStart(isExpandingFullyAbove)
endViewController.onLaunchAnimationStart(isExpandingFullyAbove)
}
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
startViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
endViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
onLaunchAnimationEnd()
}
override fun onLaunchAnimationProgress(
state: LaunchAnimator.State,
progress: Float,
linearProgress: Float
) {
startViewController.onLaunchAnimationProgress(state, progress, linearProgress)
// The end view is visible only iff the starting view is not visible.
state.visible = !state.visible
endViewController.onLaunchAnimationProgress(state, progress, linearProgress)
// If the dialog content is complex, its dimension might change during the launch
// animation. The animation end position might also change during the exit
// animation, for instance when locking the phone when the dialog is open. Therefore
// we update the end state to the new position/size. Usually the dialog dimension or
// position will change in the early frames, so changing the end state shouldn't
// really be noticeable.
endViewController.fillGhostedViewState(endState)
}
}
launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor) launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
} }
@@ -821,7 +853,7 @@ private class AnimatedDialog(
return (touchSurface.parent as? View)?.isShown ?: true return (touchSurface.parent as? View)?.isShown ?: true
} }
/** A layout listener to animate the change of bounds of the dialog background. */ /** A layout listener to animate the change of bounds of the dialog background. */
class AnimatedBoundsLayoutListener : View.OnLayoutChangeListener { class AnimatedBoundsLayoutListener : View.OnLayoutChangeListener {
companion object { companion object {
private const val ANIMATION_DURATION = 500L private const val ANIMATION_DURATION = 500L
@@ -866,32 +898,35 @@ private class AnimatedDialog(
currentAnimator?.cancel() currentAnimator?.cancel()
currentAnimator = null currentAnimator = null
val animator = ValueAnimator.ofFloat(0f, 1f).apply { val animator =
duration = ANIMATION_DURATION ValueAnimator.ofFloat(0f, 1f).apply {
interpolator = Interpolators.STANDARD duration = ANIMATION_DURATION
interpolator = Interpolators.STANDARD
addListener(object : AnimatorListenerAdapter() { addListener(
override fun onAnimationEnd(animation: Animator) { object : AnimatorListenerAdapter() {
currentAnimator = null override fun onAnimationEnd(animation: Animator) {
currentAnimator = null
}
}
)
addUpdateListener { animatedValue ->
val progress = animatedValue.animatedFraction
// Compute new bounds.
bounds.left = MathUtils.lerp(startLeft, left, progress).roundToInt()
bounds.top = MathUtils.lerp(startTop, top, progress).roundToInt()
bounds.right = MathUtils.lerp(startRight, right, progress).roundToInt()
bounds.bottom = MathUtils.lerp(startBottom, bottom, progress).roundToInt()
// Set the new bounds.
view.left = bounds.left
view.top = bounds.top
view.right = bounds.right
view.bottom = bounds.bottom
} }
})
addUpdateListener { animatedValue ->
val progress = animatedValue.animatedFraction
// Compute new bounds.
bounds.left = MathUtils.lerp(startLeft, left, progress).roundToInt()
bounds.top = MathUtils.lerp(startTop, top, progress).roundToInt()
bounds.right = MathUtils.lerp(startRight, right, progress).roundToInt()
bounds.bottom = MathUtils.lerp(startBottom, bottom, progress).roundToInt()
// Set the new bounds.
view.left = bounds.left
view.top = bounds.top
view.right = bounds.right
view.bottom = bounds.bottom
} }
}
currentAnimator = animator currentAnimator = animator
animator.start() animator.start()

View File

@@ -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,10 +159,11 @@ open class GhostedViewLaunchAnimatorController(
} }
override fun createAnimatorState(): LaunchAnimator.State { override fun createAnimatorState(): LaunchAnimator.State {
val state = LaunchAnimator.State( val state =
topCornerRadius = getCurrentTopCornerRadius(), LaunchAnimator.State(
bottomCornerRadius = getCurrentBottomCornerRadius() topCornerRadius = getCurrentTopCornerRadius(),
) bottomCornerRadius = getCurrentBottomCornerRadius()
)
fillGhostedViewState(state) fillGhostedViewState(state)
return state return state
} }
@@ -255,13 +254,14 @@ 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]
) )
ghostViewMatrix.postTranslate( ghostViewMatrix.postTranslate(
(leftChange + rightChange) / 2f, (leftChange + rightChange) / 2f,
(topChange + bottomChange) / 2f (topChange + bottomChange) / 2f
) )
ghostView.animationMatrix = ghostViewMatrix ghostView.animationMatrix = ghostViewMatrix

View File

@@ -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)
@@ -75,10 +72,10 @@ class LaunchAnimator(
* with the opening window. * with the opening window.
* *
* 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,10 +258,11 @@ 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 =
setColor(windowBackgroundColor) GradientDrawable().apply {
alpha = 0 setColor(windowBackgroundColor)
} alpha = 0
}
// Update state. // Update state.
val animator = ValueAnimator.ofFloat(0f, 1f) val animator = ValueAnimator.ofFloat(0f, 1f)
@@ -270,38 +273,41 @@ 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.viewRootImpl != controller.launchContainer.viewRootImpl openingWindowSyncView != null &&
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(
override fun onAnimationStart(animation: Animator?, isReverse: Boolean) { object : AnimatorListenerAdapter() {
if (DEBUG) { override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
Log.d(TAG, "Animation started") if (DEBUG) {
Log.d(TAG, "Animation started")
}
controller.onLaunchAnimationStart(isExpandingFullyAbove)
// Add the drawable to the launch container overlay. Overlays always draw
// drawables after views, so we know that it will be drawn above any view added
// by the controller.
launchContainerOverlay.add(windowBackgroundLayer)
} }
controller.onLaunchAnimationStart(isExpandingFullyAbove)
// Add the drawable to the launch container overlay. Overlays always draw override fun onAnimationEnd(animation: Animator?) {
// drawables after views, so we know that it will be drawn above any view added if (DEBUG) {
// by the controller. Log.d(TAG, "Animation ended")
launchContainerOverlay.add(windowBackgroundLayer) }
} controller.onLaunchAnimationEnd(isExpandingFullyAbove)
launchContainerOverlay.remove(windowBackgroundLayer)
override fun onAnimationEnd(animation: Animator?) { if (moveBackgroundLayerWhenAppIsVisible) {
if (DEBUG) { openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
Log.d(TAG, "Animation ended") }
}
controller.onLaunchAnimationEnd(isExpandingFullyAbove)
launchContainerOverlay.remove(windowBackgroundLayer)
if (moveBackgroundLayerWhenAppIsVisible) {
openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
} }
} }
}) )
animator.addUpdateListener { animation -> animator.addUpdateListener { animation ->
if (cancelled) { if (cancelled) {
@@ -333,12 +339,13 @@ 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 =
timings, getProgress(
linearProgress, timings,
timings.contentBeforeFadeOutDelay, linearProgress,
timings.contentBeforeFadeOutDuration timings.contentBeforeFadeOutDelay,
) < 1 timings.contentBeforeFadeOutDuration
) < 1
if (moveBackgroundLayerWhenAppIsVisible && !state.visible && !movedBackgroundLayer) { if (moveBackgroundLayerWhenAppIsVisible && !state.visible && !movedBackgroundLayer) {
// The expanding view is not visible, so the opening app is visible. If this is the // The expanding view is not visible, so the opening app is visible. If this is the
@@ -352,17 +359,19 @@ class LaunchAnimator(
ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {}) ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {})
} }
val container = if (movedBackgroundLayer) { val container =
openingWindowSyncView!! if (movedBackgroundLayer) {
} else { openingWindowSyncView!!
controller.launchContainer } else {
} controller.launchContainer
}
applyStateToWindowBackgroundLayer( applyStateToWindowBackgroundLayer(
windowBackgroundLayer, windowBackgroundLayer,
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,23 +425,25 @@ 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 =
timings, getProgress(
linearProgress, timings,
timings.contentBeforeFadeOutDelay, linearProgress,
timings.contentBeforeFadeOutDuration timings.contentBeforeFadeOutDelay,
) timings.contentBeforeFadeOutDuration
)
if (fadeInProgress < 1) { if (fadeInProgress < 1) {
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 =
timings, getProgress(
linearProgress, timings,
timings.contentAfterFadeInDelay, linearProgress,
timings.contentAfterFadeInDuration timings.contentAfterFadeInDelay,
) timings.contentAfterFadeInDuration
)
val alpha = val alpha =
1 - interpolators.contentAfterFadeInInterpolator.getInterpolation(fadeOutProgress) 1 - interpolators.contentAfterFadeInInterpolator.getInterpolation(fadeOutProgress)
drawable.alpha = (alpha * 0xFF).roundToInt() drawable.alpha = (alpha * 0xFF).roundToInt()
@@ -439,6 +451,8 @@ class LaunchAnimator(
if (drawHole) { if (drawHole) {
drawable.setXfermode(SRC_MODE) drawable.setXfermode(SRC_MODE)
} }
} else {
drawable.alpha = 0xFF
} }
} }
} }

View File

@@ -27,4 +27,4 @@ interface LaunchableView {
* [transition][android.view.View.setTransitionVisibility] visibility changes must be blocked. * [transition][android.view.View.setTransitionVisibility] visibility changes must be blocked.
*/ */
fun setShouldBlockVisibilityChanges(block: Boolean) fun setShouldBlockVisibilityChanges(block: Boolean)
} }

View File

@@ -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,12 +150,13 @@ 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),
/* isTranslucent */ (change.flags and TransitionInfo.FLAG_TRANSLUCENT != 0 || /* isTranslucent */ (change.flags and TransitionInfo.FLAG_TRANSLUCENT != 0 ||
change.flags and TransitionInfo.FLAG_SHOW_WALLPAPER != 0), change.flags and TransitionInfo.FLAG_SHOW_WALLPAPER != 0),
/* clipRect */ null, /* clipRect */ null,
/* contentInsets */ Rect(0, 0, 0, 0), /* contentInsets */ Rect(0, 0, 0, 0),
/* prefixOrderIndex */ order, /* prefixOrderIndex */ order,
@@ -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,37 +312,47 @@ class RemoteTransitionAdapter {
} }
} }
t.apply() t.apply()
val animationFinishedCallback = object : IRemoteAnimationFinishedCallback { val animationFinishedCallback =
override fun onAnimationFinished() { object : IRemoteAnimationFinishedCallback {
val finishTransaction = SurfaceControl.Transaction() override fun onAnimationFinished() {
counterLauncher.cleanUp(finishTransaction) val finishTransaction = SurfaceControl.Transaction()
counterWallpaper.cleanUp(finishTransaction) counterLauncher.cleanUp(finishTransaction)
// Release surface references now. This is apparently to free GPU memory counterWallpaper.cleanUp(finishTransaction)
// while doing quick operations (eg. during CTS). // Release surface references now. This is apparently to free GPU
for (i in info.changes.indices.reversed()) { // memory while doing quick operations (eg. during CTS).
info.changes[i].leash.release() for (i in info.changes.indices.reversed()) {
info.changes[i].leash.release()
}
for (i in leashMap.size - 1 downTo 0) {
leashMap.valueAt(i).release()
}
try {
finishCallback.onTransitionFinished(
null /* wct */,
finishTransaction
)
} catch (e: RemoteException) {
Log.e(
"ActivityOptionsCompat",
"Failed to call app controlled" +
" animation finished callback",
e
)
}
} }
for (i in leashMap.size - 1 downTo 0) {
leashMap.valueAt(i).release()
}
try {
finishCallback.onTransitionFinished(null /* wct */,
finishTransaction)
} catch (e: RemoteException) {
Log.e("ActivityOptionsCompat", "Failed to call app controlled" +
" animation finished callback", e)
}
}
override fun asBinder(): IBinder? { override fun asBinder(): IBinder? {
return null return null
}
} }
}
// 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,18 +369,14 @@ 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
private set private set
@@ -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,21 +415,19 @@ 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
finishTransaction.remove(surface!!) finishTransaction.remove(surface!!)
} }
} }
} }

View File

@@ -31,7 +31,7 @@ object ShadeInterpolation {
} else { } else {
val oneMinusFrac = 1f - mappedFraction val oneMinusFrac = 1f - mappedFraction
(1f - 0.5f * (1f - Math.cos((3.14159f * oneMinusFrac * oneMinusFrac).toDouble()))) (1f - 0.5f * (1f - Math.cos((3.14159f * oneMinusFrac * oneMinusFrac).toDouble())))
.toFloat() .toFloat()
} }
} }
} }

View File

@@ -41,12 +41,13 @@ 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 =
Bound.LEFT to createViewProperty(Bound.LEFT), mapOf(
Bound.TOP to createViewProperty(Bound.TOP), Bound.LEFT to createViewProperty(Bound.LEFT),
Bound.RIGHT to createViewProperty(Bound.RIGHT), Bound.TOP to createViewProperty(Bound.TOP),
Bound.BOTTOM to createViewProperty(Bound.BOTTOM) Bound.RIGHT to createViewProperty(Bound.RIGHT),
) Bound.BOTTOM to createViewProperty(Bound.BOTTOM)
)
private fun createViewProperty(bound: Bound): IntProperty<View> { private fun createViewProperty(bound: Bound): IntProperty<View> {
return object : IntProperty<View>(bound.label) { return object : IntProperty<View>(bound.label) {
@@ -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,9 +181,13 @@ 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,24 +260,26 @@ class ViewHierarchyAnimator {
return return
} }
val startValues = processStartValues( val startValues =
origin, processStartValues(
left, origin,
top, left,
right, top,
bottom, right,
startLeft, bottom,
startTop, startLeft,
startRight, startTop,
startBottom, startRight,
ignorePreviousValues startBottom,
) ignorePreviousValues
val endValues = mapOf( )
Bound.LEFT to left, val endValues =
Bound.TOP to top, mapOf(
Bound.RIGHT to right, Bound.LEFT to left,
Bound.BOTTOM to bottom Bound.TOP to top,
) Bound.RIGHT to right,
Bound.BOTTOM to bottom
)
val boundsToAnimate = mutableSetOf<Bound>() val boundsToAnimate = mutableSetOf<Bound>()
if (startValues.getValue(Bound.LEFT) != left) boundsToAnimate.add(Bound.LEFT) if (startValues.getValue(Bound.LEFT) != left) boundsToAnimate.add(Bound.LEFT)
@@ -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,19 +348,21 @@ 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 =
Bound.LEFT to rootView.left, mapOf(
Bound.TOP to rootView.top, Bound.LEFT to rootView.left,
Bound.RIGHT to rootView.right, Bound.TOP to rootView.top,
Bound.BOTTOM to rootView.bottom Bound.RIGHT to rootView.right,
) Bound.BOTTOM to rootView.bottom
val endValues = processEndValuesForRemoval( )
destination, val endValues =
rootView.left, processEndValuesForRemoval(
rootView.top, destination,
rootView.right, rootView.left,
rootView.bottom rootView.top,
) rootView.right,
rootView.bottom
)
val boundsToAnimate = mutableSetOf<Bound>() val boundsToAnimate = mutableSetOf<Bound>()
if (rootView.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT) if (rootView.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT)
@@ -400,20 +404,24 @@ class ViewHierarchyAnimator {
(animation.animatedValue as Float) * startAlphas[i] (animation.animatedValue as Float) * startAlphas[i]
} }
} }
animator.addListener(object : AnimatorListenerAdapter() { animator.addListener(
override fun onAnimationEnd(animation: Animator) { object : AnimatorListenerAdapter() {
rootView.animate() override fun onAnimationEnd(animation: Animator) {
.alpha(0f) rootView
.setInterpolator(Interpolators.ALPHA_OUT) .animate()
.setDuration(duration / 2) .alpha(0f)
.withEndAction { parent.overlay.remove(rootView) } .setInterpolator(Interpolators.ALPHA_OUT)
.start() .setDuration(duration / 2)
.withEndAction { parent.overlay.remove(rootView) }
.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,21 +448,23 @@ 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 =
Bound.LEFT to child.left, mapOf(
Bound.TOP to child.top, Bound.LEFT to child.left,
Bound.RIGHT to child.right, Bound.TOP to child.top,
Bound.BOTTOM to child.bottom Bound.RIGHT to child.right,
) Bound.BOTTOM to child.bottom
val childEndValues = processChildEndValuesForRemoval( )
destination, val childEndValues =
child.left, processChildEndValuesForRemoval(
child.top, destination,
child.right, child.left,
child.bottom, child.top,
endValues.getValue(Bound.RIGHT) - endValues.getValue(Bound.LEFT), child.right,
endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP) child.bottom,
) endValues.getValue(Bound.RIGHT) - endValues.getValue(Bound.LEFT),
endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP)
)
val boundsToAnimate = mutableSetOf<Bound>() val boundsToAnimate = mutableSetOf<Bound>()
if (child.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT) if (child.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT)
@@ -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,42 +567,54 @@ class ViewHierarchyAnimator {
var bottom = startBottom var bottom = startBottom
if (origin != null) { if (origin != null) {
left = when (origin) { left =
Hotspot.CENTER -> (newLeft + newRight) / 2 when (origin) {
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft) Hotspot.CENTER -> (newLeft + newRight) / 2
Hotspot.TOP, Hotspot.BOTTOM -> newLeft Hotspot.BOTTOM_LEFT,
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max( Hotspot.LEFT,
startRight, Hotspot.TOP_LEFT -> min(startLeft, newLeft)
newRight Hotspot.TOP,
) Hotspot.BOTTOM -> newLeft
} Hotspot.TOP_RIGHT,
top = when (origin) { Hotspot.RIGHT,
Hotspot.CENTER -> (newTop + newBottom) / 2 Hotspot.BOTTOM_RIGHT -> max(startRight, newRight)
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop) }
Hotspot.LEFT, Hotspot.RIGHT -> newTop top =
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max( when (origin) {
startBottom, Hotspot.CENTER -> (newTop + newBottom) / 2
newBottom Hotspot.TOP_LEFT,
) Hotspot.TOP,
} Hotspot.TOP_RIGHT -> min(startTop, newTop)
right = when (origin) { Hotspot.LEFT,
Hotspot.CENTER -> (newLeft + newRight) / 2 Hotspot.RIGHT -> newTop
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max( Hotspot.BOTTOM_RIGHT,
startRight, Hotspot.BOTTOM,
newRight Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom)
) }
Hotspot.TOP, Hotspot.BOTTOM -> newRight right =
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft) when (origin) {
} Hotspot.CENTER -> (newLeft + newRight) / 2
bottom = when (origin) { Hotspot.TOP_RIGHT,
Hotspot.CENTER -> (newTop + newBottom) / 2 Hotspot.RIGHT,
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max( Hotspot.BOTTOM_RIGHT -> max(startRight, newRight)
startBottom, Hotspot.TOP,
newBottom Hotspot.BOTTOM -> newRight
) Hotspot.BOTTOM_LEFT,
Hotspot.LEFT, Hotspot.RIGHT -> newBottom Hotspot.LEFT,
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop) Hotspot.TOP_LEFT -> min(startLeft, newLeft)
} }
bottom =
when (origin) {
Hotspot.CENTER -> (newTop + newBottom) / 2
Hotspot.BOTTOM_RIGHT,
Hotspot.BOTTOM,
Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom)
Hotspot.LEFT,
Hotspot.RIGHT -> newBottom
Hotspot.TOP_LEFT,
Hotspot.TOP,
Hotspot.TOP_RIGHT -> min(startTop, newTop)
}
} }
return mapOf( return mapOf(
@@ -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,32 +658,54 @@ class ViewHierarchyAnimator {
right: Int, right: Int,
bottom: Int bottom: Int
): Map<Bound, Int> { ): Map<Bound, Int> {
val endLeft = when (destination) { val endLeft =
Hotspot.CENTER -> (left + right) / 2 when (destination) {
Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP -> Hotspot.CENTER -> (left + right) / 2
left Hotspot.BOTTOM,
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> right Hotspot.BOTTOM_LEFT,
} Hotspot.LEFT,
val endTop = when (destination) { Hotspot.TOP_LEFT,
Hotspot.CENTER -> (top + bottom) / 2 Hotspot.TOP -> left
Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT -> Hotspot.TOP_RIGHT,
top Hotspot.RIGHT,
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> bottom Hotspot.BOTTOM_RIGHT -> right
} }
val endRight = when (destination) { val endTop =
Hotspot.CENTER -> (left + right) / 2 when (destination) {
Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.CENTER -> (top + bottom) / 2
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM -> Hotspot.LEFT,
right Hotspot.TOP_LEFT,
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> left Hotspot.TOP,
} Hotspot.TOP_RIGHT,
val endBottom = when (destination) { Hotspot.RIGHT -> top
Hotspot.CENTER -> (top + bottom) / 2 Hotspot.BOTTOM_RIGHT,
Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM,
Hotspot.BOTTOM_LEFT, Hotspot.LEFT -> Hotspot.BOTTOM_LEFT -> bottom
bottom }
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> top val endRight =
} when (destination) {
Hotspot.CENTER -> (left + right) / 2
Hotspot.TOP,
Hotspot.TOP_RIGHT,
Hotspot.RIGHT,
Hotspot.BOTTOM_RIGHT,
Hotspot.BOTTOM -> right
Hotspot.BOTTOM_LEFT,
Hotspot.LEFT,
Hotspot.TOP_LEFT -> left
}
val endBottom =
when (destination) {
Hotspot.CENTER -> (top + bottom) / 2
Hotspot.RIGHT,
Hotspot.BOTTOM_RIGHT,
Hotspot.BOTTOM,
Hotspot.BOTTOM_LEFT,
Hotspot.LEFT -> bottom
Hotspot.TOP_LEFT,
Hotspot.TOP,
Hotspot.TOP_RIGHT -> top
}
return mapOf( return mapOf(
Bound.LEFT to endLeft, Bound.LEFT to endLeft,
@@ -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,32 +745,54 @@ 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 =
Hotspot.CENTER -> (parentWidth / 2) - halfWidth when (destination) {
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> -halfWidth Hotspot.CENTER -> (parentWidth / 2) - halfWidth
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth Hotspot.BOTTOM_LEFT,
Hotspot.TOP, Hotspot.BOTTOM -> left Hotspot.LEFT,
} Hotspot.TOP_LEFT -> -halfWidth
val endTop = when (destination) { Hotspot.TOP_RIGHT,
Hotspot.CENTER -> (parentHeight / 2) - halfHeight Hotspot.RIGHT,
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> -halfHeight Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> Hotspot.TOP,
parentHeight - halfHeight Hotspot.BOTTOM -> left
Hotspot.LEFT, Hotspot.RIGHT -> top }
} val endTop =
val endRight = when (destination) { when (destination) {
Hotspot.CENTER -> (parentWidth / 2) + halfWidth Hotspot.CENTER -> (parentHeight / 2) - halfHeight
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth Hotspot.TOP_LEFT,
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> halfWidth Hotspot.TOP,
Hotspot.TOP, Hotspot.BOTTOM -> right Hotspot.TOP_RIGHT -> -halfHeight
} Hotspot.BOTTOM_RIGHT,
val endBottom = when (destination) { Hotspot.BOTTOM,
Hotspot.CENTER -> (parentHeight / 2) + halfHeight Hotspot.BOTTOM_LEFT -> parentHeight - halfHeight
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> Hotspot.LEFT,
parentHeight + halfHeight Hotspot.RIGHT -> top
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> halfHeight }
Hotspot.LEFT, Hotspot.RIGHT -> bottom val endRight =
} when (destination) {
Hotspot.CENTER -> (parentWidth / 2) + halfWidth
Hotspot.TOP_RIGHT,
Hotspot.RIGHT,
Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth
Hotspot.BOTTOM_LEFT,
Hotspot.LEFT,
Hotspot.TOP_LEFT -> halfWidth
Hotspot.TOP,
Hotspot.BOTTOM -> right
}
val endBottom =
when (destination) {
Hotspot.CENTER -> (parentHeight / 2) + halfHeight
Hotspot.BOTTOM_RIGHT,
Hotspot.BOTTOM,
Hotspot.BOTTOM_LEFT -> parentHeight + halfHeight
Hotspot.TOP_LEFT,
Hotspot.TOP,
Hotspot.TOP_RIGHT -> halfHeight
Hotspot.LEFT,
Hotspot.RIGHT -> bottom
}
return mapOf( return mapOf(
Bound.LEFT to endLeft, Bound.LEFT to endLeft,
@@ -790,44 +862,49 @@ class ViewHierarchyAnimator {
duration: Long, duration: Long,
ephemeral: Boolean ephemeral: Boolean
) { ) {
val propertyValuesHolders = buildList { val propertyValuesHolders =
bounds.forEach { bound -> buildList {
add( bounds.forEach { bound ->
PropertyValuesHolder.ofInt( add(
PROPERTIES[bound], PropertyValuesHolder.ofInt(
startValues.getValue(bound), PROPERTIES[bound],
endValues.getValue(bound) startValues.getValue(bound),
) endValues.getValue(bound)
) )
} )
}.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(
var cancelled = false object : AnimatorListenerAdapter() {
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
recursivelyRemoveListener(view) // listener.
recursivelyRemoveListener(view)
}
}
override fun onAnimationCancel(animation: Animator?) {
cancelled = true
} }
} }
)
override fun onAnimationCancel(animation: Animator?) {
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) {

View File

@@ -11,37 +11,36 @@ 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 ||
if (!view.isAttachedToWindow || view.viewRootImpl == null || otherView.viewRootImpl == null ||
!otherView.isAttachedToWindow || otherView.viewRootImpl == null || view.viewRootImpl == otherView.viewRootImpl
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 =
val syncId = setupSync(Runnable { then() }) SurfaceSyncer().apply {
addToSync(syncId, view) val syncId = setupSync(Runnable { then() })
addToSync(syncId, otherView) addToSync(syncId, view)
markSyncReady(syncId) addToSync(syncId, otherView)
} markSyncReady(syncId)
}
} }
/** /** 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)
} }
} }