Extract ActivityLaunchAnimator into a reusable class.
This CL removes all logic specific to notifications in
ActivityLaunchAnimator, so that it can be reused for other launch
animations.
I would suggest to look at ag/14057092 in parallel to make more sense of
the abstractions added in this CL.
For the sake of not making this CL even bigger than it already is, a few
things still need to be done in follow-up CLs:
- Show the status bar icons at the right time, instead of at the end of
the animation, when using the StatusBarLaunchAnimatorController.
- Move the animation/ package outside of the SystemUIPluginLib library
and instead have it in its own reusable library.
- Replace the animation durations and interpolator to the latest
designs.
- Improve split screen by retrieving the final window bounds and
prevent the clipping of the window during the animation.
- Handle animations in the lock screen.
For review, I would recommend to review in order:
1. ActivityLaunchAnimator.kt
2. StatusBarLaunchAnimatorController.kt
3. NotificationLaunchAnimatorController.kt
4. GhostedViewLaunchAnimatorController.kt
5. Everything else.
Bug: 184121838
Bug: 181654098
Test: Tap a notification when the shade is open and unlocked.
Change-Id: If4c3c64fcd153bb8e89111f56332013ca6dff156
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index d6204db..b3aba22 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -27,6 +27,7 @@
srcs: [
"src/**/*.java",
+ "src/**/*.kt",
"bcsmartspace/src/**/*.java",
],
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
index 25a3fa2..e003b2e 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
@@ -19,6 +19,7 @@
import android.content.Intent;
import android.view.View;
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator;
import com.android.systemui.plugins.annotations.ProvidesInterface;
/**
@@ -44,7 +45,15 @@
* specifies an associated view that should be used for the activity launch animation.
*/
void startPendingIntentDismissingKeyguard(PendingIntent intent,
- Runnable intentSentUiThreadCallback, View associatedView);
+ Runnable intentSentUiThreadCallback, @Nullable View associatedView);
+
+ /**
+ * Similar to {@link #startPendingIntentDismissingKeyguard(PendingIntent, Runnable)}, but also
+ * specifies an animation controller that should be used for the activity launch animation.
+ */
+ void startPendingIntentDismissingKeyguard(PendingIntent intent,
+ Runnable intentSentUiThreadCallback,
+ @Nullable ActivityLaunchAnimator.Controller animationController);
/**
* The intent flag can be specified in startActivity().
@@ -55,6 +64,14 @@
void startActivity(Intent intent, boolean dismissShade, Callback callback);
void postStartActivityDismissingKeyguard(Intent intent, int delay);
void postStartActivityDismissingKeyguard(PendingIntent intent);
+
+ /**
+ * Similar to {@link #postStartActivityDismissingKeyguard(PendingIntent)}, but also specifies an
+ * animation controller that should be used for the activity launch animation.
+ */
+ void postStartActivityDismissingKeyguard(PendingIntent intent,
+ @Nullable ActivityLaunchAnimator.Controller animationController);
+
void postQSRunnableDismissingKeyguard(Runnable runnable);
void dismissKeyguardThenExecute(OnDismissAction action, @Nullable Runnable cancel,
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/ActivityLaunchAnimator.kt
new file mode 100644
index 0000000..e8bdb67
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/ActivityLaunchAnimator.kt
@@ -0,0 +1,479 @@
+package com.android.systemui.plugins.animation
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.app.ActivityManager
+import android.app.PendingIntent
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.os.RemoteException
+import android.util.MathUtils
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationAdapter
+import android.view.RemoteAnimationTarget
+import android.view.SyncRtSurfaceTransactionApplier
+import android.view.View
+import android.view.WindowManager
+import android.view.animation.LinearInterpolator
+import android.view.animation.PathInterpolator
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.policy.ScreenDecorationsUtils
+import kotlin.math.roundToInt
+
+/**
+ * A class that allows activities to be started in a seamless way from a view that is transforming
+ * nicely into the starting window.
+ */
+class ActivityLaunchAnimator {
+ companion object {
+ const val ANIMATION_DURATION = 400L
+ const val ANIMATION_DURATION_FADE_OUT_CONTENT = 67L
+ const val ANIMATION_DURATION_FADE_IN_WINDOW = 200L
+ private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
+ private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
+ private const val ANIMATION_DELAY_NAV_FADE_IN =
+ ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN
+ private const val LAUNCH_TIMEOUT = 500L
+
+ // TODO(b/184121838): Use android.R.interpolator.fast_out_extra_slow_in instead.
+ // TODO(b/184121838): Move com.android.systemui.Interpolators in an animation library we can
+ // reuse here.
+ private val ANIMATION_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f)
+ private val LINEAR_INTERPOLATOR = LinearInterpolator()
+ private val ALPHA_IN_INTERPOLATOR = PathInterpolator(0.4f, 0f, 1f, 1f)
+ private val ALPHA_OUT_INTERPOLATOR = PathInterpolator(0f, 0f, 0.8f, 1f)
+ private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f)
+ private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
+
+ /**
+ * Given the [linearProgress] of a launch animation, return the linear progress of the
+ * sub-animation starting [delay] ms after the launch animation and that lasts [duration].
+ */
+ @JvmStatic
+ fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float {
+ return MathUtils.constrain(
+ (linearProgress * ANIMATION_DURATION - delay) / duration,
+ 0.0f,
+ 1.0f
+ )
+ }
+ }
+
+ /**
+ * Start an intent and animate the opening window. The intent will be started by running
+ * [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch
+ * result. [controller] is responsible from animating the view from which the intent was started
+ * in [Controller.onLaunchAnimationProgress]. No animation will start if there is no window
+ * opening.
+ *
+ * If [controller] is null, then the intent will be started and no animation will run.
+ *
+ * This method will throw any exception thrown by [intentStarter].
+ */
+ inline fun startIntentWithAnimation(
+ controller: Controller?,
+ intentStarter: (RemoteAnimationAdapter?) -> Int
+ ) {
+ if (controller == null) {
+ intentStarter(null)
+ return
+ }
+
+ val runner = Runner(controller)
+ val animationAdapter = RemoteAnimationAdapter(
+ runner,
+ ANIMATION_DURATION,
+ ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
+ )
+ val launchResult = intentStarter(animationAdapter)
+ val willAnimate = launchResult == ActivityManager.START_TASK_TO_FRONT ||
+ launchResult == ActivityManager.START_SUCCESS
+ runner.context.mainExecutor.execute { controller.onIntentStarted(willAnimate) }
+
+ // If we expect an animation, post a timeout to cancel it in case the remote animation is
+ // never started.
+ if (willAnimate) {
+ runner.postTimeout()
+ }
+ }
+
+ /**
+ * Same as [startIntentWithAnimation] but allows [intentStarter] to throw a
+ * [PendingIntent.CanceledException] which must then be handled by the caller. This is useful
+ * for Java caller starting a [PendingIntent].
+ */
+ @Throws(PendingIntent.CanceledException::class)
+ fun startPendingIntentWithAnimation(
+ controller: Controller?,
+ intentStarter: PendingIntentStarter
+ ) {
+ startIntentWithAnimation(controller) { intentStarter.startPendingIntent(it) }
+ }
+
+ interface PendingIntentStarter {
+ /**
+ * Start a pending intent using the provided [animationAdapter] and return the launch
+ * result.
+ */
+ @Throws(PendingIntent.CanceledException::class)
+ fun startPendingIntent(animationAdapter: RemoteAnimationAdapter?): Int
+ }
+
+ /**
+ * A controller that takes care of applying the animation to an expanding view.
+ *
+ * Note that all callbacks (onXXX methods) are all called on the main thread.
+ */
+ interface Controller {
+ companion object {
+ /**
+ * Return a [Controller] that will animate and expand [view] into the opening window.
+ *
+ * Important: The view must be attached to the window when calling this function and
+ * during the animation.
+ */
+ @JvmStatic
+ fun fromView(view: View): Controller = GhostedViewLaunchAnimatorController(view)
+ }
+
+ /**
+ * Return the root [View] that contains the view that started the intent and will be
+ * animating together with the window.
+ *
+ * This view will be used to:
+ * - Get the associated [Context].
+ * - Compute whether we are expanding fully above the current window.
+ * - Apply surface transactions in sync with RenderThread.
+ */
+ fun getRootView(): View
+
+ /**
+ * Return the [State] of the view that will be animated. We will animate from this state to
+ * the final window state.
+ *
+ * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the
+ * animation.
+ */
+ fun createAnimatorState(): State
+
+ /**
+ * The intent was started. If [willAnimate] is false, nothing else will happen and the
+ * animation will not be started.
+ */
+ fun onIntentStarted(willAnimate: Boolean) {}
+
+ /**
+ * The animation started. This is typically used to initialize any additional resource
+ * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
+ * fully above the [root view][getRootView].
+ */
+ fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
+
+ /** The animation made progress and the expandable view [state] should be updated. */
+ fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
+
+ /**
+ * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was
+ * called previously. This is typically used to clean up the resources initialized when the
+ * animation was started.
+ */
+ fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
+
+ /**
+ * The animation was cancelled remotely. Note that [onLaunchAnimationEnd] will still be
+ * called after this if the animation was already started, i.e. if [onLaunchAnimationStart]
+ * was called before the cancellation.
+ */
+ fun onLaunchAnimationCancelled() {}
+
+ /**
+ * The remote animation was not started within the expected time. It timed out and will
+ * never [start][onLaunchAnimationStart].
+ */
+ fun onLaunchAnimationTimedOut() {}
+
+ /**
+ * The animation was aborted because the opening window was not found. It will never
+ * [start][onLaunchAnimationStart].
+ */
+ fun onLaunchAnimationAborted() {}
+ }
+
+ /** The state of an expandable view during an [ActivityLaunchAnimator] animation. */
+ open class State(
+ /** The position of the view in screen space coordinates. */
+ var top: Int,
+ var bottom: Int,
+ var left: Int,
+ var right: Int,
+
+ var topCornerRadius: Float = 0f,
+ var bottomCornerRadius: Float = 0f,
+
+ var contentAlpha: Float = 1f,
+ var backgroundAlpha: Float = 1f
+ ) {
+ private val startTop = top
+ private val startLeft = left
+ private val startRight = right
+
+ val width: Int
+ get() = right - left
+
+ val height: Int
+ get() = bottom - top
+
+ open val topChange: Int
+ get() = top - startTop
+
+ val leftChange: Int
+ get() = left - startLeft
+
+ val rightChange: Int
+ get() = right - startRight
+ }
+
+ @VisibleForTesting
+ class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
+ private val rootView = controller.getRootView()
+ @PublishedApi internal val context = rootView.context
+ private val transactionApplier = SyncRtSurfaceTransactionApplier(rootView)
+ private var animator: ValueAnimator? = null
+
+ private var windowCrop = Rect()
+ private var timedOut = false
+ private var cancelled = false
+
+ // A timeout to cancel the remote animation if it is not started within X milliseconds after
+ // the intent was started.
+ //
+ // Note that this is important to keep this a Runnable (and not a Kotlin lambda), otherwise
+ // it will be automatically converted when posted and we wouldn't be able to remove it after
+ // posting it.
+ private var onTimeout = Runnable { onAnimationTimedOut() }
+
+ @PublishedApi
+ internal fun postTimeout() {
+ rootView.postDelayed(onTimeout, LAUNCH_TIMEOUT)
+ }
+
+ private fun removeTimeout() {
+ rootView.removeCallbacks(onTimeout)
+ }
+
+ override fun onAnimationStart(
+ @WindowManager.TransitionOldType transit: Int,
+ remoteAnimationTargets: Array<out RemoteAnimationTarget>,
+ remoteAnimationWallpaperTargets: Array<out RemoteAnimationTarget>,
+ remoteAnimationNonAppTargets: Array<out RemoteAnimationTarget>,
+ iRemoteAnimationFinishedCallback: IRemoteAnimationFinishedCallback
+ ) {
+ removeTimeout()
+
+ // The animation was started too late and we already notified the controller that it
+ // timed out.
+ if (timedOut) {
+ invokeCallback(iRemoteAnimationFinishedCallback)
+ return
+ }
+
+ // This should not happen, but let's make sure we don't start the animation if it was
+ // cancelled before and we already notified the controller.
+ if (cancelled) {
+ return
+ }
+
+ context.mainExecutor.execute {
+ startAnimation(remoteAnimationTargets, iRemoteAnimationFinishedCallback)
+ }
+ }
+
+ private fun startAnimation(
+ remoteAnimationTargets: Array<out RemoteAnimationTarget>,
+ iCallback: IRemoteAnimationFinishedCallback
+ ) {
+ val window = remoteAnimationTargets.firstOrNull {
+ it.mode == RemoteAnimationTarget.MODE_OPENING
+ }
+
+ if (window == null) {
+ removeTimeout()
+ invokeCallback(iCallback)
+ controller.onLaunchAnimationAborted()
+ return
+ }
+
+ val navigationBar = remoteAnimationTargets.firstOrNull {
+ it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
+ }
+
+ // Start state.
+ val state = controller.createAnimatorState()
+
+ val startTop = state.top
+ val startBottom = state.bottom
+ val startLeft = state.left
+ val startRight = state.right
+
+ val startTopCornerRadius = state.topCornerRadius
+ val startBottomCornerRadius = state.bottomCornerRadius
+
+ // End state.
+ val windowBounds = window.screenSpaceBounds
+ val endTop = windowBounds.top
+ val endBottom = windowBounds.bottom
+ val endLeft = windowBounds.left
+ val endRight = windowBounds.right
+
+ // TODO(b/184121838): Ensure that we are launching on the same screen.
+ val rootViewLocation = rootView.locationOnScreen
+ val isExpandingFullyAbove = endTop <= rootViewLocation[1] &&
+ endBottom >= rootViewLocation[1] + rootView.height &&
+ endLeft <= rootViewLocation[0] &&
+ endRight >= rootViewLocation[0] + rootView.width
+
+ // TODO(b/184121838): We should somehow get the top and bottom radius of the window.
+ val endRadius = if (isExpandingFullyAbove) {
+ // Most of the time, expanding fully above the root view means expanding in full
+ // screen.
+ ScreenDecorationsUtils.getWindowCornerRadius(context.resources)
+ } else {
+ // This usually means we are in split screen mode, so 2 out of 4 corners will have
+ // a radius of 0.
+ 0f
+ }
+
+ // Update state.
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ this.animator = animator
+ animator.duration = ANIMATION_DURATION
+ animator.interpolator = LINEAR_INTERPOLATOR
+
+ animator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
+ controller.onLaunchAnimationStart(isExpandingFullyAbove)
+ }
+
+ override fun onAnimationEnd(animation: Animator?) {
+ invokeCallback(iCallback)
+ controller.onLaunchAnimationEnd(isExpandingFullyAbove)
+ }
+ })
+
+ animator.addUpdateListener { animation ->
+ if (cancelled) {
+ return@addUpdateListener
+ }
+
+ val linearProgress = animation.animatedFraction
+ val progress = ANIMATION_INTERPOLATOR.getInterpolation(linearProgress)
+
+ state.top = lerp(startTop, endTop, progress).roundToInt()
+ state.bottom = lerp(startBottom, endBottom, progress).roundToInt()
+ state.left = lerp(startLeft, endLeft, progress).roundToInt()
+ state.right = lerp(startRight, endRight, progress).roundToInt()
+
+ state.topCornerRadius = MathUtils.lerp(startTopCornerRadius, endRadius, progress)
+ state.bottomCornerRadius =
+ MathUtils.lerp(startBottomCornerRadius, endRadius, progress)
+
+ val contentAlphaProgress = getProgress(linearProgress, 0,
+ ANIMATION_DURATION_FADE_OUT_CONTENT)
+ state.contentAlpha =
+ 1 - ALPHA_OUT_INTERPOLATOR.getInterpolation(contentAlphaProgress)
+
+ val backgroundAlphaProgress = getProgress(linearProgress,
+ ANIMATION_DURATION_FADE_OUT_CONTENT, ANIMATION_DURATION_FADE_IN_WINDOW)
+ state.backgroundAlpha =
+ 1 - ALPHA_IN_INTERPOLATOR.getInterpolation(backgroundAlphaProgress)
+
+ applyStateToWindow(window, state)
+ navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
+ controller.onLaunchAnimationProgress(state, progress, linearProgress)
+ }
+
+ animator.start()
+ }
+
+ private fun applyStateToWindow(window: RemoteAnimationTarget, state: State) {
+ val m = Matrix()
+ m.postTranslate(0f, (state.top - window.sourceContainerBounds.top).toFloat())
+ windowCrop.set(state.left, 0, state.right, state.height)
+
+ val cornerRadius = minOf(state.topCornerRadius, state.bottomCornerRadius)
+ val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash)
+ .withAlpha(1f)
+ .withMatrix(m)
+ .withWindowCrop(windowCrop)
+ .withLayer(window.prefixOrderIndex)
+ .withCornerRadius(cornerRadius)
+ .withVisibility(true)
+ .build()
+
+ transactionApplier.scheduleApply(params)
+ }
+
+ private fun applyStateToNavigationBar(
+ navigationBar: RemoteAnimationTarget,
+ state: State,
+ linearProgress: Float
+ ) {
+ val fadeInProgress = getProgress(linearProgress, ANIMATION_DELAY_NAV_FADE_IN,
+ ANIMATION_DURATION_NAV_FADE_OUT)
+
+ val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
+ if (fadeInProgress > 0) {
+ val m = Matrix()
+ m.postTranslate(0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat())
+ windowCrop.set(state.left, 0, state.right, state.height)
+ params
+ .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
+ .withMatrix(m)
+ .withWindowCrop(windowCrop)
+ .withVisibility(true)
+ } else {
+ val fadeOutProgress = getProgress(linearProgress, 0,
+ ANIMATION_DURATION_NAV_FADE_OUT)
+ params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
+ }
+
+ transactionApplier.scheduleApply(params.build())
+ }
+
+ private fun onAnimationTimedOut() {
+ if (cancelled) {
+ return
+ }
+
+ timedOut = true
+ controller.onLaunchAnimationTimedOut()
+ }
+
+ override fun onAnimationCancelled() {
+ if (timedOut) {
+ return
+ }
+
+ cancelled = true
+ removeTimeout()
+ context.mainExecutor.execute {
+ animator?.cancel()
+ controller.onLaunchAnimationCancelled()
+ }
+ }
+
+ private fun invokeCallback(iCallback: IRemoteAnimationFinishedCallback) {
+ try {
+ iCallback.onAnimationFinished()
+ } catch (e: RemoteException) {
+ e.printStackTrace()
+ }
+ }
+
+ private fun lerp(start: Int, stop: Int, amount: Float): Float {
+ return MathUtils.lerp(start.toFloat(), stop.toFloat(), amount)
+ }
+ }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/GhostedViewLaunchAnimatorController.kt
new file mode 100644
index 0000000..a237224
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/GhostedViewLaunchAnimatorController.kt
@@ -0,0 +1,196 @@
+package com.android.systemui.plugins.animation
+
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.view.GhostView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+
+/**
+ * A base implementation of [ActivityLaunchAnimator.Controller] which creates a [ghost][GhostView]
+ * of [ghostedView] as well as an expandable background view, which are drawn and animated instead
+ * of the ghosted view.
+ *
+ * Important: [ghostedView] must be attached to the window when calling this function and during the
+ * animation.
+ *
+ * Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView]
+ * whenever possible instead.
+ */
+open class GhostedViewLaunchAnimatorController(
+ /** The view that will be ghosted and from which the background will be extracted. */
+ private val ghostedView: View
+) : ActivityLaunchAnimator.Controller {
+ /** The root view to which we will add the ghost view and expanding background. */
+ private val rootView = ghostedView.rootView as ViewGroup
+ private val rootViewOverlay = rootView.overlay
+
+ /** The ghost view that is drawn and animated instead of the ghosted view. */
+ private var ghostView: View? = null
+
+ /**
+ * The expanding background view that will be added to [rootView] (below [ghostView]) and
+ * animate.
+ */
+ private var backgroundView: FrameLayout? = null
+
+ /**
+ * The drawable wrapping the [ghostedView] background and used as background for
+ * [backgroundView].
+ */
+ private var backgroundDrawable: WrappedDrawable? = null
+ private var startBackgroundAlpha: Int = 0xFF
+
+ /**
+ * Return the background of the [ghostedView]. This background will be used to draw the
+ * background of the background view that is expanding up to the final animation position. This
+ * is called at the start of the animation.
+ *
+ * Note that during the animation, the alpha value value of this background will be set to 0,
+ * then set back to its initial value at the end of the animation.
+ */
+ protected open fun getBackground(): Drawable? = ghostedView.background
+
+ /**
+ * Set the corner radius of [background]. The background is the one that was returned by
+ * [getBackground].
+ */
+ protected open fun setBackgroundCornerRadius(
+ background: Drawable,
+ topCornerRadius: Float,
+ bottomCornerRadius: Float
+ ) {
+ // TODO(b/184121838): Add default support for GradientDrawable and LayerDrawable to make
+ // this work out of the box for common rounded backgrounds.
+ }
+
+ /** Return the current top corner radius of the background. */
+ protected open fun getCurrentTopCornerRadius(): Float = 0f
+
+ /** Return the current bottom corner radius of the background. */
+ protected open fun getCurrentBottomCornerRadius(): Float = 0f
+
+ override fun getRootView(): View {
+ return rootView
+ }
+
+ override fun createAnimatorState(): ActivityLaunchAnimator.State {
+ val location = ghostedView.locationOnScreen
+ return ActivityLaunchAnimator.State(
+ top = location[1],
+ bottom = location[1] + ghostedView.height,
+ left = location[0],
+ right = location[0] + ghostedView.width,
+ topCornerRadius = getCurrentTopCornerRadius(),
+ bottomCornerRadius = getCurrentBottomCornerRadius()
+ )
+ }
+
+ override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ backgroundView = FrameLayout(rootView.context).apply {
+ forceHasOverlappingRendering(true)
+ }
+ rootViewOverlay.add(backgroundView)
+
+ // We wrap the ghosted view background and use it to draw the expandable background. Its
+ // alpha will be set to 0 as soon as we start drawing the expanding background.
+ val drawable = getBackground()
+ startBackgroundAlpha = drawable?.alpha ?: 0xFF
+ backgroundDrawable = WrappedDrawable(drawable)
+ backgroundView?.background = backgroundDrawable
+
+ // Create a ghost of the view that will be moving and fading out. This allows to fade out
+ // the content before fading out the background.
+ ghostView = GhostView.addGhost(ghostedView, rootView).apply {
+ setLayerType(View.LAYER_TYPE_HARDWARE, null)
+ }
+ }
+
+ override fun onLaunchAnimationProgress(
+ state: ActivityLaunchAnimator.State,
+ progress: Float,
+ linearProgress: Float
+ ) {
+ val ghostView = this.ghostView!!
+ ghostView.translationX = (state.leftChange + state.rightChange) / 2.toFloat()
+ ghostView.translationY = state.topChange.toFloat()
+ ghostView.alpha = state.contentAlpha
+
+ val backgroundView = this.backgroundView!!
+ backgroundView.top = state.top
+ backgroundView.bottom = state.bottom
+ backgroundView.left = state.left
+ backgroundView.right = state.right
+
+ val backgroundDrawable = backgroundDrawable!!
+ backgroundDrawable.alpha = (0xFF * state.backgroundAlpha).toInt()
+ backgroundDrawable.wrapped?.let {
+ setBackgroundCornerRadius(it, state.topCornerRadius, state.bottomCornerRadius)
+ }
+ }
+
+ override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha
+
+ GhostView.removeGhost(ghostedView)
+ rootViewOverlay.remove(backgroundView)
+ ghostedView.invalidate()
+ }
+
+ private class WrappedDrawable(val wrapped: Drawable?) : Drawable() {
+ companion object {
+ private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
+ }
+
+ private var currentAlpha = 0xFF
+ private var previousBounds = Rect()
+
+ override fun draw(canvas: Canvas) {
+ val wrapped = this.wrapped ?: return
+
+ wrapped.copyBounds(previousBounds)
+
+ wrapped.alpha = currentAlpha
+ wrapped.bounds = bounds
+ wrapped.setXfermode(SRC_MODE)
+
+ wrapped.draw(canvas)
+
+ // The background view (and therefore this drawable) is drawn before the ghost view, so
+ // the ghosted view background alpha should always be 0 when it is drawn above the
+ // background.
+ wrapped.alpha = 0
+ wrapped.bounds = previousBounds
+ wrapped.setXfermode(null)
+ }
+
+ override fun setAlpha(alpha: Int) {
+ if (alpha != currentAlpha) {
+ currentAlpha = alpha
+ invalidateSelf()
+ }
+ }
+
+ override fun getAlpha() = currentAlpha
+
+ override fun getOpacity(): Int {
+ val wrapped = this.wrapped ?: return PixelFormat.TRANSPARENT
+
+ val previousAlpha = wrapped.alpha
+ wrapped.alpha = currentAlpha
+ val opacity = wrapped.opacity
+ wrapped.alpha = previousAlpha
+ return opacity
+ }
+
+ override fun setColorFilter(filter: ColorFilter?) {
+ wrapped?.colorFilter = filter
+ }
+ }
+}