Move ActivityLaunchAnimator in its own lib. (1/n)
Bug: 184121838
Test: Manual
Change-Id: Ib979fed2f59d9dbf5f0696edb5fcb4956600e6e0
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index b3aba22..f415da8 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -34,6 +34,7 @@
static_libs: [
"PluginCoreLib",
"SystemUI-sensors",
+ "SystemUIAnimationLib",
],
}
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 055fe37..00bea8d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
@@ -19,7 +19,7 @@
import android.content.Intent;
import android.view.View;
-import com.android.systemui.plugins.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.plugins.annotations.ProvidesInterface;
/**
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
deleted file mode 100644
index 5af8dab..0000000
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/ActivityLaunchAnimator.kt
+++ /dev/null
@@ -1,479 +0,0 @@
-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 = 1000L
-
- // 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(0.4f, 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
deleted file mode 100644
index a5494ad..0000000
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/GhostedViewLaunchAnimatorController.kt
+++ /dev/null
@@ -1,329 +0,0 @@
-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.graphics.drawable.GradientDrawable
-import android.graphics.drawable.LayerDrawable
-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
- ) {
- // By default, we rely on WrappedDrawable to set/restore the background radii before/after
- // each draw.
- backgroundDrawable?.setBackgroundRadius(topCornerRadius, bottomCornerRadius)
- }
-
- /** Return the current top corner radius of the background. */
- protected open fun getCurrentTopCornerRadius(): Float {
- val drawable = getBackground() ?: return 0f
- val gradient = findGradientDrawable(drawable) ?: return 0f
-
- // TODO(b/184121838): Support more than symmetric top & bottom radius.
- return gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius
- }
-
- /** Return the current bottom corner radius of the background. */
- protected open fun getCurrentBottomCornerRadius(): Float {
- val drawable = getBackground() ?: return 0f
- val gradient = findGradientDrawable(drawable) ?: return 0f
-
- // TODO(b/184121838): Support more than symmetric top & bottom radius.
- return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
- }
-
- 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(false)
- }
- 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()
- }
-
- companion object {
- private const val CORNER_RADIUS_TOP_INDEX = 0
- private const val CORNER_RADIUS_BOTTOM_INDEX = 4
-
- /**
- * Return the first [GradientDrawable] found in [drawable], or null if none is found. If
- * [drawable] is a [LayerDrawable], this will return the first layer that is a
- * [GradientDrawable].
- */
- private fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
- if (drawable is GradientDrawable) {
- return drawable
- }
-
- if (drawable is LayerDrawable) {
- for (i in 0 until drawable.numberOfLayers) {
- val maybeGradient = drawable.getDrawable(i)
- if (maybeGradient is GradientDrawable) {
- return maybeGradient
- }
- }
- }
-
- return null
- }
- }
-
- 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()
-
- private var cornerRadii = FloatArray(8) { -1f }
- private var previousCornerRadii = FloatArray(8)
-
- override fun draw(canvas: Canvas) {
- val wrapped = this.wrapped ?: return
-
- wrapped.copyBounds(previousBounds)
-
- wrapped.alpha = currentAlpha
- wrapped.bounds = bounds
- setXfermode(wrapped, SRC_MODE)
- applyBackgroundRadii()
-
- 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
- setXfermode(wrapped, null)
- restoreBackgroundRadii()
- }
-
- 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
- }
-
- private fun setXfermode(background: Drawable, mode: PorterDuffXfermode?) {
- if (background !is LayerDrawable) {
- background.setXfermode(mode)
- return
- }
-
- // We set the xfermode on the first layer that is not a mask. Most of the time it will
- // be the "background layer".
- for (i in 0 until background.numberOfLayers) {
- if (background.getId(i) != android.R.id.mask) {
- background.getDrawable(i).setXfermode(mode)
- break
- }
- }
- }
-
- fun setBackgroundRadius(topCornerRadius: Float, bottomCornerRadius: Float) {
- updateRadii(cornerRadii, topCornerRadius, bottomCornerRadius)
- invalidateSelf()
- }
-
- private fun updateRadii(
- radii: FloatArray,
- topCornerRadius: Float,
- bottomCornerRadius: Float
- ) {
- radii[0] = topCornerRadius
- radii[1] = topCornerRadius
- radii[2] = topCornerRadius
- radii[3] = topCornerRadius
-
- radii[4] = bottomCornerRadius
- radii[5] = bottomCornerRadius
- radii[6] = bottomCornerRadius
- radii[7] = bottomCornerRadius
- }
-
- private fun applyBackgroundRadii() {
- if (cornerRadii[0] < 0 || wrapped == null) {
- return
- }
-
- savePreviousBackgroundRadii(wrapped)
- applyBackgroundRadii(wrapped, cornerRadii)
- }
-
- private fun savePreviousBackgroundRadii(background: Drawable) {
- // TODO(b/184121838): This method assumes that all GradientDrawable in background will
- // have the same radius. Should we save/restore the radii for each layer instead?
- val gradient = findGradientDrawable(background) ?: return
-
- // TODO(b/184121838): GradientDrawable#getCornerRadii clones its radii array. Should we
- // try to avoid that?
- val radii = gradient.cornerRadii
- if (radii != null) {
- radii.copyInto(previousCornerRadii)
- } else {
- // Copy the cornerRadius into previousCornerRadii.
- val radius = gradient.cornerRadius
- updateRadii(previousCornerRadii, radius, radius)
- }
- }
-
- private fun applyBackgroundRadii(drawable: Drawable, radii: FloatArray) {
- if (drawable is GradientDrawable) {
- drawable.cornerRadii = radii
- return
- }
-
- if (drawable !is LayerDrawable) {
- return
- }
-
- for (i in 0 until drawable.numberOfLayers) {
- (drawable.getDrawable(i) as? GradientDrawable)?.cornerRadii = radii
- }
- }
-
- private fun restoreBackgroundRadii() {
- if (cornerRadii[0] < 0 || wrapped == null) {
- return
- }
-
- applyBackgroundRadii(wrapped, previousCornerRadii)
- }
- }
-}