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,7 +59,8 @@ class ActivityLaunchAnimator(
companion object { companion object {
/** The timings when animating a View into an app. */ /** The timings when animating a View into an app. */
@JvmField @JvmField
val TIMINGS = LaunchAnimator.Timings( val TIMINGS =
LaunchAnimator.Timings(
totalDuration = 500L, totalDuration = 500L,
contentBeforeFadeOutDelay = 0L, contentBeforeFadeOutDelay = 0L,
contentBeforeFadeOutDuration = 150L, contentBeforeFadeOutDuration = 150L,
@@ -72,13 +73,12 @@ class ActivityLaunchAnimator(
* showing the app (which is under the dialog window) so that the dialog window dim is fully * showing the app (which is under the dialog window) so that the dialog window dim is fully
* faded out, to avoid flicker. * faded out, to avoid flicker.
*/ */
val DIALOG_TIMINGS = TIMINGS.copy( val DIALOG_TIMINGS =
contentBeforeFadeOutDuration = 200L, TIMINGS.copy(contentBeforeFadeOutDuration = 200L, contentAfterFadeInDelay = 200L)
contentAfterFadeInDelay = 200L
)
/** The interpolators when animating a View or a dialog into an app. */ /** The interpolators when animating a View or a dialog into an app. */
val INTERPOLATORS = LaunchAnimator.Interpolators( val INTERPOLATORS =
LaunchAnimator.Interpolators(
positionInterpolator = Interpolators.EMPHASIZED, positionInterpolator = Interpolators.EMPHASIZED,
positionXInterpolator = createPositionXInterpolator(), positionXInterpolator = createPositionXInterpolator(),
contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN, contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN,
@@ -98,7 +98,8 @@ class ActivityLaunchAnimator(
private const val LAUNCH_TIMEOUT = 1000L private const val LAUNCH_TIMEOUT = 1000L
private fun createPositionXInterpolator(): Interpolator { private fun createPositionXInterpolator(): Interpolator {
val path = Path().apply { val path =
Path().apply {
moveTo(0f, 0f) moveTo(0f, 0f)
cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f) cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f)
cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f) cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f)
@@ -150,14 +151,18 @@ class ActivityLaunchAnimator(
return return
} }
val callback = this.callback ?: throw IllegalStateException( val callback =
"ActivityLaunchAnimator.callback must be set before using this animator") this.callback
?: throw IllegalStateException(
"ActivityLaunchAnimator.callback must be set before using this animator"
)
val runner = Runner(controller) val runner = Runner(controller)
val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen
// Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the // Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the
// keyguard with the animation // keyguard with the animation
val animationAdapter = if (!hideKeyguardWithAnimation) { val animationAdapter =
if (!hideKeyguardWithAnimation) {
RemoteAnimationAdapter( RemoteAnimationAdapter(
runner, runner,
TIMINGS.totalDuration, TIMINGS.totalDuration,
@@ -171,8 +176,12 @@ class ActivityLaunchAnimator(
// activity launches. // activity launches.
if (packageName != null && animationAdapter != null) { if (packageName != null && animationAdapter != null) {
try { try {
ActivityTaskManager.getService().registerRemoteAnimationForNextActivityStart( ActivityTaskManager.getService()
packageName, animationAdapter, null /* launchCookie */) .registerRemoteAnimationForNextActivityStart(
packageName,
animationAdapter,
null /* launchCookie */
)
} catch (e: RemoteException) { } catch (e: RemoteException) {
Log.w(TAG, "Unable to register the remote animation", e) Log.w(TAG, "Unable to register the remote animation", e)
} }
@@ -188,8 +197,11 @@ class ActivityLaunchAnimator(
(launchResult == ActivityManager.START_DELIVERED_TO_TOP && (launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
hideKeyguardWithAnimation) hideKeyguardWithAnimation)
Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " + Log.i(
"hideKeyguardWithAnimation=$hideKeyguardWithAnimation") TAG,
"launchResult=$launchResult willAnimate=$willAnimate " +
"hideKeyguardWithAnimation=$hideKeyguardWithAnimation"
)
controller.callOnIntentStartedOnMainThread(willAnimate) controller.callOnIntentStartedOnMainThread(willAnimate)
// If we expect an animation, post a timeout to cancel it in case the remote animation is // If we expect an animation, post a timeout to cancel it in case the remote animation is
@@ -206,9 +218,7 @@ class ActivityLaunchAnimator(
private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) { private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) {
if (Looper.myLooper() != Looper.getMainLooper()) { if (Looper.myLooper() != Looper.getMainLooper()) {
this.launchContainer.context.mainExecutor.execute { this.launchContainer.context.mainExecutor.execute { this.onIntentStarted(willAnimate) }
this.onIntentStarted(willAnimate)
}
} else { } else {
this.onIntentStarted(willAnimate) this.onIntentStarted(willAnimate)
} }
@@ -246,8 +256,7 @@ class ActivityLaunchAnimator(
} }
/** Create a new animation [Runner] controlled by [controller]. */ /** Create a new animation [Runner] controlled by [controller]. */
@VisibleForTesting @VisibleForTesting fun createRunner(controller: Controller): Runner = Runner(controller)
fun createRunner(controller: Controller): Runner = Runner(controller)
interface PendingIntentStarter { interface PendingIntentStarter {
/** /**
@@ -271,19 +280,16 @@ class ActivityLaunchAnimator(
interface Listener { interface Listener {
/** Called when an activity launch animation started. */ /** Called when an activity launch animation started. */
@JvmDefault @JvmDefault fun onLaunchAnimationStart() {}
fun onLaunchAnimationStart() {}
/** /**
* Called when an activity launch animation is finished. This will be called if and only if * Called when an activity launch animation is finished. This will be called if and only if
* [onLaunchAnimationStart] was called earlier. * [onLaunchAnimationStart] was called earlier.
*/ */
@JvmDefault @JvmDefault fun onLaunchAnimationEnd() {}
fun onLaunchAnimationEnd() {}
/** Called when an activity launch animation made progress. */ /** Called when an activity launch animation made progress. */
@JvmDefault @JvmDefault fun onLaunchAnimationProgress(linearProgress: Float) {}
fun onLaunchAnimationProgress(linearProgress: Float) {}
} }
/** /**
@@ -326,6 +332,17 @@ class ActivityLaunchAnimator(
val isDialogLaunch: Boolean val isDialogLaunch: Boolean
get() = false get() = false
/**
* Whether the expandable controller by this [Controller] is below the launching window that
* is going to be animated.
*
* This should be `false` when launching an app from the shade or status bar, given that
* they are drawn above all apps. This is usually `true` when using this launcher in a
* normal app or a launcher, that are drawn below the animating activity/window.
*/
val isBelowAnimatingWindow: Boolean
get() = false
/** /**
* The intent was started. If [willAnimate] is false, nothing else will happen and the * The intent was started. If [willAnimate] is false, nothing else will happen and the
* animation will not be started. * animation will not be started.
@@ -394,9 +411,7 @@ class ActivityLaunchAnimator(
return return
} }
context.mainExecutor.execute { context.mainExecutor.execute { startAnimation(apps, nonApps, iCallback) }
startAnimation(apps, nonApps, iCallback)
}
} }
private fun startAnimation( private fun startAnimation(
@@ -408,9 +423,7 @@ class ActivityLaunchAnimator(
Log.d(TAG, "Remote animation started") Log.d(TAG, "Remote animation started")
} }
val window = apps?.firstOrNull { val window = apps?.firstOrNull { it.mode == RemoteAnimationTarget.MODE_OPENING }
it.mode == RemoteAnimationTarget.MODE_OPENING
}
if (window == null) { if (window == null) {
Log.i(TAG, "Aborting the animation as no window is opening") Log.i(TAG, "Aborting the animation as no window is opening")
@@ -420,23 +433,26 @@ class ActivityLaunchAnimator(
return return
} }
val navigationBar = nonApps?.firstOrNull { val navigationBar =
nonApps?.firstOrNull {
it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
} }
val windowBounds = window.screenSpaceBounds val windowBounds = window.screenSpaceBounds
val endState = LaunchAnimator.State( val endState =
LaunchAnimator.State(
top = windowBounds.top, top = windowBounds.top,
bottom = windowBounds.bottom, bottom = windowBounds.bottom,
left = windowBounds.left, left = windowBounds.left,
right = windowBounds.right right = windowBounds.right
) )
val callback = this@ActivityLaunchAnimator.callback!! val callback = this@ActivityLaunchAnimator.callback!!
val windowBackgroundColor = window.taskInfo?.let { callback.getBackgroundColor(it) } val windowBackgroundColor =
?: window.backgroundColor window.taskInfo?.let { callback.getBackgroundColor(it) } ?: window.backgroundColor
// Make sure we use the modified timings when animating a dialog into an app. // Make sure we use the modified timings when animating a dialog into an app.
val launchAnimator = if (controller.isDialogLaunch) { val launchAnimator =
if (controller.isDialogLaunch) {
dialogToAppAnimator dialogToAppAnimator
} else { } else {
launchAnimator launchAnimator
@@ -446,12 +462,14 @@ class ActivityLaunchAnimator(
// instead of recomputing isExpandingFullyAbove here. // instead of recomputing isExpandingFullyAbove here.
val isExpandingFullyAbove = val isExpandingFullyAbove =
launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState) launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState)
val endRadius = if (isExpandingFullyAbove) { val endRadius =
if (isExpandingFullyAbove) {
// Most of the time, expanding fully above the root view means expanding in full // Most of the time, expanding fully above the root view means expanding in full
// screen. // screen.
ScreenDecorationsUtils.getWindowCornerRadius(context) ScreenDecorationsUtils.getWindowCornerRadius(context)
} else { } else {
// This usually means we are in split screen mode, so 2 out of 4 corners will have // This usually means we are in split screen mode, so 2 out of 4 corners will
// have
// a radius of 0. // a radius of 0.
0f 0f
} }
@@ -460,7 +478,8 @@ class ActivityLaunchAnimator(
// We animate the opening window and delegate the view expansion to [this.controller]. // We animate the opening window and delegate the view expansion to [this.controller].
val delegate = this.controller val delegate = this.controller
val controller = object : LaunchAnimator.Controller by delegate { val controller =
object : Controller by delegate {
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
listeners.forEach { it.onLaunchAnimationStart() } listeners.forEach { it.onLaunchAnimationStart() }
delegate.onLaunchAnimationStart(isExpandingFullyAbove) delegate.onLaunchAnimationStart(isExpandingFullyAbove)
@@ -477,10 +496,10 @@ class ActivityLaunchAnimator(
progress: Float, progress: Float,
linearProgress: Float linearProgress: Float
) { ) {
// Apply the state to the window only if it is visible, i.e. when the expanding // Apply the state to the window only if it is visible, i.e. when the
// view is *not* visible. // expanding view is *not* visible.
if (!state.visible) { if (!state.visible) {
applyStateToWindow(window, state) applyStateToWindow(window, state, linearProgress)
} }
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
@@ -489,11 +508,21 @@ class ActivityLaunchAnimator(
} }
} }
animation = launchAnimator.startAnimation( animation =
controller, endState, windowBackgroundColor, drawHole = true) launchAnimator.startAnimation(
controller,
endState,
windowBackgroundColor,
fadeOutWindowBackgroundLayer = !controller.isBelowAnimatingWindow,
drawHole = !controller.isBelowAnimatingWindow,
)
} }
private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) { private fun applyStateToWindow(
window: RemoteAnimationTarget,
state: LaunchAnimator.State,
linearProgress: Float,
) {
if (transactionApplierView.viewRootImpl == null) { if (transactionApplierView.viewRootImpl == null) {
// If the view root we synchronize with was detached, don't apply any transaction // If the view root we synchronize with was detached, don't apply any transaction
// (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw). // (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw).
@@ -535,14 +564,33 @@ class ActivityLaunchAnimator(
windowCropF.bottom.roundToInt() windowCropF.bottom.roundToInt()
) )
// The alpha of the opening window. If it opens above the expandable, then it should
// fade in progressively. Otherwise, it should be fully opaque and will be progressively
// revealed as the window background color layer above the window fades out.
val alpha =
if (controller.isBelowAnimatingWindow) {
val windowProgress =
LaunchAnimator.getProgress(
TIMINGS,
linearProgress,
TIMINGS.contentAfterFadeInDelay,
TIMINGS.contentAfterFadeInDuration
)
INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation(windowProgress)
} else {
1f
}
// The scale will also be applied to the corner radius, so we divide by the scale to // The scale will also be applied to the corner radius, so we divide by the scale to
// keep the original radius. We use the max of (topCornerRadius, bottomCornerRadius) to // keep the original radius. We use the max of (topCornerRadius, bottomCornerRadius) to
// make sure that the window does not draw itself behind the expanding view. This is // make sure that the window does not draw itself behind the expanding view. This is
// especially important for lock screen animations, where the window is not clipped by // especially important for lock screen animations, where the window is not clipped by
// the shade. // the shade.
val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale
val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash) val params =
.withAlpha(1f) SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash)
.withAlpha(alpha)
.withMatrix(matrix) .withMatrix(matrix)
.withWindowCrop(windowCrop) .withWindowCrop(windowCrop)
.withCornerRadius(cornerRadius) .withCornerRadius(cornerRadius)
@@ -563,14 +611,21 @@ class ActivityLaunchAnimator(
return return
} }
val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress, val fadeInProgress =
ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT) LaunchAnimator.getProgress(
TIMINGS,
linearProgress,
ANIMATION_DELAY_NAV_FADE_IN,
ANIMATION_DURATION_NAV_FADE_OUT
)
val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash) val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
if (fadeInProgress > 0) { if (fadeInProgress > 0) {
matrix.reset() matrix.reset()
matrix.setTranslate( matrix.setTranslate(
0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat()) 0f,
(state.top - navigationBar.sourceContainerBounds.top).toFloat()
)
windowCrop.set(state.left, 0, state.right, state.height) windowCrop.set(state.left, 0, state.right, state.height)
params params
.withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress)) .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
@@ -578,8 +633,13 @@ class ActivityLaunchAnimator(
.withWindowCrop(windowCrop) .withWindowCrop(windowCrop)
.withVisibility(true) .withVisibility(true)
} else { } else {
val fadeOutProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress, 0, val fadeOutProgress =
ANIMATION_DURATION_NAV_FADE_OUT) LaunchAnimator.getProgress(
TIMINGS,
linearProgress,
0,
ANIMATION_DURATION_NAV_FADE_OUT
)
params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress)) params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
} }

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,7 +64,8 @@ class DialogLaunchAnimator @JvmOverloads constructor(
// We use the same interpolator for X and Y axis to make sure the dialog does not move out // We use the same interpolator for X and Y axis to make sure the dialog does not move out
// of the screen bounds during the animation. // of the screen bounds during the animation.
private val INTERPOLATORS = ActivityLaunchAnimator.INTERPOLATORS.copy( private val INTERPOLATORS =
ActivityLaunchAnimator.INTERPOLATORS.copy(
positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
) )
@@ -105,8 +108,10 @@ class DialogLaunchAnimator @JvmOverloads constructor(
// If the view we are launching from belongs to another dialog, then this means the caller // If the view we are launching from belongs to another dialog, then this means the caller
// intent is to launch a dialog from another dialog. // intent is to launch a dialog from another dialog.
val animatedParent = openedDialogs val animatedParent =
.firstOrNull { it.dialog.window.decorView.viewRootImpl == view.viewRootImpl } openedDialogs.firstOrNull {
it.dialog.window.decorView.viewRootImpl == view.viewRootImpl
}
val animateFrom = animatedParent?.dialogContentWithBackground ?: view val animateFrom = animatedParent?.dialogContentWithBackground ?: view
// Make sure we don't run the launch animation from the same view twice at the same time. // Make sure we don't run the launch animation from the same view twice at the same time.
@@ -118,7 +123,8 @@ class DialogLaunchAnimator @JvmOverloads constructor(
animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true) animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
val animatedDialog = AnimatedDialog( val animatedDialog =
AnimatedDialog(
launchAnimator, launchAnimator,
dreamManager, dreamManager,
interactionJankMonitor, interactionJankMonitor,
@@ -146,9 +152,8 @@ class DialogLaunchAnimator @JvmOverloads constructor(
animateFrom: Dialog, animateFrom: Dialog,
animateBackgroundBoundsChange: Boolean = false animateBackgroundBoundsChange: Boolean = false
) { ) {
val view = openedDialogs val view =
.firstOrNull { it.dialog == animateFrom } openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground
?.dialogContentWithBackground
?: throw IllegalStateException( ?: throw IllegalStateException(
"The animateFrom dialog was not animated using " + "The animateFrom dialog was not animated using " +
"DialogLaunchAnimator.showFrom(View|Dialog)" "DialogLaunchAnimator.showFrom(View|Dialog)"
@@ -175,8 +180,10 @@ class DialogLaunchAnimator @JvmOverloads constructor(
view: View, view: View,
cujType: Int? = null cujType: Int? = null
): ActivityLaunchAnimator.Controller? { ): ActivityLaunchAnimator.Controller? {
val animatedDialog = openedDialogs val animatedDialog =
.firstOrNull { it.dialog.window.decorView.viewRootImpl == view.viewRootImpl } openedDialogs.firstOrNull {
it.dialog.window.decorView.viewRootImpl == view.viewRootImpl
}
?: return null ?: return null
// At this point, we know that the intent of the caller is to dismiss the dialog to show // At this point, we know that the intent of the caller is to dismiss the dialog to show
@@ -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,7 +358,8 @@ private class AnimatedDialog(
private var isOriginalDialogViewLaidOut = false private var isOriginalDialogViewLaidOut = false
/** A layout listener to animate the dialog height change. */ /** A layout listener to animate the dialog height change. */
private val backgroundLayoutListener = if (animateBackgroundBoundsChange) { private val backgroundLayoutListener =
if (animateBackgroundBoundsChange) {
AnimatedBoundsLayoutListener() AnimatedBoundsLayoutListener()
} else { } else {
null null
@@ -381,37 +387,41 @@ private class AnimatedDialog(
val window = dialog.window!! val window = dialog.window!!
val isWindowFullScreen = val isWindowFullScreen =
window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT
val dialogContentWithBackground = if (isWindowFullScreen) { val dialogContentWithBackground =
// If the dialog window is already fullscreen, then we look for the first ViewGroup that if (isWindowFullScreen) {
// has a background (and is not the DecorView, which always has a background) and // If the dialog window is already fullscreen, then we look for the first ViewGroup
// animate towards that ViewGroup given that this is probably what represents the actual // that has a background (and is not the DecorView, which always has a background)
// dialog view. // and animate towards that ViewGroup given that this is probably what represents
// the actual dialog view.
var viewGroupWithBackground: ViewGroup? = null var viewGroupWithBackground: ViewGroup? = null
for (i in 0 until decorView.childCount) { for (i in 0 until decorView.childCount) {
viewGroupWithBackground = findFirstViewGroupWithBackground(decorView.getChildAt(i)) viewGroupWithBackground =
findFirstViewGroupWithBackground(decorView.getChildAt(i))
if (viewGroupWithBackground != null) { if (viewGroupWithBackground != null) {
break break
} }
} }
// Animate that view with the background. Throw if we didn't find one, because otherwise // Animate that view with the background. Throw if we didn't find one, because
// otherwise
// it's not clear what we should animate. // it's not clear what we should animate.
viewGroupWithBackground viewGroupWithBackground
?: throw IllegalStateException("Unable to find ViewGroup with background") ?: throw IllegalStateException("Unable to find ViewGroup with background")
} else { } else {
// We will make the dialog window (and therefore its DecorView) fullscreen to make it // We will make the dialog window (and therefore its DecorView) fullscreen to make
// possible to animate outside its bounds. // it possible to animate outside its bounds.
// //
// Before that, we add a new View as a child of the DecorView with the same size and // Before that, we add a new View as a child of the DecorView with the same size and
// gravity as that DecorView, then we add all original children of the DecorView to that // gravity as that DecorView, then we add all original children of the DecorView to
// new View. Finally we remove the background of the DecorView and add it to the new // that new View. Finally we remove the background of the DecorView and add it to
// View, then we make the DecorView fullscreen. This new View now acts as a fake (non // the new View, then we make the DecorView fullscreen. This new View now acts as a
// fullscreen) window. // fake (non fullscreen) window.
// //
// On top of that, we also add a fullscreen transparent background between the DecorView // On top of that, we also add a fullscreen transparent background between the
// and the view that we added so that we can dismiss the dialog when this view is // DecorView and the view that we added so that we can dismiss the dialog when this
// clicked. This is necessary because DecorView overrides onTouchEvent and therefore we // view is clicked. This is necessary because DecorView overrides onTouchEvent and
// can't set the click listener directly on the (now fullscreen) DecorView. // therefore we can't set the click listener directly on the (now fullscreen)
// DecorView.
val fullscreenTransparentBackground = FrameLayout(dialog.context) val fullscreenTransparentBackground = FrameLayout(dialog.context)
decorView.addView( decorView.addView(
fullscreenTransparentBackground, fullscreenTransparentBackground,
@@ -422,17 +432,19 @@ private class AnimatedDialog(
val dialogContentWithBackground = FrameLayout(dialog.context) val dialogContentWithBackground = FrameLayout(dialog.context)
dialogContentWithBackground.background = decorView.background dialogContentWithBackground.background = decorView.background
// Make the window background transparent. Note that setting the window (or DecorView) // Make the window background transparent. Note that setting the window (or
// background drawable to null leads to issues with background color (not being // DecorView) background drawable to null leads to issues with background color (not
// transparent) or with insets that are not refreshed. Therefore we need to set it to // being transparent) or with insets that are not refreshed. Therefore we need to
// something not null, hence we are using android.R.color.transparent here. // set it to something not null, hence we are using android.R.color.transparent
// here.
window.setBackgroundDrawableResource(android.R.color.transparent) window.setBackgroundDrawableResource(android.R.color.transparent)
// Close the dialog when clicking outside of it. // Close the dialog when clicking outside of it.
fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() } fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
dialogContentWithBackground.isClickable = true dialogContentWithBackground.isClickable = true
// Make sure the transparent and dialog backgrounds are not focusable by accessibility // Make sure the transparent and dialog backgrounds are not focusable by
// accessibility
// features. // features.
fullscreenTransparentBackground.importantForAccessibility = fullscreenTransparentBackground.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO View.IMPORTANT_FOR_ACCESSIBILITY_NO
@@ -455,15 +467,26 @@ private class AnimatedDialog(
dialogContentWithBackground.addView(view) dialogContentWithBackground.addView(view)
} }
// Make the window fullscreen and add a layout listener to ensure it stays fullscreen. // Make the window fullscreen and add a layout listener to ensure it stays
// fullscreen.
window.setLayout(MATCH_PARENT, MATCH_PARENT) window.setLayout(MATCH_PARENT, MATCH_PARENT)
decorViewLayoutListener = View.OnLayoutChangeListener { decorViewLayoutListener =
v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> View.OnLayoutChangeListener {
if (window.attributes.width != MATCH_PARENT || v,
left,
top,
right,
bottom,
oldLeft,
oldTop,
oldRight,
oldBottom ->
if (
window.attributes.width != MATCH_PARENT ||
window.attributes.height != MATCH_PARENT window.attributes.height != MATCH_PARENT
) { ) {
// The dialog size changed, copy its size to dialogContentWithBackground and // The dialog size changed, copy its size to dialogContentWithBackground
// make the dialog window full screen again. // and make the dialog window full screen again.
val layoutParams = dialogContentWithBackground.layoutParams val layoutParams = dialogContentWithBackground.layoutParams
layoutParams.width = window.attributes.width layoutParams.width = window.attributes.width
layoutParams.height = window.attributes.height layoutParams.height = window.attributes.height
@@ -482,7 +505,8 @@ private class AnimatedDialog(
originalDialogBackgroundColor = originalDialogBackgroundColor =
GhostedViewLaunchAnimatorController.findGradientDrawable(background) GhostedViewLaunchAnimatorController.findGradientDrawable(background)
?.color ?.color
?.defaultColor ?: Color.BLACK ?.defaultColor
?: Color.BLACK
// Make the background view invisible until we start the animation. We use the transition // Make the background view invisible until we start the animation. We use the transition
// visibility like GhostView does so that we don't mess up with the accessibility tree (see // visibility like GhostView does so that we don't mess up with the accessibility tree (see
@@ -508,7 +532,8 @@ private class AnimatedDialog(
} }
// Start the animation once the background view is properly laid out. // Start the animation once the background view is properly laid out.
dialogContentWithBackground.addOnLayoutChangeListener(object : View.OnLayoutChangeListener { dialogContentWithBackground.addOnLayoutChangeListener(
object : View.OnLayoutChangeListener {
override fun onLayoutChange( override fun onLayoutChange(
v: View, v: View,
left: Int, left: Int,
@@ -525,7 +550,8 @@ private class AnimatedDialog(
isOriginalDialogViewLaidOut = true isOriginalDialogViewLaidOut = true
maybeStartLaunchAnimation() maybeStartLaunchAnimation()
} }
}) }
)
// Disable the dim. We will enable it once we start the animation. // Disable the dim. We will enable it once we start the animation.
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
@@ -551,10 +577,12 @@ private class AnimatedDialog(
// Create a ghost of the touch surface (which will make the touch surface invisible) and add // Create a ghost of the touch surface (which will make the touch surface invisible) and add
// it to the host dialog. We trigger a one off synchronization to make sure that this is // it to the host dialog. We trigger a one off synchronization to make sure that this is
// done in sync between the two different windows. // done in sync between the two different windows.
synchronizeNextDraw(then = { synchronizeNextDraw(
then = {
isTouchSurfaceGhostDrawn = true isTouchSurfaceGhostDrawn = true
maybeStartLaunchAnimation() maybeStartLaunchAnimation()
}) }
)
GhostView.addGhost(touchSurface, decorView) GhostView.addGhost(touchSurface, decorView)
// The ghost of the touch surface was just created, so the touch surface is currently // The ghost of the touch surface was just created, so the touch surface is currently
@@ -616,14 +644,13 @@ private class AnimatedDialog(
onLaunchAnimationEnd = { onLaunchAnimationEnd = {
touchSurface.setTag(R.id.tag_launch_animation_running, null) touchSurface.setTag(R.id.tag_launch_animation_running, null)
// We hide the touch surface when the dialog is showing. We will make this // We hide the touch surface when the dialog is showing. We will make this view
// view visible again when dismissing the dialog. // visible again when dismissing the dialog.
touchSurface.visibility = View.INVISIBLE touchSurface.visibility = View.INVISIBLE
isLaunching = false isLaunching = false
// dismiss was called during the animation, dismiss again now to actually // dismiss was called during the animation, dismiss again now to actually dismiss.
// dismiss.
if (dismissRequested) { if (dismissRequested) {
dialog.dismiss() dialog.dismiss()
} }
@@ -632,8 +659,9 @@ private class AnimatedDialog(
// at the end of the launch animation, because the lauch animation already correctly // at the end of the launch animation, because the lauch animation already correctly
// handles bounds changes. // handles bounds changes.
if (backgroundLayoutListener != null) { if (backgroundLayoutListener != null) {
dialogContentWithBackground!! dialogContentWithBackground!!.addOnLayoutChangeListener(
.addOnLayoutChangeListener(backgroundLayoutListener) backgroundLayoutListener
)
} }
cuj?.run { interactionJankMonitor.end(cujType) } cuj?.run { interactionJankMonitor.end(cujType) }
} }
@@ -711,16 +739,19 @@ private class AnimatedDialog(
dialogContentWithBackground.visibility = View.INVISIBLE dialogContentWithBackground.visibility = View.INVISIBLE
if (backgroundLayoutListener != null) { if (backgroundLayoutListener != null) {
dialogContentWithBackground dialogContentWithBackground.removeOnLayoutChangeListener(
.removeOnLayoutChangeListener(backgroundLayoutListener) backgroundLayoutListener
)
} }
// Make sure that the removal of the ghost and making the touch surface visible is // Make sure that the removal of the ghost and making the touch surface visible is
// done at the same time. // done at the same time.
synchronizeNextDraw(then = { synchronizeNextDraw(
then = {
onAnimationFinished(true /* instantDismiss */) onAnimationFinished(true /* instantDismiss */)
onDialogDismissed(this@AnimatedDialog) onDialogDismissed(this@AnimatedDialog)
}) }
)
} }
) )
} }
@@ -740,7 +771,8 @@ private class AnimatedDialog(
endViewController.launchContainer = decorView endViewController.launchContainer = decorView
val endState = endViewController.createAnimatorState() val endState = endViewController.createAnimatorState()
val controller = object : LaunchAnimator.Controller { val controller =
object : LaunchAnimator.Controller {
override var launchContainer: ViewGroup override var launchContainer: ViewGroup
get() = startViewController.launchContainer get() = startViewController.launchContainer
set(value) { set(value) {
@@ -753,8 +785,8 @@ private class AnimatedDialog(
} }
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
// During launch, onLaunchAnimationStart will be used to remove the temporary touch // During launch, onLaunchAnimationStart will be used to remove the temporary
// surface ghost so it is important to call this before calling // touch surface ghost so it is important to call this before calling
// onLaunchAnimationStart on the controller (which will create its own ghost). // onLaunchAnimationStart on the controller (which will create its own ghost).
onLaunchAnimationStart() onLaunchAnimationStart()
@@ -780,12 +812,12 @@ private class AnimatedDialog(
state.visible = !state.visible state.visible = !state.visible
endViewController.onLaunchAnimationProgress(state, progress, linearProgress) endViewController.onLaunchAnimationProgress(state, progress, linearProgress)
// If the dialog content is complex, its dimension might change during the launch // If the dialog content is complex, its dimension might change during the
// animation. The animation end position might also change during the exit // launch animation. The animation end position might also change during the
// animation, for instance when locking the phone when the dialog is open. Therefore // exit animation, for instance when locking the phone when the dialog is open.
// we update the end state to the new position/size. Usually the dialog dimension or // Therefore we update the end state to the new position/size. Usually the
// position will change in the early frames, so changing the end state shouldn't // dialog dimension or position will change in the early frames, so changing the
// really be noticeable. // end state shouldn't really be noticeable.
endViewController.fillGhostedViewState(endState) endViewController.fillGhostedViewState(endState)
} }
} }
@@ -866,15 +898,18 @@ private class AnimatedDialog(
currentAnimator?.cancel() currentAnimator?.cancel()
currentAnimator = null currentAnimator = null
val animator = ValueAnimator.ofFloat(0f, 1f).apply { val animator =
ValueAnimator.ofFloat(0f, 1f).apply {
duration = ANIMATION_DURATION duration = ANIMATION_DURATION
interpolator = Interpolators.STANDARD interpolator = Interpolators.STANDARD
addListener(object : AnimatorListenerAdapter() { addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
currentAnimator = null currentAnimator = null
} }
}) }
)
addUpdateListener { animatedValue -> addUpdateListener { animatedValue ->
val progress = animatedValue.animatedFraction val progress = animatedValue.animatedFraction

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,7 +159,8 @@ open class GhostedViewLaunchAnimatorController(
} }
override fun createAnimatorState(): LaunchAnimator.State { override fun createAnimatorState(): LaunchAnimator.State {
val state = LaunchAnimator.State( val state =
LaunchAnimator.State(
topCornerRadius = getCurrentTopCornerRadius(), topCornerRadius = getCurrentTopCornerRadius(),
bottomCornerRadius = getCurrentBottomCornerRadius() bottomCornerRadius = getCurrentBottomCornerRadius()
) )
@@ -255,7 +254,8 @@ open class GhostedViewLaunchAnimatorController(
launchContainer.getLocationOnScreen(launchContainerLocation) launchContainer.getLocationOnScreen(launchContainerLocation)
ghostViewMatrix.postScale( ghostViewMatrix.postScale(
scale, scale, scale,
scale,
ghostedViewState.centerX - launchContainerLocation[0], ghostedViewState.centerX - launchContainerLocation[0],
ghostedViewState.centerY - launchContainerLocation[1] ghostedViewState.centerY - launchContainerLocation[1]
) )

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)
@@ -77,8 +74,8 @@ class LaunchAnimator(
* This will be used to: * This will be used to:
* - Get the associated [Context]. * - Get the associated [Context].
* - Compute whether we are expanding fully above the launch container. * - Compute whether we are expanding fully above the launch container.
* - Get to overlay to which we initially put the window background layer, until the * - Get to overlay to which we initially put the window background layer, until the opening
* opening window is made visible (see [openingWindowSyncView]). * window is made visible (see [openingWindowSyncView]).
* *
* This container can be changed to force this [Controller] to animate the expanding view * This container can be changed to force this [Controller] to animate the expanding view
* inside a different location, for instance to ensure correct layering during the * inside a different location, for instance to ensure correct layering during the
@@ -132,7 +129,6 @@ class LaunchAnimator(
var bottom: Int = 0, var bottom: Int = 0,
var left: Int = 0, var left: Int = 0,
var right: Int = 0, var right: Int = 0,
var topCornerRadius: Float = 0f, var topCornerRadius: Float = 0f,
var bottomCornerRadius: Float = 0f var bottomCornerRadius: Float = 0f
) { ) {
@@ -202,18 +198,20 @@ class LaunchAnimator(
) )
/** /**
* Start a launch animation controlled by [controller] towards [endState]. An intermediary * Start a launch animation controlled by [controller] towards [endState]. An intermediary layer
* layer with [windowBackgroundColor] will fade in then fade out above the expanding view, and * with [windowBackgroundColor] will fade in then (optionally) fade out above the expanding
* should be the same background color as the opening (or closing) window. If [drawHole] is * view, and should be the same background color as the opening (or closing) window.
* true, then this intermediary layer will be drawn with SRC blending mode while it fades out.
* *
* TODO(b/184121838): Remove [drawHole] and instead make the StatusBar draw this hole instead. * If [fadeOutWindowBackgroundLayer] is true, then this intermediary layer will fade out during
* the second half of the animation, and will have SRC blending mode (ultimately punching a hole
* in the [launch container][Controller.launchContainer]) iff [drawHole] is true.
*/ */
fun startAnimation( fun startAnimation(
controller: Controller, controller: Controller,
endState: State, endState: State,
windowBackgroundColor: Int, windowBackgroundColor: Int,
drawHole: Boolean = false fadeOutWindowBackgroundLayer: Boolean = true,
drawHole: Boolean = false,
): Animation { ): Animation {
val state = controller.createAnimatorState() val state = controller.createAnimatorState()
@@ -238,8 +236,12 @@ class LaunchAnimator(
val endBottomCornerRadius = endState.bottomCornerRadius val endBottomCornerRadius = endState.bottomCornerRadius
fun maybeUpdateEndState() { fun maybeUpdateEndState() {
if (endTop != endState.top || endBottom != endState.bottom || if (
endLeft != endState.left || endRight != endState.right) { endTop != endState.top ||
endBottom != endState.bottom ||
endLeft != endState.left ||
endRight != endState.right
) {
endTop = endState.top endTop = endState.top
endBottom = endState.bottom endBottom = endState.bottom
endLeft = endState.left endLeft = endState.left
@@ -256,7 +258,8 @@ class LaunchAnimator(
// color, which is usually the same color of the app background. We first fade in this layer // color, which is usually the same color of the app background. We first fade in this layer
// to hide the expanding view, then we fade it out with SRC mode to draw a hole in the // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
// launch container and reveal the opening window. // launch container and reveal the opening window.
val windowBackgroundLayer = GradientDrawable().apply { val windowBackgroundLayer =
GradientDrawable().apply {
setColor(windowBackgroundColor) setColor(windowBackgroundColor)
alpha = 0 alpha = 0
} }
@@ -270,14 +273,16 @@ class LaunchAnimator(
// [Controller.openingWindowSyncView] once the opening app window starts to be visible. // [Controller.openingWindowSyncView] once the opening app window starts to be visible.
val openingWindowSyncView = controller.openingWindowSyncView val openingWindowSyncView = controller.openingWindowSyncView
val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
val moveBackgroundLayerWhenAppIsVisible = openingWindowSyncView != null && val moveBackgroundLayerWhenAppIsVisible =
openingWindowSyncView != null &&
openingWindowSyncView.viewRootImpl != controller.launchContainer.viewRootImpl openingWindowSyncView.viewRootImpl != controller.launchContainer.viewRootImpl
val launchContainerOverlay = launchContainer.overlay val launchContainerOverlay = launchContainer.overlay
var cancelled = false var cancelled = false
var movedBackgroundLayer = false var movedBackgroundLayer = false
animator.addListener(object : AnimatorListenerAdapter() { animator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?, isReverse: Boolean) { override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Animation started") Log.d(TAG, "Animation started")
@@ -301,7 +306,8 @@ class LaunchAnimator(
openingWindowSyncViewOverlay?.remove(windowBackgroundLayer) openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
} }
} }
}) }
)
animator.addUpdateListener { animation -> animator.addUpdateListener { animation ->
if (cancelled) { if (cancelled) {
@@ -333,7 +339,8 @@ class LaunchAnimator(
// The expanding view can/should be hidden once it is completely covered by the opening // The expanding view can/should be hidden once it is completely covered by the opening
// window. // window.
state.visible = getProgress( state.visible =
getProgress(
timings, timings,
linearProgress, linearProgress,
timings.contentBeforeFadeOutDelay, timings.contentBeforeFadeOutDelay,
@@ -352,7 +359,8 @@ class LaunchAnimator(
ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {}) ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {})
} }
val container = if (movedBackgroundLayer) { val container =
if (movedBackgroundLayer) {
openingWindowSyncView!! openingWindowSyncView!!
} else { } else {
controller.launchContainer controller.launchContainer
@@ -363,6 +371,7 @@ class LaunchAnimator(
state, state,
linearProgress, linearProgress,
container, container,
fadeOutWindowBackgroundLayer,
drawHole drawHole
) )
controller.onLaunchAnimationProgress(state, progress, linearProgress) controller.onLaunchAnimationProgress(state, progress, linearProgress)
@@ -391,6 +400,7 @@ class LaunchAnimator(
state: State, state: State,
linearProgress: Float, linearProgress: Float,
launchContainer: View, launchContainer: View,
fadeOutWindowBackgroundLayer: Boolean,
drawHole: Boolean drawHole: Boolean
) { ) {
// Update position. // Update position.
@@ -415,7 +425,8 @@ class LaunchAnimator(
// We first fade in the background layer to hide the expanding view, then fade it out // We first fade in the background layer to hide the expanding view, then fade it out
// with SRC mode to draw a hole punch in the status bar and reveal the opening window. // with SRC mode to draw a hole punch in the status bar and reveal the opening window.
val fadeInProgress = getProgress( val fadeInProgress =
getProgress(
timings, timings,
linearProgress, linearProgress,
timings.contentBeforeFadeOutDelay, timings.contentBeforeFadeOutDelay,
@@ -425,8 +436,9 @@ class LaunchAnimator(
val alpha = val alpha =
interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress) interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress)
drawable.alpha = (alpha * 0xFF).roundToInt() drawable.alpha = (alpha * 0xFF).roundToInt()
} else { } else if (fadeOutWindowBackgroundLayer) {
val fadeOutProgress = getProgress( val fadeOutProgress =
getProgress(
timings, timings,
linearProgress, linearProgress,
timings.contentAfterFadeInDelay, timings.contentAfterFadeInDelay,
@@ -439,6 +451,8 @@ class LaunchAnimator(
if (drawHole) { if (drawHole) {
drawable.setXfermode(SRC_MODE) drawable.setXfermode(SRC_MODE)
} }
} else {
drawable.alpha = 0xFF
} }
} }
} }

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,7 +150,8 @@ class RemoteTransitionAdapter {
info: TransitionInfo, info: TransitionInfo,
t: SurfaceControl.Transaction t: SurfaceControl.Transaction
): RemoteAnimationTarget { ): RemoteAnimationTarget {
val target = RemoteAnimationTarget( val target =
RemoteAnimationTarget(
/* taskId */ if (change.taskInfo != null) change.taskInfo!!.taskId else -1, /* taskId */ if (change.taskInfo != null) change.taskInfo!!.taskId else -1,
/* mode */ newModeToLegacyMode(change.mode), /* mode */ newModeToLegacyMode(change.mode),
/* leash */ createLeash(info, change, order, t), /* leash */ createLeash(info, change, order, t),
@@ -151,15 +164,16 @@ class RemoteTransitionAdapter {
/* localBounds */ rectOffsetTo(change.endAbsBounds, change.endRelOffset), /* localBounds */ rectOffsetTo(change.endAbsBounds, change.endRelOffset),
/* screenSpaceBounds */ Rect(change.endAbsBounds), /* screenSpaceBounds */ Rect(change.endAbsBounds),
/* windowConfig */ if (change.taskInfo != null) /* windowConfig */ if (change.taskInfo != null)
change.taskInfo!!.configuration.windowConfiguration else change.taskInfo!!.configuration.windowConfiguration
WindowConfiguration(), else WindowConfiguration(),
/* isNotInRecents */ if (change.taskInfo != null) /* isNotInRecents */ if (change.taskInfo != null) !change.taskInfo!!.isRunning
!change.taskInfo!!.isRunning else true, else true,
/* startLeash */ null, /* startLeash */ null,
/* startBounds */ Rect(change.startAbsBounds), /* startBounds */ Rect(change.startAbsBounds),
/* taskInfo */ change.taskInfo, /* taskInfo */ change.taskInfo,
/* allowEnterPip */ change.allowEnterPip, /* allowEnterPip */ change.allowEnterPip,
/* windowType */ WindowManager.LayoutParams.INVALID_WINDOW_TYPE) /* windowType */ WindowManager.LayoutParams.INVALID_WINDOW_TYPE
)
target.backgroundColor = change.backgroundColor target.backgroundColor = change.backgroundColor
return target return target
} }
@@ -192,9 +206,7 @@ class RemoteTransitionAdapter {
} }
@JvmStatic @JvmStatic
fun adaptRemoteRunner( fun adaptRemoteRunner(runner: IRemoteAnimationRunner): IRemoteTransition.Stub {
runner: IRemoteAnimationRunner
): IRemoteTransition.Stub {
return object : IRemoteTransition.Stub() { return object : IRemoteTransition.Stub() {
override fun startAnimation( override fun startAnimation(
token: IBinder, token: IBinder,
@@ -218,18 +230,24 @@ class RemoteTransitionAdapter {
var displayH = 0f var displayH = 0f
for (i in info.changes.indices.reversed()) { for (i in info.changes.indices.reversed()) {
val change = info.changes[i] val change = info.changes[i]
if (change.taskInfo != null && if (
change.taskInfo!!.activityType change.taskInfo != null &&
== WindowConfiguration.ACTIVITY_TYPE_HOME) { change.taskInfo!!.activityType ==
isReturnToHome = (change.mode == WindowManager.TRANSIT_OPEN || WindowConfiguration.ACTIVITY_TYPE_HOME
) {
isReturnToHome =
(change.mode == WindowManager.TRANSIT_OPEN ||
change.mode == WindowManager.TRANSIT_TO_FRONT) change.mode == WindowManager.TRANSIT_TO_FRONT)
launcherTask = change launcherTask = change
launcherLayer = info.changes.size - i launcherLayer = info.changes.size - i
} else if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) { } else if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) {
wallpaper = change wallpaper = change
} }
if (change.parent == null && change.endRotation >= 0 && if (
change.endRotation != change.startRotation) { change.parent == null &&
change.endRotation >= 0 &&
change.endRotation != change.startRotation
) {
rotateDelta = change.endRotation - change.startRotation rotateDelta = change.endRotation - change.startRotation
displayW = change.endAbsBounds.width().toFloat() displayW = change.endAbsBounds.width().toFloat()
displayH = change.endAbsBounds.height().toFloat() displayH = change.endAbsBounds.height().toFloat()
@@ -240,8 +258,13 @@ class RemoteTransitionAdapter {
val counterLauncher = CounterRotator() val counterLauncher = CounterRotator()
val counterWallpaper = CounterRotator() val counterWallpaper = CounterRotator()
if (launcherTask != null && rotateDelta != 0 && launcherTask.parent != null) { if (launcherTask != null && rotateDelta != 0 && launcherTask.parent != null) {
counterLauncher.setup(t, info.getChange(launcherTask.parent!!)!!.leash, counterLauncher.setup(
rotateDelta, displayW, displayH) t,
info.getChange(launcherTask.parent!!)!!.leash,
rotateDelta,
displayW,
displayH
)
if (counterLauncher.surface != null) { if (counterLauncher.surface != null) {
t.setLayer(counterLauncher.surface!!, launcherLayer) t.setLayer(counterLauncher.surface!!, launcherLayer)
} }
@@ -257,8 +280,10 @@ class RemoteTransitionAdapter {
val mode = info.changes[i].mode val mode = info.changes[i].mode
// Only deal with independent layers // Only deal with independent layers
if (!TransitionInfo.isIndependent(change, info)) continue if (!TransitionInfo.isIndependent(change, info)) continue
if (mode == WindowManager.TRANSIT_CLOSE || if (
mode == WindowManager.TRANSIT_TO_BACK) { mode == WindowManager.TRANSIT_CLOSE ||
mode == WindowManager.TRANSIT_TO_BACK
) {
t.setLayer(leash!!, info.changes.size * 3 - i) t.setLayer(leash!!, info.changes.size * 3 - i)
counterLauncher.addChild(t, leash) counterLauncher.addChild(t, leash)
} }
@@ -273,8 +298,13 @@ class RemoteTransitionAdapter {
counterLauncher.addChild(t, leashMap[launcherTask.leash]) counterLauncher.addChild(t, leashMap[launcherTask.leash])
} }
if (wallpaper != null && rotateDelta != 0 && wallpaper.parent != null) { if (wallpaper != null && rotateDelta != 0 && wallpaper.parent != null) {
counterWallpaper.setup(t, info.getChange(wallpaper.parent!!)!!.leash, counterWallpaper.setup(
rotateDelta, displayW, displayH) t,
info.getChange(wallpaper.parent!!)!!.leash,
rotateDelta,
displayW,
displayH
)
if (counterWallpaper.surface != null) { if (counterWallpaper.surface != null) {
t.setLayer(counterWallpaper.surface!!, -1) t.setLayer(counterWallpaper.surface!!, -1)
counterWallpaper.addChild(t, leashMap[wallpaper.leash]) counterWallpaper.addChild(t, leashMap[wallpaper.leash])
@@ -282,13 +312,14 @@ class RemoteTransitionAdapter {
} }
} }
t.apply() t.apply()
val animationFinishedCallback = object : IRemoteAnimationFinishedCallback { val animationFinishedCallback =
object : IRemoteAnimationFinishedCallback {
override fun onAnimationFinished() { override fun onAnimationFinished() {
val finishTransaction = SurfaceControl.Transaction() val finishTransaction = SurfaceControl.Transaction()
counterLauncher.cleanUp(finishTransaction) counterLauncher.cleanUp(finishTransaction)
counterWallpaper.cleanUp(finishTransaction) counterWallpaper.cleanUp(finishTransaction)
// Release surface references now. This is apparently to free GPU memory // Release surface references now. This is apparently to free GPU
// while doing quick operations (eg. during CTS). // memory while doing quick operations (eg. during CTS).
for (i in info.changes.indices.reversed()) { for (i in info.changes.indices.reversed()) {
info.changes[i].leash.release() info.changes[i].leash.release()
} }
@@ -296,11 +327,17 @@ class RemoteTransitionAdapter {
leashMap.valueAt(i).release() leashMap.valueAt(i).release()
} }
try { try {
finishCallback.onTransitionFinished(null /* wct */, finishCallback.onTransitionFinished(
finishTransaction) null /* wct */,
finishTransaction
)
} catch (e: RemoteException) { } catch (e: RemoteException) {
Log.e("ActivityOptionsCompat", "Failed to call app controlled" + Log.e(
" animation finished callback", e) "ActivityOptionsCompat",
"Failed to call app controlled" +
" animation finished callback",
e
)
} }
} }
@@ -311,8 +348,11 @@ class RemoteTransitionAdapter {
// TODO(bc-unlcok): Pass correct transit type. // TODO(bc-unlcok): Pass correct transit type.
runner.onAnimationStart( runner.onAnimationStart(
WindowManager.TRANSIT_OLD_NONE, WindowManager.TRANSIT_OLD_NONE,
appsCompat, wallpapersCompat, nonAppsCompat, appsCompat,
animationFinishedCallback) wallpapersCompat,
nonAppsCompat,
animationFinishedCallback
)
} }
override fun mergeAnimation( override fun mergeAnimation(
@@ -329,16 +369,12 @@ class RemoteTransitionAdapter {
} }
@JvmStatic @JvmStatic
fun adaptRemoteAnimation( fun adaptRemoteAnimation(adapter: RemoteAnimationAdapter): RemoteTransition {
adapter: RemoteAnimationAdapter
): RemoteTransition {
return RemoteTransition(adaptRemoteRunner(adapter.runner), adapter.callingApplication) return RemoteTransition(adaptRemoteRunner(adapter.runner), adapter.callingApplication)
} }
} }
/** /** Utility class that takes care of counter-rotating surfaces during a transition animation. */
* Utility class that takes care of counter-rotating surfaces during a transition animation.
*/
class CounterRotator { class CounterRotator {
/** Gets the surface with the counter-rotation. */ /** Gets the surface with the counter-rotation. */
var surface: SurfaceControl? = null var surface: SurfaceControl? = null
@@ -358,7 +394,8 @@ class RemoteTransitionAdapter {
parentH: Float parentH: Float
) { ) {
if (rotateDelta == 0) return if (rotateDelta == 0) return
val surface = SurfaceControl.Builder() val surface =
SurfaceControl.Builder()
.setName("Transition Unrotate") .setName("Transition Unrotate")
.setContainerLayer() .setContainerLayer()
.setParent(parent) .setParent(parent)
@@ -378,17 +415,15 @@ class RemoteTransitionAdapter {
t.show(surface) t.show(surface)
} }
/** /** Adds a surface that needs to be counter-rotate. */
* Adds a surface that needs to be counter-rotate.
*/
fun addChild(t: SurfaceControl.Transaction, child: SurfaceControl?) { fun addChild(t: SurfaceControl.Transaction, child: SurfaceControl?) {
if (surface == null) return if (surface == null) return
t.reparent(child!!, surface) t.reparent(child!!, surface)
} }
/** /**
* Clean-up. Since finishTransaction should reset all change leashes, we only need to remove the * Clean-up. Since finishTransaction should reset all change leashes, we only need to remove
* counter rotation surface. * the counter rotation surface.
*/ */
fun cleanUp(finishTransaction: SurfaceControl.Transaction) { fun cleanUp(finishTransaction: SurfaceControl.Transaction) {
if (surface == null) return if (surface == null) return

View File

@@ -41,7 +41,8 @@ class ViewHierarchyAnimator {
private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE
/** The properties used to animate the view bounds. */ /** The properties used to animate the view bounds. */
private val PROPERTIES = mapOf( private val PROPERTIES =
mapOf(
Bound.LEFT to createViewProperty(Bound.LEFT), Bound.LEFT to createViewProperty(Bound.LEFT),
Bound.TOP to createViewProperty(Bound.TOP), Bound.TOP to createViewProperty(Bound.TOP),
Bound.RIGHT to createViewProperty(Bound.RIGHT), Bound.RIGHT to createViewProperty(Bound.RIGHT),
@@ -103,7 +104,8 @@ class ViewHierarchyAnimator {
duration: Long, duration: Long,
ephemeral: Boolean ephemeral: Boolean
): Boolean { ): Boolean {
if (!isVisible( if (
!isVisible(
rootView.visibility, rootView.visibility,
rootView.left, rootView.left,
rootView.top, rootView.top,
@@ -131,11 +133,7 @@ class ViewHierarchyAnimator {
duration: Long, duration: Long,
ephemeral: Boolean ephemeral: Boolean
): View.OnLayoutChangeListener { ): View.OnLayoutChangeListener {
return createListener( return createListener(interpolator, duration, ephemeral)
interpolator,
duration,
ephemeral
)
} }
/** /**
@@ -171,7 +169,8 @@ class ViewHierarchyAnimator {
duration: Long = DEFAULT_DURATION, duration: Long = DEFAULT_DURATION,
includeMargins: Boolean = false includeMargins: Boolean = false
): Boolean { ): Boolean {
if (isVisible( if (
isVisible(
rootView.visibility, rootView.visibility,
rootView.left, rootView.left,
rootView.top, rootView.top,
@@ -182,8 +181,12 @@ class ViewHierarchyAnimator {
return false return false
} }
val listener = createAdditionListener( val listener =
origin, interpolator, duration, ignorePreviousValues = !includeMargins createAdditionListener(
origin,
interpolator,
duration,
ignorePreviousValues = !includeMargins
) )
addListener(rootView, listener, recursive = true) addListener(rootView, listener, recursive = true)
return true return true
@@ -257,7 +260,8 @@ class ViewHierarchyAnimator {
return return
} }
val startValues = processStartValues( val startValues =
processStartValues(
origin, origin,
left, left,
top, top,
@@ -269,7 +273,8 @@ class ViewHierarchyAnimator {
startBottom, startBottom,
ignorePreviousValues ignorePreviousValues
) )
val endValues = mapOf( val endValues =
mapOf(
Bound.LEFT to left, Bound.LEFT to left,
Bound.TOP to top, Bound.TOP to top,
Bound.RIGHT to right, Bound.RIGHT to right,
@@ -313,7 +318,8 @@ class ViewHierarchyAnimator {
interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR, interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR,
duration: Long = DEFAULT_DURATION duration: Long = DEFAULT_DURATION
): Boolean { ): Boolean {
if (!isVisible( if (
!isVisible(
rootView.visibility, rootView.visibility,
rootView.left, rootView.left,
rootView.top, rootView.top,
@@ -327,11 +333,7 @@ class ViewHierarchyAnimator {
val parent = rootView.parent as ViewGroup val parent = rootView.parent as ViewGroup
// Ensure that rootView's siblings animate nicely around the removal. // Ensure that rootView's siblings animate nicely around the removal.
val listener = createUpdateListener( val listener = createUpdateListener(interpolator, duration, ephemeral = true)
interpolator,
duration,
ephemeral = true
)
for (i in 0 until parent.childCount) { for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i) val child = parent.getChildAt(i)
if (child == rootView) continue if (child == rootView) continue
@@ -346,13 +348,15 @@ class ViewHierarchyAnimator {
// them manually during the animation. // them manually during the animation.
parent.overlay.add(rootView) parent.overlay.add(rootView)
val startValues = mapOf( val startValues =
mapOf(
Bound.LEFT to rootView.left, Bound.LEFT to rootView.left,
Bound.TOP to rootView.top, Bound.TOP to rootView.top,
Bound.RIGHT to rootView.right, Bound.RIGHT to rootView.right,
Bound.BOTTOM to rootView.bottom Bound.BOTTOM to rootView.bottom
) )
val endValues = processEndValuesForRemoval( val endValues =
processEndValuesForRemoval(
destination, destination,
rootView.left, rootView.left,
rootView.top, rootView.top,
@@ -400,20 +404,24 @@ class ViewHierarchyAnimator {
(animation.animatedValue as Float) * startAlphas[i] (animation.animatedValue as Float) * startAlphas[i]
} }
} }
animator.addListener(object : AnimatorListenerAdapter() { animator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
rootView.animate() rootView
.animate()
.alpha(0f) .alpha(0f)
.setInterpolator(Interpolators.ALPHA_OUT) .setInterpolator(Interpolators.ALPHA_OUT)
.setDuration(duration / 2) .setDuration(duration / 2)
.withEndAction { parent.overlay.remove(rootView) } .withEndAction { parent.overlay.remove(rootView) }
.start() .start()
} }
}) }
)
animator.start() animator.start()
} else { } else {
// Fade out the view during the second half of the removal. // Fade out the view during the second half of the removal.
rootView.animate() rootView
.animate()
.alpha(0f) .alpha(0f)
.setInterpolator(Interpolators.ALPHA_OUT) .setInterpolator(Interpolators.ALPHA_OUT)
.setDuration(duration / 2) .setDuration(duration / 2)
@@ -440,13 +448,15 @@ class ViewHierarchyAnimator {
) { ) {
for (i in 0 until rootView.childCount) { for (i in 0 until rootView.childCount) {
val child = rootView.getChildAt(i) val child = rootView.getChildAt(i)
val childStartValues = mapOf( val childStartValues =
mapOf(
Bound.LEFT to child.left, Bound.LEFT to child.left,
Bound.TOP to child.top, Bound.TOP to child.top,
Bound.RIGHT to child.right, Bound.RIGHT to child.right,
Bound.BOTTOM to child.bottom Bound.BOTTOM to child.bottom
) )
val childEndValues = processChildEndValuesForRemoval( val childEndValues =
processChildEndValuesForRemoval(
destination, destination,
child.left, child.left,
child.top, child.top,
@@ -500,6 +510,7 @@ class ViewHierarchyAnimator {
* not newly introduced margins are included. * not newly introduced margins are included.
* *
* Base case * Base case
* ```
* 1) origin=TOP * 1) origin=TOP
* x---------x x---------x x---------x x---------x x---------x * x---------x x---------x x---------x x---------x x---------x
* x---------x | | | | | | * x---------x | | | | | |
@@ -518,11 +529,11 @@ class ViewHierarchyAnimator {
* x -> x---x -> | | -> | | -> | | * x -> x---x -> | | -> | | -> | |
* x-----x x-------x | | * x-----x x-------x | |
* x---------x * x---------x
* * ```
* In case the start and end values differ in the direction of the origin, and * In case the start and end values differ in the direction of the origin, and
* [ignorePreviousValues] is false, the previous values are used and a translation is * [ignorePreviousValues] is false, the previous values are used and a translation is
* included in addition to the view expansion. * included in addition to the view expansion.
* * ```
* origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70) * origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70)
* x * x
* x--x * x--x
@@ -531,6 +542,7 @@ class ViewHierarchyAnimator {
* x----x | | * x----x | |
* | | * | |
* x------x * x------x
* ```
*/ */
private fun processStartValues( private fun processStartValues(
origin: Hotspot?, origin: Hotspot?,
@@ -555,41 +567,53 @@ class ViewHierarchyAnimator {
var bottom = startBottom var bottom = startBottom
if (origin != null) { if (origin != null) {
left = when (origin) { left =
when (origin) {
Hotspot.CENTER -> (newLeft + newRight) / 2 Hotspot.CENTER -> (newLeft + newRight) / 2
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft) Hotspot.BOTTOM_LEFT,
Hotspot.TOP, Hotspot.BOTTOM -> newLeft Hotspot.LEFT,
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max( Hotspot.TOP_LEFT -> min(startLeft, newLeft)
startRight, Hotspot.TOP,
newRight Hotspot.BOTTOM -> newLeft
) Hotspot.TOP_RIGHT,
Hotspot.RIGHT,
Hotspot.BOTTOM_RIGHT -> max(startRight, newRight)
} }
top = when (origin) { top =
when (origin) {
Hotspot.CENTER -> (newTop + newBottom) / 2 Hotspot.CENTER -> (newTop + newBottom) / 2
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop) Hotspot.TOP_LEFT,
Hotspot.LEFT, Hotspot.RIGHT -> newTop Hotspot.TOP,
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max( Hotspot.TOP_RIGHT -> min(startTop, newTop)
startBottom, Hotspot.LEFT,
newBottom Hotspot.RIGHT -> newTop
) Hotspot.BOTTOM_RIGHT,
Hotspot.BOTTOM,
Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom)
} }
right = when (origin) { right =
when (origin) {
Hotspot.CENTER -> (newLeft + newRight) / 2 Hotspot.CENTER -> (newLeft + newRight) / 2
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max( Hotspot.TOP_RIGHT,
startRight, Hotspot.RIGHT,
newRight Hotspot.BOTTOM_RIGHT -> max(startRight, newRight)
) Hotspot.TOP,
Hotspot.TOP, Hotspot.BOTTOM -> newRight Hotspot.BOTTOM -> newRight
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft) Hotspot.BOTTOM_LEFT,
Hotspot.LEFT,
Hotspot.TOP_LEFT -> min(startLeft, newLeft)
} }
bottom = when (origin) { bottom =
when (origin) {
Hotspot.CENTER -> (newTop + newBottom) / 2 Hotspot.CENTER -> (newTop + newBottom) / 2
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max( Hotspot.BOTTOM_RIGHT,
startBottom, Hotspot.BOTTOM,
newBottom Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom)
) Hotspot.LEFT,
Hotspot.LEFT, Hotspot.RIGHT -> newBottom Hotspot.RIGHT -> newBottom
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop) Hotspot.TOP_LEFT,
Hotspot.TOP,
Hotspot.TOP_RIGHT -> min(startTop, newTop)
} }
} }
@@ -606,6 +630,7 @@ class ViewHierarchyAnimator {
* view's starting bounds. * view's starting bounds.
* *
* Examples: * Examples:
* ```
* 1) destination=TOP * 1) destination=TOP
* x---------x x---------x x---------x x---------x x---------x * x---------x x---------x x---------x x---------x x---------x
* | | | | | | x---------x * | | | | | | x---------x
@@ -624,6 +649,7 @@ class ViewHierarchyAnimator {
* | | -> | | -> | | -> x---x -> x * | | -> | | -> | | -> x---x -> x
* | | x-------x x-----x * | | x-------x x-----x
* x---------x * x---------x
* ```
*/ */
private fun processEndValuesForRemoval( private fun processEndValuesForRemoval(
destination: Hotspot, destination: Hotspot,
@@ -632,31 +658,53 @@ class ViewHierarchyAnimator {
right: Int, right: Int,
bottom: Int bottom: Int
): Map<Bound, Int> { ): Map<Bound, Int> {
val endLeft = when (destination) { val endLeft =
when (destination) {
Hotspot.CENTER -> (left + right) / 2 Hotspot.CENTER -> (left + right) / 2
Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP -> Hotspot.BOTTOM,
left Hotspot.BOTTOM_LEFT,
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> right Hotspot.LEFT,
Hotspot.TOP_LEFT,
Hotspot.TOP -> left
Hotspot.TOP_RIGHT,
Hotspot.RIGHT,
Hotspot.BOTTOM_RIGHT -> right
} }
val endTop = when (destination) { val endTop =
when (destination) {
Hotspot.CENTER -> (top + bottom) / 2 Hotspot.CENTER -> (top + bottom) / 2
Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT -> Hotspot.LEFT,
top Hotspot.TOP_LEFT,
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> bottom Hotspot.TOP,
Hotspot.TOP_RIGHT,
Hotspot.RIGHT -> top
Hotspot.BOTTOM_RIGHT,
Hotspot.BOTTOM,
Hotspot.BOTTOM_LEFT -> bottom
} }
val endRight = when (destination) { val endRight =
when (destination) {
Hotspot.CENTER -> (left + right) / 2 Hotspot.CENTER -> (left + right) / 2
Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.TOP,
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM -> Hotspot.TOP_RIGHT,
right Hotspot.RIGHT,
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> left Hotspot.BOTTOM_RIGHT,
Hotspot.BOTTOM -> right
Hotspot.BOTTOM_LEFT,
Hotspot.LEFT,
Hotspot.TOP_LEFT -> left
} }
val endBottom = when (destination) { val endBottom =
when (destination) {
Hotspot.CENTER -> (top + bottom) / 2 Hotspot.CENTER -> (top + bottom) / 2
Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.RIGHT,
Hotspot.BOTTOM_LEFT, Hotspot.LEFT -> Hotspot.BOTTOM_RIGHT,
bottom Hotspot.BOTTOM,
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> top Hotspot.BOTTOM_LEFT,
Hotspot.LEFT -> bottom
Hotspot.TOP_LEFT,
Hotspot.TOP,
Hotspot.TOP_RIGHT -> top
} }
return mapOf( return mapOf(
@@ -675,6 +723,7 @@ class ViewHierarchyAnimator {
* its center is at the [destination]. * its center is at the [destination].
* *
* Examples: * Examples:
* ```
* 1) destination=TOP * 1) destination=TOP
* The child maintains its left and right positions, but is shifted up so that its * The child maintains its left and right positions, but is shifted up so that its
* center is on the parent's end top edge. * center is on the parent's end top edge.
@@ -682,6 +731,7 @@ class ViewHierarchyAnimator {
* The child shifts so that its center is on the parent's end bottom left corner. * The child shifts so that its center is on the parent's end bottom left corner.
* 3) destination=CENTER * 3) destination=CENTER
* The child shifts so that its own center is on the parent's end center. * The child shifts so that its own center is on the parent's end center.
* ```
*/ */
private fun processChildEndValuesForRemoval( private fun processChildEndValuesForRemoval(
destination: Hotspot, destination: Hotspot,
@@ -695,31 +745,53 @@ class ViewHierarchyAnimator {
val halfWidth = (right - left) / 2 val halfWidth = (right - left) / 2
val halfHeight = (bottom - top) / 2 val halfHeight = (bottom - top) / 2
val endLeft = when (destination) { val endLeft =
when (destination) {
Hotspot.CENTER -> (parentWidth / 2) - halfWidth Hotspot.CENTER -> (parentWidth / 2) - halfWidth
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> -halfWidth Hotspot.BOTTOM_LEFT,
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth Hotspot.LEFT,
Hotspot.TOP, Hotspot.BOTTOM -> left Hotspot.TOP_LEFT -> -halfWidth
Hotspot.TOP_RIGHT,
Hotspot.RIGHT,
Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth
Hotspot.TOP,
Hotspot.BOTTOM -> left
} }
val endTop = when (destination) { val endTop =
when (destination) {
Hotspot.CENTER -> (parentHeight / 2) - halfHeight Hotspot.CENTER -> (parentHeight / 2) - halfHeight
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> -halfHeight Hotspot.TOP_LEFT,
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> Hotspot.TOP,
parentHeight - halfHeight Hotspot.TOP_RIGHT -> -halfHeight
Hotspot.LEFT, Hotspot.RIGHT -> top Hotspot.BOTTOM_RIGHT,
Hotspot.BOTTOM,
Hotspot.BOTTOM_LEFT -> parentHeight - halfHeight
Hotspot.LEFT,
Hotspot.RIGHT -> top
} }
val endRight = when (destination) { val endRight =
when (destination) {
Hotspot.CENTER -> (parentWidth / 2) + halfWidth Hotspot.CENTER -> (parentWidth / 2) + halfWidth
Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth Hotspot.TOP_RIGHT,
Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> halfWidth Hotspot.RIGHT,
Hotspot.TOP, Hotspot.BOTTOM -> right Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth
Hotspot.BOTTOM_LEFT,
Hotspot.LEFT,
Hotspot.TOP_LEFT -> halfWidth
Hotspot.TOP,
Hotspot.BOTTOM -> right
} }
val endBottom = when (destination) { val endBottom =
when (destination) {
Hotspot.CENTER -> (parentHeight / 2) + halfHeight Hotspot.CENTER -> (parentHeight / 2) + halfHeight
Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> Hotspot.BOTTOM_RIGHT,
parentHeight + halfHeight Hotspot.BOTTOM,
Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> halfHeight Hotspot.BOTTOM_LEFT -> parentHeight + halfHeight
Hotspot.LEFT, Hotspot.RIGHT -> bottom Hotspot.TOP_LEFT,
Hotspot.TOP,
Hotspot.TOP_RIGHT -> halfHeight
Hotspot.LEFT,
Hotspot.RIGHT -> bottom
} }
return mapOf( return mapOf(
@@ -790,7 +862,8 @@ class ViewHierarchyAnimator {
duration: Long, duration: Long,
ephemeral: Boolean ephemeral: Boolean
) { ) {
val propertyValuesHolders = buildList { val propertyValuesHolders =
buildList {
bounds.forEach { bound -> bounds.forEach { bound ->
add( add(
PropertyValuesHolder.ofInt( PropertyValuesHolder.ofInt(
@@ -800,26 +873,29 @@ class ViewHierarchyAnimator {
) )
) )
} }
}.toTypedArray() }
.toTypedArray()
(view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel() (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()
val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders) val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders)
animator.interpolator = interpolator animator.interpolator = interpolator
animator.duration = duration animator.duration = duration
animator.addListener(object : AnimatorListenerAdapter() { animator.addListener(
object : AnimatorListenerAdapter() {
var cancelled = false var cancelled = false
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
view.setTag(R.id.tag_animator, null /* tag */) view.setTag(R.id.tag_animator, null /* tag */)
bounds.forEach { view.setTag(it.overrideTag, null /* tag */) } bounds.forEach { view.setTag(it.overrideTag, null /* tag */) }
// When an animation is cancelled, a new one might be taking over. We shouldn't // When an animation is cancelled, a new one might be taking over. We
// unregister the listener yet. // shouldn't unregister the listener yet.
if (ephemeral && !cancelled) { if (ephemeral && !cancelled) {
// The duration is the same for the whole hierarchy, so it's safe to remove // The duration is the same for the whole hierarchy, so it's safe to
// the listener recursively. We do this because some descendant views might // remove the listener recursively. We do this because some descendant
// not change bounds, and therefore not animate and leak the listener. // views might not change bounds, and therefore not animate and leak the
// listener.
recursivelyRemoveListener(view) recursivelyRemoveListener(view)
} }
} }
@@ -827,7 +903,8 @@ class ViewHierarchyAnimator {
override fun onAnimationCancel(animation: Animator?) { override fun onAnimationCancel(animation: Animator?) {
cancelled = true cancelled = true
} }
}) }
)
bounds.forEach { bound -> setBound(view, bound, startValues.getValue(bound)) } bounds.forEach { bound -> setBound(view, bound, startValues.getValue(bound)) }
@@ -838,7 +915,15 @@ class ViewHierarchyAnimator {
/** An enum used to determine the origin of addition animations. */ /** An enum used to determine the origin of addition animations. */
enum class Hotspot { enum class Hotspot {
CENTER, LEFT, TOP_LEFT, TOP, TOP_RIGHT, RIGHT, BOTTOM_RIGHT, BOTTOM, BOTTOM_LEFT CENTER,
LEFT,
TOP_LEFT,
TOP,
TOP_RIGHT,
RIGHT,
BOTTOM_RIGHT,
BOTTOM,
BOTTOM_LEFT
} }
private enum class Bound(val label: String, val overrideTag: Int) { private enum class Bound(val label: String, val overrideTag: Int) {

View File

@@ -11,25 +11,26 @@ object ViewRootSync {
/** /**
* Synchronize the next draw between the view roots of [view] and [otherView], then run [then]. * Synchronize the next draw between the view roots of [view] and [otherView], then run [then].
* *
* Note that in some cases, the synchronization might not be possible (e.g. WM consumed the * Note that in some cases, the synchronization might not be possible (e.g. WM consumed the next
* next transactions) or disabled (temporarily, on low ram devices). In this case, [then] will * transactions) or disabled (temporarily, on low ram devices). In this case, [then] will be
* be called without synchronizing. * called without synchronizing.
*/ */
fun synchronizeNextDraw( fun synchronizeNextDraw(view: View, otherView: View, then: () -> Unit) {
view: View, if (
otherView: View, !view.isAttachedToWindow ||
then: () -> Unit view.viewRootImpl == null ||
!otherView.isAttachedToWindow ||
otherView.viewRootImpl == null ||
view.viewRootImpl == otherView.viewRootImpl
) { ) {
if (!view.isAttachedToWindow || view.viewRootImpl == null ||
!otherView.isAttachedToWindow || otherView.viewRootImpl == null ||
view.viewRootImpl == otherView.viewRootImpl) {
// No need to synchronize if either the touch surface or dialog view is not attached // No need to synchronize if either the touch surface or dialog view is not attached
// to a window. // to a window.
then() then()
return return
} }
surfaceSyncer = SurfaceSyncer().apply { surfaceSyncer =
SurfaceSyncer().apply {
val syncId = setupSync(Runnable { then() }) val syncId = setupSync(Runnable { then() })
addToSync(syncId, view) addToSync(syncId, view)
addToSync(syncId, otherView) addToSync(syncId, otherView)
@@ -37,9 +38,7 @@ object ViewRootSync {
} }
} }
/** /** A Java-friendly API for [synchronizeNextDraw]. */
* A Java-friendly API for [synchronizeNextDraw].
*/
@JvmStatic @JvmStatic
fun synchronizeNextDraw(view: View, otherView: View, then: Runnable) { fun synchronizeNextDraw(view: View, otherView: View, then: Runnable) {
synchronizeNextDraw(view, otherView, then::run) synchronizeNextDraw(view, otherView, then::run)