blob: 0c3c3cc0e04b9593a2f348fb887221e55dbb34dc [file] [log] [blame]
package com.android.systemui.animation
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.app.ActivityManager
import android.app.PendingIntent
import android.content.Context
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.AnimationUtils
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(context: Context) {
companion object {
const val ANIMATION_DURATION = 500L
const val ANIMATION_DURATION_FADE_OUT_CONTENT = 183L
const val ANIMATION_DURATION_FADE_IN_WINDOW = 217L
const val ANIMATION_DELAY_FADE_IN_WINDOW = 167L
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
private val CONTENT_FADE_OUT_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f)
private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 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
)
}
}
/** The interpolator used for the width, height, Y position and corner radius. */
private val animationInterpolator = AnimationUtils.loadInterpolator(context,
R.interpolator.launch_animation_interpolator_y)
/** The interpolator used for the X position. */
private val animationInterpolatorX = AnimationUtils.loadInterpolator(context,
R.interpolator.launch_animation_interpolator_x)
/**
* 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) }
}
/** Create a new animation [Runner] controlled by [controller]. */
@VisibleForTesting
fun createRunner(controller: Controller): Runner = Runner(controller)
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 startBottom = bottom
private val startLeft = left
private val startRight = right
private val startWidth = width
private val startHeight = height
val startCenterX = centerX
val startCenterY = centerY
val width: Int
get() = right - left
val height: Int
get() = bottom - top
open val topChange: Int
get() = top - startTop
open val bottomChange: Int
get() = bottom - startBottom
val leftChange: Int
get() = left - startLeft
val rightChange: Int
get() = right - startRight
val widthRatio: Float
get() = width.toFloat() / startWidth
val heightRatio: Float
get() = height.toFloat() / startHeight
val centerX: Float
get() = left + width / 2f
val centerY: Float
get() = top + height / 2f
}
@VisibleForTesting
inner 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 startXCenter = (startLeft + startRight) / 2f
val startWidth = startRight - startLeft
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
val endXCenter = (endLeft + endRight) / 2f
val endWidth = endRight - endLeft
// 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 = Interpolators.LINEAR
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 = animationInterpolator.getInterpolation(linearProgress)
val xProgress = animationInterpolatorX.getInterpolation(linearProgress)
val xCenter = MathUtils.lerp(startXCenter, endXCenter, xProgress)
val halfWidth = lerp(startWidth, endWidth, progress) / 2
state.top = lerp(startTop, endTop, progress).roundToInt()
state.bottom = lerp(startBottom, endBottom, progress).roundToInt()
state.left = (xCenter - halfWidth).roundToInt()
state.right = (xCenter + halfWidth).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 - CONTENT_FADE_OUT_INTERPOLATOR.getInterpolation(contentAlphaProgress)
val backgroundAlphaProgress = getProgress(linearProgress,
ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW)
state.backgroundAlpha =
1 - WINDOW_FADE_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)
}
}
}