1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.wm.shell.back
18
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.content.res.Configuration
24 import android.graphics.Color
25 import android.graphics.Matrix
26 import android.graphics.PointF
27 import android.graphics.Rect
28 import android.graphics.RectF
29 import android.os.RemoteException
30 import android.util.TimeUtils
31 import android.view.Choreographer
32 import android.view.Display
33 import android.view.IRemoteAnimationFinishedCallback
34 import android.view.IRemoteAnimationRunner
35 import android.view.RemoteAnimationTarget
36 import android.view.SurfaceControl
37 import android.view.animation.DecelerateInterpolator
38 import android.view.animation.Interpolator
39 import android.view.animation.Transformation
40 import android.window.BackEvent
41 import android.window.BackMotionEvent
42 import android.window.BackNavigationInfo
43 import android.window.BackProgressAnimator
44 import android.window.IOnBackInvokedCallback
45 import com.android.internal.dynamicanimation.animation.FloatValueHolder
46 import com.android.internal.dynamicanimation.animation.SpringAnimation
47 import com.android.internal.dynamicanimation.animation.SpringForce
48 import com.android.internal.jank.Cuj
49 import com.android.internal.policy.ScreenDecorationsUtils
50 import com.android.internal.policy.SystemBarUtils
51 import com.android.internal.protolog.common.ProtoLog
52 import com.android.wm.shell.R
53 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
54 import com.android.wm.shell.animation.Interpolators
55 import com.android.wm.shell.protolog.ShellProtoLogGroup
56 import kotlin.math.abs
57 import kotlin.math.max
58 import kotlin.math.min
59
60 abstract class CrossActivityBackAnimation(
61 private val context: Context,
62 private val background: BackAnimationBackground,
63 private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
64 protected val transaction: SurfaceControl.Transaction
65 ) : ShellBackAnimation() {
66
67 protected val startClosingRect = RectF()
68 protected val targetClosingRect = RectF()
69 protected val currentClosingRect = RectF()
70
71 protected val startEnteringRect = RectF()
72 protected val targetEnteringRect = RectF()
73 protected val currentEnteringRect = RectF()
74
75 protected val backAnimRect = Rect()
76 private val cropRect = Rect()
77 private val tempRectF = RectF()
78
79 private var cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
80 private var statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
81
82 private val backAnimationRunner =
83 BackAnimationRunner(Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY)
84 private val initialTouchPos = PointF()
85 private val transformMatrix = Matrix()
86 private val tmpFloat9 = FloatArray(9)
87 protected var enteringTarget: RemoteAnimationTarget? = null
88 protected var closingTarget: RemoteAnimationTarget? = null
89 private var triggerBack = false
90 private var finishCallback: IRemoteAnimationFinishedCallback? = null
91 private val progressAnimator = BackProgressAnimator()
92 protected val displayBoundsMargin =
93 context.resources.getDimension(R.dimen.cross_task_back_vertical_margin)
94
95 private val gestureInterpolator = Interpolators.BACK_GESTURE
96 private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator()
97
98 private var scrimLayer: SurfaceControl? = null
99 private var maxScrimAlpha: Float = 0f
100
101 private var isLetterboxed = false
102 private var enteringHasSameLetterbox = false
103 private var leftLetterboxLayer: SurfaceControl? = null
104 private var rightLetterboxLayer: SurfaceControl? = null
105 private var letterboxColor: Int = 0
106
107 private val postCommitFlingScale = FloatValueHolder(SPRING_SCALE)
108 private var lastPostCommitFlingScale = SPRING_SCALE
109 private val postCommitFlingSpring = SpringForce(SPRING_SCALE)
110 .setStiffness(SpringForce.STIFFNESS_LOW)
111 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
112 protected var gestureProgress = 0f
113
114 /** Background color to be used during the animation, also see [getBackgroundColor] */
115 protected var customizedBackgroundColor = 0
116
117 /**
118 * Whether the entering target should be shifted vertically with the user gesture in pre-commit
119 */
120 abstract val allowEnteringYShift: Boolean
121
122 /**
123 * Subclasses must set the [startClosingRect] and [targetClosingRect] to define the movement
124 * of the closingTarget during pre-commit phase.
125 */
126 abstract fun preparePreCommitClosingRectMovement(@BackEvent.SwipeEdge swipeEdge: Int)
127
128 /**
129 * Subclasses must set the [startEnteringRect] and [targetEnteringRect] to define the movement
130 * of the enteringTarget during pre-commit phase.
131 */
132 abstract fun preparePreCommitEnteringRectMovement()
133
134 /**
135 * Subclasses must provide a duration (in ms) for the post-commit part of the animation
136 */
137 abstract fun getPostCommitAnimationDuration(): Long
138
139 /**
140 * Returns a base transformation to apply to the entering target during pre-commit. The system
141 * will apply the default animation on top of it.
142 */
143 protected open fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation? =
144 null
145
146 override fun onConfigurationChanged(newConfiguration: Configuration) {
147 cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
148 statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
149 }
150
151 override fun getRunner() = backAnimationRunner
152
153 private fun getBackgroundColor(): Int =
154 when {
155 customizedBackgroundColor != 0 -> customizedBackgroundColor
156 isLetterboxed -> letterboxColor
157 enteringTarget != null -> enteringTarget!!.taskInfo.taskDescription!!.backgroundColor
158 else -> 0
159 }
160
161 protected open fun startBackAnimation(backMotionEvent: BackMotionEvent) {
162 if (enteringTarget == null || closingTarget == null) {
163 ProtoLog.d(
164 ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
165 "Entering target or closing target is null."
166 )
167 return
168 }
169 triggerBack = backMotionEvent.triggerBack
170 initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY)
171
172 transaction.setAnimationTransaction()
173 isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed
174 enteringHasSameLetterbox =
175 isLetterboxed && closingTarget!!.localBounds.equals(enteringTarget!!.localBounds)
176
177 if (isLetterboxed && !enteringHasSameLetterbox) {
178 // Play animation with letterboxes, if closing and entering target have mismatching
179 // letterboxes
180 backAnimRect.set(closingTarget!!.windowConfiguration.bounds)
181 } else {
182 // otherwise play animation on localBounds only
183 backAnimRect.set(closingTarget!!.localBounds)
184 }
185 // Offset start rectangle to align task bounds.
186 backAnimRect.offsetTo(0, 0)
187
188 preparePreCommitClosingRectMovement(backMotionEvent.swipeEdge)
189 preparePreCommitEnteringRectMovement()
190
191 background.ensureBackground(
192 closingTarget!!.windowConfiguration.bounds,
193 getBackgroundColor(),
194 transaction,
195 statusbarHeight
196 )
197 ensureScrimLayer()
198 if (isLetterboxed && enteringHasSameLetterbox) {
199 // crop left and right letterboxes
200 cropRect.set(
201 closingTarget!!.localBounds.left,
202 0,
203 closingTarget!!.localBounds.right,
204 closingTarget!!.windowConfiguration.bounds.height()
205 )
206 // and add fake letterbox square surfaces instead
207 ensureLetterboxes()
208 } else {
209 cropRect.set(backAnimRect)
210 }
211 applyTransaction()
212 }
213
214 private fun onGestureProgress(backEvent: BackEvent) {
215 val progress = gestureInterpolator.getInterpolation(backEvent.progress)
216 gestureProgress = progress
217 currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
218 val yOffset = getYOffset(currentClosingRect, backEvent.touchY)
219 currentClosingRect.offset(0f, yOffset)
220 applyTransform(closingTarget?.leash, currentClosingRect, 1f)
221 currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
222 if (allowEnteringYShift) currentEnteringRect.offset(0f, yOffset)
223 val enteringTransformation = getPreCommitEnteringBaseTransformation(progress)
224 applyTransform(
225 enteringTarget?.leash,
226 currentEnteringRect,
227 enteringTransformation?.alpha ?: 1f,
228 enteringTransformation
229 )
230 applyTransaction()
231 background.customizeStatusBarAppearance(currentClosingRect.top.toInt())
232 }
233
234 private fun getYOffset(centeredRect: RectF, touchY: Float): Float {
235 val screenHeight = backAnimRect.height()
236 // Base the window movement in the Y axis on the touch movement in the Y axis.
237 val rawYDelta = touchY - initialTouchPos.y
238 val yDirection = (if (rawYDelta < 0) -1 else 1)
239 // limit yDelta interpretation to 1/2 of screen height in either direction
240 val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f)
241 val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio)
242 // limit y-shift so surface never passes 8dp screen margin
243 val deltaY =
244 max(0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin) *
245 interpolatedYRatio *
246 yDirection
247 return deltaY
248 }
249
250 protected open fun onGestureCommitted(velocity: Float) {
251 if (
252 closingTarget?.leash == null ||
253 enteringTarget?.leash == null ||
254 !enteringTarget!!.leash.isValid ||
255 !closingTarget!!.leash.isValid
256 ) {
257 finishAnimation()
258 return
259 }
260
261 // kick off spring animation with the current velocity from the pre-commit phase, this
262 // affects the scaling of the closing and/or opening activity during post-commit
263 val startVelocity =
264 if (gestureProgress < 0.1f) -DEFAULT_FLING_VELOCITY else -velocity * SPRING_SCALE
265 val flingAnimation = SpringAnimation(postCommitFlingScale, SPRING_SCALE)
266 .setStartVelocity(startVelocity.coerceIn(-MAX_FLING_VELOCITY, 0f))
267 .setStartValue(SPRING_SCALE)
268 .setSpring(postCommitFlingSpring)
269 flingAnimation.start()
270 // do an animation-frame immediately to prevent idle frame
271 flingAnimation.doAnimationFrame(
272 Choreographer.getInstance().lastFrameTimeNanos / TimeUtils.NANOS_PER_MS
273 )
274
275 val valueAnimator =
276 ValueAnimator.ofFloat(1f, 0f).setDuration(getPostCommitAnimationDuration())
277 valueAnimator.addUpdateListener { animation: ValueAnimator ->
278 val progress = animation.animatedFraction
279 onPostCommitProgress(progress)
280 if (progress > 1 - BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD) {
281 background.resetStatusBarCustomization()
282 }
283 }
284 valueAnimator.addListener(
285 object : AnimatorListenerAdapter() {
286 override fun onAnimationEnd(animation: Animator) {
287 background.resetStatusBarCustomization()
288 finishAnimation()
289 }
290 }
291 )
292 valueAnimator.start()
293 }
294
295 protected open fun onPostCommitProgress(linearProgress: Float) {
296 scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) }
297 }
298
299 protected open fun finishAnimation() {
300 enteringTarget?.let {
301 if (it.leash != null && it.leash.isValid) {
302 transaction.setCornerRadius(it.leash, 0f)
303 if (!triggerBack) transaction.setAlpha(it.leash, 0f)
304 it.leash.release()
305 }
306 enteringTarget = null
307 }
308
309 closingTarget?.leash?.release()
310 closingTarget = null
311
312 background.removeBackground(transaction)
313 applyTransaction()
314 transformMatrix.reset()
315 initialTouchPos.set(0f, 0f)
316 try {
317 finishCallback?.onAnimationFinished()
318 } catch (e: RemoteException) {
319 e.printStackTrace()
320 }
321 finishCallback = null
322 removeScrimLayer()
323 removeLetterbox()
324 isLetterboxed = false
325 enteringHasSameLetterbox = false
326 lastPostCommitFlingScale = SPRING_SCALE
327 gestureProgress = 0f
328 triggerBack = false
329 }
330
331 protected fun applyTransform(
332 leash: SurfaceControl?,
333 rect: RectF,
334 alpha: Float,
335 baseTransformation: Transformation? = null,
336 flingMode: FlingMode = FlingMode.NO_FLING
337 ) {
338 if (leash == null || !leash.isValid) return
339 tempRectF.set(rect)
340 if (flingMode != FlingMode.NO_FLING) {
341 lastPostCommitFlingScale = min(
342 postCommitFlingScale.value / SPRING_SCALE,
343 if (flingMode == FlingMode.FLING_BOUNCE) 1f else lastPostCommitFlingScale
344 )
345 // apply an additional scale to the closing target to account for fling velocity
346 tempRectF.scaleCentered(lastPostCommitFlingScale)
347 }
348 val scale = tempRectF.width() / backAnimRect.width()
349 val matrix = baseTransformation?.matrix ?: transformMatrix.apply { reset() }
350 val scalePivotX =
351 if (isLetterboxed && enteringHasSameLetterbox) {
352 closingTarget!!.localBounds.left.toFloat()
353 } else {
354 0f
355 }
356 matrix.postScale(scale, scale, scalePivotX, 0f)
357 matrix.postTranslate(tempRectF.left, tempRectF.top)
358 transaction
359 .setAlpha(leash, alpha)
360 .setMatrix(leash, matrix, tmpFloat9)
361 .setCrop(leash, cropRect)
362 .setCornerRadius(leash, cornerRadius)
363 }
364
365 protected fun applyTransaction() {
366 transaction.setFrameTimelineVsync(Choreographer.getInstance().vsyncId)
367 transaction.apply()
368 }
369
370 private fun ensureScrimLayer() {
371 if (scrimLayer != null) return
372 val isDarkTheme: Boolean = isDarkMode(context)
373 val scrimBuilder =
374 SurfaceControl.Builder()
375 .setName("Cross-Activity back animation scrim")
376 .setCallsite("CrossActivityBackAnimation")
377 .setColorLayer()
378 .setOpaque(false)
379 .setHidden(false)
380
381 rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder)
382 scrimLayer = scrimBuilder.build()
383 val colorComponents = floatArrayOf(0f, 0f, 0f)
384 maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT
385 val scrimCrop =
386 if (isLetterboxed) {
387 closingTarget!!.windowConfiguration.bounds
388 } else {
389 closingTarget!!.localBounds
390 }
391 transaction
392 .setColor(scrimLayer, colorComponents)
393 .setAlpha(scrimLayer!!, maxScrimAlpha)
394 .setCrop(scrimLayer!!, scrimCrop)
395 .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1)
396 .show(scrimLayer)
397 }
398
399 private fun removeScrimLayer() {
400 if (removeLayer(scrimLayer)) applyTransaction()
401 scrimLayer = null
402 }
403
404 /**
405 * Adds two "fake" letterbox square surfaces to the left and right of the localBounds of the
406 * closing target
407 */
408 private fun ensureLetterboxes() {
409 closingTarget?.let { t ->
410 if (t.localBounds.left != 0 && leftLetterboxLayer == null) {
411 val bounds =
412 Rect(
413 0,
414 t.windowConfiguration.bounds.top,
415 t.localBounds.left,
416 t.windowConfiguration.bounds.bottom
417 )
418 leftLetterboxLayer = ensureLetterbox(bounds)
419 }
420 if (
421 t.localBounds.right != t.windowConfiguration.bounds.right &&
422 rightLetterboxLayer == null
423 ) {
424 val bounds =
425 Rect(
426 t.localBounds.right,
427 t.windowConfiguration.bounds.top,
428 t.windowConfiguration.bounds.right,
429 t.windowConfiguration.bounds.bottom
430 )
431 rightLetterboxLayer = ensureLetterbox(bounds)
432 }
433 }
434 }
435
436 private fun ensureLetterbox(bounds: Rect): SurfaceControl {
437 val letterboxBuilder =
438 SurfaceControl.Builder()
439 .setName("Cross-Activity back animation letterbox")
440 .setCallsite("CrossActivityBackAnimation")
441 .setColorLayer()
442 .setOpaque(true)
443 .setHidden(false)
444
445 rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, letterboxBuilder)
446 val layer = letterboxBuilder.build()
447 val colorComponents =
448 floatArrayOf(
449 Color.red(letterboxColor) / 255f,
450 Color.green(letterboxColor) / 255f,
451 Color.blue(letterboxColor) / 255f
452 )
453 transaction
454 .setColor(layer, colorComponents)
455 .setCrop(layer, bounds)
456 .setRelativeLayer(layer, closingTarget!!.leash, 1)
457 .show(layer)
458 return layer
459 }
460
461 private fun removeLetterbox() {
462 if (removeLayer(leftLetterboxLayer) || removeLayer(rightLetterboxLayer)) applyTransaction()
463 leftLetterboxLayer = null
464 rightLetterboxLayer = null
465 }
466
467 private fun removeLayer(layer: SurfaceControl?): Boolean {
468 layer?.let {
469 if (it.isValid) {
470 transaction.remove(it)
471 return true
472 }
473 }
474 return false
475 }
476
477 override fun prepareNextAnimation(
478 animationInfo: BackNavigationInfo.CustomAnimationInfo?,
479 letterboxColor: Int
480 ): Boolean {
481 this.letterboxColor = letterboxColor
482 return false
483 }
484
485 private inner class Callback : IOnBackInvokedCallback.Default() {
486 override fun onBackStarted(backMotionEvent: BackMotionEvent) {
487 // in case we're still animating an onBackCancelled event, let's remove the finish-
488 // callback from the progress animator to prevent calling finishAnimation() before
489 // restarting a new animation
490 progressAnimator.removeOnBackCancelledFinishCallback()
491
492 startBackAnimation(backMotionEvent)
493 progressAnimator.onBackStarted(backMotionEvent) { backEvent: BackEvent ->
494 onGestureProgress(backEvent)
495 }
496 }
497
498 override fun onBackProgressed(backEvent: BackMotionEvent) {
499 triggerBack = backEvent.triggerBack
500 progressAnimator.onBackProgressed(backEvent)
501 }
502
503 override fun onBackCancelled() {
504 triggerBack = false
505 progressAnimator.onBackCancelled { finishAnimation() }
506 }
507
508 override fun onBackInvoked() {
509 triggerBack = true
510 progressAnimator.reset()
511 onGestureCommitted(progressAnimator.velocity)
512 }
513 }
514
515 private inner class Runner : IRemoteAnimationRunner.Default() {
516 override fun onAnimationStart(
517 transit: Int,
518 apps: Array<RemoteAnimationTarget>,
519 wallpapers: Array<RemoteAnimationTarget>?,
520 nonApps: Array<RemoteAnimationTarget>?,
521 finishedCallback: IRemoteAnimationFinishedCallback
522 ) {
523 ProtoLog.d(
524 ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
525 "Start back to activity animation."
526 )
527 for (a in apps) {
528 when (a.mode) {
529 RemoteAnimationTarget.MODE_CLOSING -> closingTarget = a
530 RemoteAnimationTarget.MODE_OPENING -> enteringTarget = a
531 }
532 }
533 finishCallback = finishedCallback
534 }
535
536 override fun onAnimationCancelled() {
537 finishAnimation()
538 }
539 }
540
541 companion object {
542 /** Max scale of the closing window. */
543 internal const val MAX_SCALE = 0.9f
544 private const val MAX_SCRIM_ALPHA_DARK = 0.8f
545 private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f
546 private const val SPRING_SCALE = 100f
547 private const val MAX_FLING_VELOCITY = 1000f
548 private const val DEFAULT_FLING_VELOCITY = 120f
549 }
550
551 enum class FlingMode {
552 NO_FLING,
553
554 /**
555 * This is used for the closing target in custom cross-activity back animations. When the
556 * back gesture is flung, the closing target shrinks a bit further with a spring motion.
557 */
558 FLING_SHRINK,
559
560 /**
561 * This is used for the closing and opening target in the default cross-activity back
562 * animation. When the back gesture is flung, the closing and opening targets shrink a
563 * bit further and then bounce back with a spring motion.
564 */
565 FLING_BOUNCE
566 }
567 }
568
isDarkModenull569 private fun isDarkMode(context: Context): Boolean {
570 return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
571 Configuration.UI_MODE_NIGHT_YES
572 }
573
setInterpolatedRectFnull574 internal fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) {
575 require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" }
576 left = start.left + (target.left - start.left) * progress
577 top = start.top + (target.top - start.top) * progress
578 right = start.right + (target.right - start.right) * progress
579 bottom = start.bottom + (target.bottom - start.bottom) * progress
580 }
581
scaleCenterednull582 internal fun RectF.scaleCentered(
583 scale: Float,
584 pivotX: Float = left + width() / 2,
585 pivotY: Float = top + height() / 2
586 ) {
587 offset(-pivotX, -pivotY) // move pivot to origin
588 scale(scale)
589 offset(pivotX, pivotY) // Move back to the original position
590 }
591