Files
Flexera-Launcher/wmshell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
T

639 lines
24 KiB
Kotlin

/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wm.shell.back
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.RectF
import android.os.Handler
import android.os.RemoteException
import android.util.TimeUtils
import android.view.Choreographer
import android.view.Display
import android.view.IRemoteAnimationFinishedCallback
import android.view.IRemoteAnimationRunner
import android.view.RemoteAnimationTarget
import android.view.SurfaceControl
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import android.view.animation.Transformation
import android.window.BackEvent
import android.window.BackEvent.EDGE_LEFT
import android.window.BackEvent.EDGE_RIGHT
import android.window.BackMotionEvent
import android.window.BackNavigationInfo
import android.window.BackProgressAnimator
import android.window.DesktopExperienceFlags
import android.window.IOnBackInvokedCallback
import com.android.internal.dynamicanimation.animation.FloatValueHolder
import com.android.internal.dynamicanimation.animation.SpringAnimation
import com.android.internal.dynamicanimation.animation.SpringForce
import com.android.internal.jank.Cuj
import com.android.internal.policy.ScreenDecorationsUtils
import com.android.internal.policy.SystemBarUtils
import com.android.internal.protolog.ProtoLog
import com.android.window.flags2.Flags.predictiveBackTimestampApi
import com.android.wm.shell.R
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.protolog.ShellProtoLogGroup
import com.android.wm.shell.shared.animation.Interpolators
import com.android.wm.shell.shared.annotations.ShellMainThread
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
abstract class CrossActivityBackAnimation(
private val context: Context,
private val background: BackAnimationBackground,
private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
protected val transaction: SurfaceControl.Transaction,
@ShellMainThread handler: Handler,
) : ShellBackAnimation() {
protected val startClosingRect = RectF()
protected val targetClosingRect = RectF()
protected val currentClosingRect = RectF()
protected val startEnteringRect = RectF()
protected val targetEnteringRect = RectF()
protected val currentEnteringRect = RectF()
protected val backAnimRect = Rect()
private val cropRect = Rect()
private val tempRectF = RectF()
private var cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
private var statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
private val backAnimationRunner =
BackAnimationRunner(
Callback(),
Runner(),
context,
Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY,
handler,
)
private val initialTouchPos = PointF()
private val transformMatrix = Matrix()
private val tmpFloat9 = FloatArray(9)
protected var enteringTarget: RemoteAnimationTarget? = null
protected var closingTarget: RemoteAnimationTarget? = null
private var triggerBack = false
private var finishCallback: IRemoteAnimationFinishedCallback? = null
private val progressAnimator = BackProgressAnimator()
protected val displayBoundsMargin =
context.resources.getDimension(R.dimen.cross_task_back_vertical_margin)
private val gestureInterpolator = Interpolators.BACK_GESTURE
private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator()
private var scrimLayer: SurfaceControl? = null
private var maxScrimAlpha: Float = 0f
private var isLetterboxed = false
private var enteringHasSameLetterbox = false
private var leftLetterboxLayer: SurfaceControl? = null
private var rightLetterboxLayer: SurfaceControl? = null
private var letterboxColor: Int = 0
private val postCommitFlingScale = FloatValueHolder(SPRING_SCALE)
private var lastPostCommitFlingScale = SPRING_SCALE
private val postCommitFlingSpring = SpringForce(SPRING_SCALE)
.setStiffness(SpringForce.STIFFNESS_LOW)
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
private var swipeEdge = EDGE_LEFT
protected var gestureProgress = 0f
private val velocityTracker = ProgressVelocityTracker()
/** Background color to be used during the animation, also see [getBackgroundColor] */
protected var customizedBackgroundColor = 0
/**
* Whether the entering target should be shifted vertically with the user gesture in pre-commit
*/
abstract val allowEnteringYShift: Boolean
/**
* Subclasses must set the [startClosingRect] and [targetClosingRect] to define the movement
* of the closingTarget during pre-commit phase.
*/
abstract fun preparePreCommitClosingRectMovement(@BackEvent.SwipeEdge swipeEdge: Int)
/**
* Subclasses must set the [startEnteringRect] and [targetEnteringRect] to define the movement
* of the enteringTarget during pre-commit phase.
*/
abstract fun preparePreCommitEnteringRectMovement()
/**
* Subclasses must provide a duration (in ms) for the post-commit part of the animation
*/
abstract fun getPostCommitAnimationDuration(): Long
/**
* Returns a base transformation to apply to the entering target during pre-commit. The system
* will apply the default animation on top of it.
*/
protected open fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation? =
null
override fun onConfigurationChanged(newConfiguration: Configuration) {
cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
}
override fun getRunner() = backAnimationRunner
private fun getBackgroundColor(): Int =
when {
customizedBackgroundColor != 0 -> customizedBackgroundColor
isLetterboxed -> letterboxColor
enteringTarget != null -> enteringTarget!!.taskInfo.taskDescription!!.backgroundColor
else -> 0
}
protected open fun startBackAnimation(backMotionEvent: BackMotionEvent) {
if (enteringTarget == null || closingTarget == null) {
ProtoLog.d(
ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
"Entering target or closing target is null."
)
return
}
swipeEdge = backMotionEvent.swipeEdge
triggerBack = backMotionEvent.triggerBack
initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY)
transaction.setAnimationTransaction()
isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.isTopActivityLetterboxed
enteringHasSameLetterbox =
isLetterboxed && closingTarget!!.localBounds.equals(enteringTarget!!.localBounds)
if (isLetterboxed && !enteringHasSameLetterbox) {
// Play animation with letterboxes, if closing and entering target have mismatching
// letterboxes
backAnimRect.set(closingTarget!!.windowConfiguration.bounds)
} else {
// otherwise play animation on localBounds only
backAnimRect.set(closingTarget!!.localBounds)
}
// Offset start rectangle to align task bounds.
backAnimRect.offsetTo(0, 0)
preparePreCommitClosingRectMovement(backMotionEvent.swipeEdge)
preparePreCommitEnteringRectMovement()
background.ensureBackground(
closingTarget!!.windowConfiguration.bounds,
getBackgroundColor(),
transaction,
statusbarHeight,
if (closingTarget!!.windowConfiguration.tasksAreFloating())
closingTarget!!.localBounds else null,
cornerRadius,
closingTarget!!.taskInfo.getDisplayId()
)
ensureScrimLayer()
if (isLetterboxed && enteringHasSameLetterbox) {
// crop left and right letterboxes
cropRect.set(
closingTarget!!.localBounds.left,
0,
closingTarget!!.localBounds.right,
closingTarget!!.windowConfiguration.bounds.height()
)
// and add fake letterbox square surfaces instead
ensureLetterboxes()
} else {
cropRect.set(backAnimRect)
}
applyTransaction()
}
private fun onGestureProgress(backEvent: BackEvent) {
val progress = gestureInterpolator.getInterpolation(backEvent.progress)
gestureProgress = progress
currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
val yOffset = getYOffset(currentClosingRect, backEvent.touchY)
currentClosingRect.offset(0f, yOffset)
applyTransform(closingTarget?.leash, currentClosingRect, 1f)
currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
if (allowEnteringYShift) currentEnteringRect.offset(0f, yOffset)
val enteringTransformation = getPreCommitEnteringBaseTransformation(progress)
applyTransform(
enteringTarget?.leash,
currentEnteringRect,
enteringTransformation?.alpha ?: 1f,
enteringTransformation
)
applyTransaction()
background.customizeStatusBarAppearance(currentClosingRect.top.toInt())
if (predictiveBackTimestampApi()) {
velocityTracker.addPosition(backEvent.frameTimeMillis, progress)
}
}
private fun getYOffset(centeredRect: RectF, touchY: Float): Float {
val screenHeight = backAnimRect.height()
// Base the window movement in the Y axis on the touch movement in the Y axis.
val rawYDelta = touchY - initialTouchPos.y
val yDirection = (if (rawYDelta < 0) -1 else 1)
// limit yDelta interpretation to 1/2 of screen height in either direction
val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f)
val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio)
// limit y-shift so surface never passes 8dp screen margin
val deltaY =
max(0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin) *
interpolatedYRatio *
yDirection
return deltaY
}
protected open fun onGestureCommitted(velocity: Float) {
if (
closingTarget?.leash == null ||
enteringTarget?.leash == null ||
!enteringTarget!!.leash.isValid ||
!closingTarget!!.leash.isValid
) {
finishAnimation()
return
}
// kick off spring animation with the current velocity from the pre-commit phase, this
// affects the scaling of the closing and/or opening activity during post-commit
var startVelocity = if (predictiveBackTimestampApi()) {
// pronounce fling animation more for gestures
val velocityFactor = if (swipeEdge == EDGE_LEFT || swipeEdge == EDGE_RIGHT) 2f else 1f
velocity * SPRING_SCALE * (1f - MAX_SCALE) * velocityFactor
} else {
velocity * SPRING_SCALE
}
if (gestureProgress < 0.1f) {
startVelocity = startVelocity.coerceAtLeast(DEFAULT_FLING_VELOCITY)
}
val flingAnimation = SpringAnimation(postCommitFlingScale, SPRING_SCALE)
.setStartVelocity(-startVelocity.coerceIn(0f, MAX_FLING_VELOCITY))
.setStartValue(SPRING_SCALE)
.setSpring(postCommitFlingSpring)
flingAnimation.start()
// do an animation-frame immediately to prevent idle frame
flingAnimation.doAnimationFrame(
Choreographer.getInstance().lastFrameTimeNanos / TimeUtils.NANOS_PER_MS
)
val valueAnimator =
ValueAnimator.ofFloat(1f, 0f).setDuration(getPostCommitAnimationDuration())
valueAnimator.addUpdateListener { animation: ValueAnimator ->
val progress = animation.animatedFraction
onPostCommitProgress(progress)
if (progress > 1 - BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD) {
background.resetStatusBarCustomization()
}
}
valueAnimator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
background.resetStatusBarCustomization()
finishAnimation()
}
}
)
valueAnimator.start()
}
protected open fun onPostCommitProgress(linearProgress: Float) {
scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) }
}
protected open fun finishAnimation() {
enteringTarget?.let {
if (it.leash != null && it.leash.isValid) {
transaction.setCornerRadius(it.leash, 0f)
if (!triggerBack) transaction.setAlpha(it.leash, 0f)
it.leash.release()
}
enteringTarget = null
}
closingTarget?.leash?.release()
closingTarget = null
background.removeBackground(transaction)
applyTransaction()
transformMatrix.reset()
initialTouchPos.set(0f, 0f)
try {
finishCallback?.onAnimationFinished()
} catch (e: RemoteException) {
e.printStackTrace()
}
finishCallback = null
removeScrimLayer()
removeLetterbox()
isLetterboxed = false
enteringHasSameLetterbox = false
lastPostCommitFlingScale = SPRING_SCALE
gestureProgress = 0f
triggerBack = false
velocityTracker.resetTracking()
}
protected fun applyTransform(
leash: SurfaceControl?,
rect: RectF,
alpha: Float,
baseTransformation: Transformation? = null,
flingMode: FlingMode = FlingMode.NO_FLING
) {
if (leash == null || !leash.isValid) return
tempRectF.set(rect)
if (flingMode != FlingMode.NO_FLING) {
lastPostCommitFlingScale = min(
postCommitFlingScale.value / SPRING_SCALE,
if (flingMode == FlingMode.FLING_BOUNCE) 1f else lastPostCommitFlingScale
)
// apply an additional scale to the closing target to account for fling velocity
tempRectF.scaleCentered(lastPostCommitFlingScale)
}
val scale = tempRectF.width() / backAnimRect.width()
val matrix = baseTransformation?.matrix ?: transformMatrix.apply { reset() }
val scalePivotX =
if (isLetterboxed && enteringHasSameLetterbox) {
closingTarget!!.localBounds.left.toFloat()
} else {
0f
}
matrix.postScale(scale, scale, scalePivotX, 0f)
matrix.postTranslate(tempRectF.left, tempRectF.top)
transaction
.setAlpha(leash, alpha)
.setMatrix(leash, matrix, tmpFloat9)
.setCrop(leash, cropRect)
.setCornerRadius(leash, cornerRadius)
}
protected fun applyTransaction() {
transaction.setFrameTimelineVsync(Choreographer.getInstance().vsyncId)
transaction.apply()
}
private fun ensureScrimLayer() {
if (scrimLayer != null) return
val isDarkTheme: Boolean = isDarkMode(context)
val scrimBuilder =
SurfaceControl.Builder()
.setName("Cross-Activity back animation scrim")
.setCallsite("CrossActivityBackAnimation")
.setColorLayer()
.setOpaque(false)
.setHidden(false)
if (DesktopExperienceFlags.ENABLE_MULTIDISPLAY_TRACKPAD_BACK_GESTURE.isTrue()) {
rootTaskDisplayAreaOrganizer.attachToDisplayArea(
closingTarget!!.taskInfo.getDisplayId(), scrimBuilder)
} else {
rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder)
}
scrimLayer = scrimBuilder.build()
val colorComponents = floatArrayOf(0f, 0f, 0f)
maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT
val scrimCrop =
if (isLetterboxed) {
closingTarget!!.windowConfiguration.bounds
} else {
closingTarget!!.localBounds
}
transaction
.setColor(scrimLayer, colorComponents)
.setAlpha(scrimLayer!!, maxScrimAlpha)
.setCrop(scrimLayer!!, scrimCrop)
.setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1)
.show(scrimLayer)
}
private fun removeScrimLayer() {
if (removeLayer(scrimLayer)) applyTransaction()
scrimLayer = null
}
/**
* Adds two "fake" letterbox square surfaces to the left and right of the localBounds of the
* closing target
*/
private fun ensureLetterboxes() {
closingTarget?.let { t ->
if (t.localBounds.left != 0 && leftLetterboxLayer == null) {
val bounds =
Rect(
0,
t.windowConfiguration.bounds.top,
t.localBounds.left,
t.windowConfiguration.bounds.bottom
)
leftLetterboxLayer = ensureLetterbox(bounds)
}
if (
t.localBounds.right != t.windowConfiguration.bounds.right &&
rightLetterboxLayer == null
) {
val bounds =
Rect(
t.localBounds.right,
t.windowConfiguration.bounds.top,
t.windowConfiguration.bounds.right,
t.windowConfiguration.bounds.bottom
)
rightLetterboxLayer = ensureLetterbox(bounds)
}
}
}
private fun ensureLetterbox(bounds: Rect): SurfaceControl {
val letterboxBuilder =
SurfaceControl.Builder()
.setName("Cross-Activity back animation letterbox")
.setCallsite("CrossActivityBackAnimation")
.setColorLayer()
.setOpaque(true)
.setHidden(false)
if (DesktopExperienceFlags.ENABLE_MULTIDISPLAY_TRACKPAD_BACK_GESTURE.isTrue()) {
rootTaskDisplayAreaOrganizer.attachToDisplayArea(
closingTarget!!.taskInfo.getDisplayId(), letterboxBuilder)
} else {
rootTaskDisplayAreaOrganizer.attachToDisplayArea(
Display.DEFAULT_DISPLAY, letterboxBuilder)
}
val layer = letterboxBuilder.build()
val colorComponents =
floatArrayOf(
Color.red(letterboxColor) / 255f,
Color.green(letterboxColor) / 255f,
Color.blue(letterboxColor) / 255f
)
transaction
.setColor(layer, colorComponents)
.setCrop(layer, bounds)
.setRelativeLayer(layer, closingTarget!!.leash, 1)
.show(layer)
return layer
}
private fun removeLetterbox() {
if (removeLayer(leftLetterboxLayer) || removeLayer(rightLetterboxLayer)) applyTransaction()
leftLetterboxLayer = null
rightLetterboxLayer = null
}
private fun removeLayer(layer: SurfaceControl?): Boolean {
layer?.let {
if (it.isValid) {
transaction.remove(it)
return true
}
}
return false
}
override fun prepareNextAnimation(
animationInfo: BackNavigationInfo.CustomAnimationInfo?,
letterboxColor: Int
): Boolean {
this.letterboxColor = letterboxColor
return false
}
private inner class Callback : IOnBackInvokedCallback.Default() {
override fun onBackStarted(backMotionEvent: BackMotionEvent) {
// in case we're still animating an onBackCancelled event, let's remove the finish-
// callback from the progress animator to prevent calling finishAnimation() before
// restarting a new animation
progressAnimator.removeOnBackCancelledFinishCallback()
startBackAnimation(backMotionEvent)
progressAnimator.onBackStarted(backMotionEvent) { backEvent: BackEvent ->
onGestureProgress(backEvent)
}
}
override fun onBackProgressed(backEvent: BackMotionEvent) {
triggerBack = backEvent.triggerBack
progressAnimator.onBackProgressed(backEvent)
}
override fun onBackCancelled() {
triggerBack = false
progressAnimator.onBackCancelled { finishAnimation() }
}
override fun onBackInvoked() {
triggerBack = true
progressAnimator.reset()
if (predictiveBackTimestampApi()) {
onGestureCommitted(velocityTracker.calculateVelocity())
} else {
onGestureCommitted(progressAnimator.velocity)
}
}
}
private inner class Runner : IRemoteAnimationRunner.Default() {
override fun onAnimationStart(
transit: Int,
apps: Array<RemoteAnimationTarget>,
wallpapers: Array<RemoteAnimationTarget>?,
nonApps: Array<RemoteAnimationTarget>?,
finishedCallback: IRemoteAnimationFinishedCallback
) {
ProtoLog.d(
ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
"Start back to activity animation."
)
for (a in apps) {
when (a.mode) {
RemoteAnimationTarget.MODE_CLOSING -> closingTarget = a
RemoteAnimationTarget.MODE_OPENING -> enteringTarget = a
}
}
finishCallback = finishedCallback
}
override fun onAnimationCancelled() {
finishAnimation()
}
}
companion object {
/** Max scale of the closing window. */
internal const val MAX_SCALE = 0.9f
private const val MAX_SCRIM_ALPHA_DARK = 0.8f
private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f
private const val SPRING_SCALE = 100f
private const val MAX_FLING_VELOCITY = 1000f
private const val DEFAULT_FLING_VELOCITY = 120f
}
enum class FlingMode {
NO_FLING,
/**
* This is used for the closing target in custom cross-activity back animations. When the
* back gesture is flung, the closing target shrinks a bit further with a spring motion.
*/
FLING_SHRINK,
/**
* This is used for the closing and opening target in the default cross-activity back
* animation. When the back gesture is flung, the closing and opening targets shrink a
* bit further and then bounce back with a spring motion.
*/
FLING_BOUNCE
}
}
private fun isDarkMode(context: Context): Boolean {
return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES
}
internal fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) {
require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" }
left = start.left + (target.left - start.left) * progress
top = start.top + (target.top - start.top) * progress
right = start.right + (target.right - start.right) * progress
bottom = start.bottom + (target.bottom - start.bottom) * progress
}
internal fun RectF.scaleCentered(
scale: Float,
pivotX: Float = left + width() / 2,
pivotY: Float = top + height() / 2
) {
offset(-pivotX, -pivotY) // move pivot to origin
scale(scale)
offset(pivotX, pivotY) // Move back to the original position
}