Add bottomsheet predictive back animation for SysUI
Bug: 340724858
Flag: com.android.systemui.predictiveBackAnimateDialogs
Test: atest BackAnimationSpecTest
Test: atest SystemUIDialogTest
Test: atest BackTransformationTest
Test: Manual, i.e. testing predictive back animation for volume panel bottomsheet on device
Change-Id: Ica27bb102e747dd83503eae70a93cfaad1906050
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
index b057296..536f297 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
@@ -73,3 +73,11 @@
maxMarginYdp = 8f,
minScale = 0.9f,
)
+
+/**
+ * SysUI transitions - Bottomsheet (AT3)
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/at-3-bottom-sheets
+ */
+fun BackAnimationSpec.Companion.bottomSheetForSysUi(
+ displayMetricsProvider: () -> DisplayMetrics,
+): BackAnimationSpec = BackAnimationSpec.createBottomsheetAnimationSpec(displayMetricsProvider)
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
index 49d1fb4..029f62c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
@@ -26,12 +26,36 @@
var translateX: Float = Float.NaN,
var translateY: Float = Float.NaN,
var scale: Float = Float.NaN,
+ var scalePivotPosition: ScalePivotPosition? = null,
)
+/** Enum that describes the location of the scale pivot position */
+enum class ScalePivotPosition {
+ // more options may be added in the future
+ CENTER,
+ BOTTOM_CENTER;
+
+ fun applyTo(view: View) {
+ val pivotX =
+ when (this) {
+ CENTER -> view.width / 2f
+ BOTTOM_CENTER -> view.width / 2f
+ }
+ val pivotY =
+ when (this) {
+ CENTER -> view.height / 2f
+ BOTTOM_CENTER -> view.height.toFloat()
+ }
+ view.pivotX = pivotX
+ view.pivotY = pivotY
+ }
+}
+
/** Apply the transformation to the [targetView] */
fun BackTransformation.applyTo(targetView: View) {
if (translateX.isFinite()) targetView.translationX = translateX
if (translateY.isFinite()) targetView.translationY = translateY
+ scalePivotPosition?.applyTo(targetView)
if (scale.isFinite()) {
targetView.scaleX = scale
targetView.scaleY = scale
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BottomsheetBackAnimationSpec.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BottomsheetBackAnimationSpec.kt
new file mode 100644
index 0000000..b1945a1
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BottomsheetBackAnimationSpec.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.systemui.animation.back
+
+import android.util.DisplayMetrics
+import android.view.animation.Interpolator
+import com.android.app.animation.Interpolators
+import com.android.systemui.util.dpToPx
+
+private const val MAX_SCALE_DELTA_DP = 48
+
+/** Create a [BackAnimationSpec] from [displayMetrics] and design specs. */
+fun BackAnimationSpec.Companion.createBottomsheetAnimationSpec(
+ displayMetricsProvider: () -> DisplayMetrics,
+ scaleEasing: Interpolator = Interpolators.BACK_GESTURE,
+): BackAnimationSpec {
+ return BackAnimationSpec { backEvent, _, result ->
+ val displayMetrics = displayMetricsProvider()
+ val screenWidthPx = displayMetrics.widthPixels
+ val minScale = 1 - MAX_SCALE_DELTA_DP.dpToPx(displayMetrics) / screenWidthPx
+ val progressX = backEvent.progress
+ val ratioScale = scaleEasing.getInterpolation(progressX)
+ result.apply {
+ scale = 1f - ratioScale * (1f - minScale)
+ scalePivotPosition = ScalePivotPosition.BOTTOM_CENTER
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/EdgeToEdgeDialogDelegate.kt b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/EdgeToEdgeDialogDelegate.kt
index 55dfed4..5fc78c0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/EdgeToEdgeDialogDelegate.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/EdgeToEdgeDialogDelegate.kt
@@ -17,8 +17,11 @@
package com.android.systemui.statusbar.phone
import android.os.Bundle
+import android.util.DisplayMetrics
import android.view.Gravity
import android.view.WindowManager
+import com.android.systemui.animation.back.BackAnimationSpec
+import com.android.systemui.animation.back.bottomSheetForSysUi
/** [DialogDelegate] that configures a dialog to be an edge-to-edge one. */
class EdgeToEdgeDialogDelegate : DialogDelegate<SystemUIDialog> {
@@ -40,4 +43,10 @@
override fun getWidth(dialog: SystemUIDialog): Int = WindowManager.LayoutParams.MATCH_PARENT
override fun getHeight(dialog: SystemUIDialog): Int = WindowManager.LayoutParams.MATCH_PARENT
+
+ override fun getBackAnimationSpec(
+ displayMetricsProvider: () -> DisplayMetrics
+ ): BackAnimationSpec {
+ return BackAnimationSpec.bottomSheetForSysUi(displayMetricsProvider)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
index 1cdf8dc..79b5cc3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
@@ -21,8 +21,10 @@
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.BroadcastReceiver;
import android.content.Intent;
@@ -41,6 +43,7 @@
import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.back.BackAnimationSpec;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.model.SysUiState;
@@ -78,6 +81,8 @@
MockitoAnnotations.initMocks(this);
mDependency.injectTestDependency(BroadcastDispatcher.class, mBroadcastDispatcher);
+ when(mDelegate.getBackAnimationSpec(ArgumentMatchers.any()))
+ .thenReturn(mock(BackAnimationSpec.class));
}
@Test
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DialogDelegate.kt
index 25d1f05..2beb66b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DialogDelegate.kt
@@ -19,7 +19,10 @@
import android.app.Dialog
import android.content.res.Configuration
import android.os.Bundle
+import android.util.DisplayMetrics
import android.view.ViewRootImpl
+import com.android.systemui.animation.back.BackAnimationSpec
+import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
/**
* A delegate class that should be implemented in place of subclassing [Dialog].
@@ -49,4 +52,7 @@
fun getWidth(dialog: T): Int = SystemUIDialog.getDefaultDialogWidth(dialog)
fun getHeight(dialog: T): Int = SystemUIDialog.getDefaultDialogHeight()
+
+ fun getBackAnimationSpec(displayMetricsProvider: () -> DisplayMetrics): BackAnimationSpec =
+ BackAnimationSpec.floatingSystemSurfacesForSysUi(displayMetricsProvider)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index c74dde5..e01556f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -269,9 +269,12 @@
mOnCreateRunnables.get(i).run();
}
if (predictiveBackAnimateDialogs()) {
+ View targetView = getWindow().getDecorView();
DialogKt.registerAnimationOnBackInvoked(
/* dialog = */ this,
- /* targetView = */ getWindow().getDecorView()
+ /* targetView = */ targetView,
+ /* backAnimationSpec= */mDelegate.getBackAnimationSpec(
+ () -> targetView.getResources().getDisplayMetrics())
);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
index 190babd..0ed84ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
@@ -4,7 +4,9 @@
import android.window.BackEvent
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.dpToPx
import com.google.common.truth.Truth.assertThat
+import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -60,6 +62,58 @@
expected = BackTransformation(translateX = 0f, translateY = maxY, scale = 1f),
)
}
+
+ @Test
+ fun sysUi_bottomsheet_animationValues() {
+ val minScale = 1 - 48.dpToPx(displayMetrics) / displayMetrics.widthPixels
+
+ val backAnimationSpec = BackAnimationSpec.bottomSheetForSysUi { displayMetrics }
+
+ assertBackTransformation(
+ backAnimationSpec = backAnimationSpec,
+ backInput = BackInput(progressX = 0f, progressY = 0f, edge = BackEvent.EDGE_LEFT),
+ expected =
+ BackTransformation(
+ translateX = Float.NaN,
+ translateY = Float.NaN,
+ scale = 1f,
+ scalePivotPosition = ScalePivotPosition.BOTTOM_CENTER
+ ),
+ )
+ assertBackTransformation(
+ backAnimationSpec = backAnimationSpec,
+ backInput = BackInput(progressX = 1f, progressY = 0f, edge = BackEvent.EDGE_LEFT),
+ expected =
+ BackTransformation(
+ translateX = Float.NaN,
+ translateY = Float.NaN,
+ scale = minScale,
+ scalePivotPosition = ScalePivotPosition.BOTTOM_CENTER
+ ),
+ )
+ assertBackTransformation(
+ backAnimationSpec = backAnimationSpec,
+ backInput = BackInput(progressX = 1f, progressY = 0f, edge = BackEvent.EDGE_RIGHT),
+ expected =
+ BackTransformation(
+ translateX = Float.NaN,
+ translateY = Float.NaN,
+ scale = minScale,
+ scalePivotPosition = ScalePivotPosition.BOTTOM_CENTER
+ ),
+ )
+ assertBackTransformation(
+ backAnimationSpec = backAnimationSpec,
+ backInput = BackInput(progressX = 1f, progressY = 1f, edge = BackEvent.EDGE_LEFT),
+ expected =
+ BackTransformation(
+ translateX = Float.NaN,
+ translateY = Float.NaN,
+ scale = minScale,
+ scalePivotPosition = ScalePivotPosition.BOTTOM_CENTER
+ ),
+ )
+ }
}
private fun assertBackTransformation(
@@ -81,7 +135,16 @@
)
val tolerance = 0f
- assertThat(actual.translateX).isWithin(tolerance).of(expected.translateX)
- assertThat(actual.translateY).isWithin(tolerance).of(expected.translateY)
+ if (expected.translateX.isNaN()) {
+ assertEquals(expected.translateX, actual.translateX)
+ } else {
+ assertThat(actual.translateX).isWithin(tolerance).of(expected.translateX)
+ }
+ if (expected.translateY.isNaN()) {
+ assertEquals(expected.translateY, actual.translateY)
+ } else {
+ assertThat(actual.translateY).isWithin(tolerance).of(expected.translateY)
+ }
assertThat(actual.scale).isWithin(tolerance).of(expected.scale)
+ assertEquals(expected.scalePivotPosition, actual.scalePivotPosition)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
index 190b3d2..44a5467 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
@@ -5,17 +5,25 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.kotlin.whenever
@SmallTest
@RunWith(JUnit4::class)
class BackTransformationTest : SysuiTestCase() {
private val targetView: View = mock()
+ @Before
+ fun setup() {
+ whenever(targetView.width).thenReturn(TARGET_VIEW_WIDTH)
+ whenever(targetView.height).thenReturn(TARGET_VIEW_HEIGHT)
+ }
+
@Test
fun defaultValue_noTransformation() {
val transformation = BackTransformation()
@@ -70,6 +78,16 @@
}
@Test
+ fun applyTo_targetView_scale_pivot() {
+ val transformation = BackTransformation(scalePivotPosition = ScalePivotPosition.CENTER)
+
+ transformation.applyTo(targetView = targetView)
+
+ verify(targetView).pivotX = TARGET_VIEW_WIDTH / 2f
+ verify(targetView).pivotY = TARGET_VIEW_HEIGHT / 2f
+ }
+
+ @Test
fun applyTo_targetView_noTransformation() {
val transformation = BackTransformation()
@@ -77,4 +95,9 @@
verifyNoMoreInteractions(targetView)
}
+
+ companion object {
+ private const val TARGET_VIEW_WIDTH = 100
+ private const val TARGET_VIEW_HEIGHT = 50
+ }
}