AnimatedSharedAsState does not change value when bouncing
If the value doesn't change during overscroll then it shouldn't change
during bouncing either.
Test: atest ElementTest
Bug: 327257459
Flag: NA
Change-Id: I9b8b11830afdf7779bf8ac4a35600b113c0595e3
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index 37d763b..5d1a7c5 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -281,6 +281,15 @@
// relayout/redraw for nothing.
fromValue
} else {
+ // In the case of bouncing, if the value remains constant during the overscroll,
+ // we should use the value of the scene we are bouncing around.
+ if (!canOverflow && transition is TransitionState.HasOverscrollProperties) {
+ val bouncingScene = transition.bouncingScene
+ if (bouncingScene != null) {
+ return sceneValue(bouncingScene)
+ }
+ }
+
val progress =
if (canOverflow) transition.progress
else transition.progress.fastCoerceIn(0f, 1f)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 0804761..597da9e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -33,11 +33,13 @@
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -545,7 +547,9 @@
layoutWidth: Dp,
layoutHeight: Dp,
sceneTransitions: SceneTransitionsBuilder.() -> Unit,
- firstScroll: Float
+ firstScroll: Float,
+ animatedFloatRange: ClosedFloatingPointRange<Float>,
+ onAnimatedFloat: (Float) -> Unit,
): MutableSceneTransitionLayoutStateImpl {
// The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
// detected as a drag event.
@@ -568,10 +572,24 @@
key = TestScenes.SceneA,
userActions = mapOf(Swipe.Down to TestScenes.SceneB)
) {
+ animateSceneFloatAsState(
+ value = animatedFloatRange.start,
+ key = TestValues.Value1,
+ false
+ )
Spacer(Modifier.fillMaxSize())
}
scene(TestScenes.SceneB) {
+ val animatedFloat by
+ animateSceneFloatAsState(
+ value = animatedFloatRange.endInclusive,
+ key = TestValues.Value1,
+ canOverflow = false
+ )
Spacer(Modifier.element(TestElements.Foo).fillMaxSize())
+ LaunchedEffect(Unit) {
+ snapshotFlow { animatedFloat }.collect { onAnimatedFloat(it) }
+ }
}
}
}
@@ -594,6 +612,7 @@
val layoutWidth = 200.dp
val layoutHeight = 400.dp
val overscrollTranslateY = 10.dp
+ var animatedFloat = 0f
val state =
setupOverscrollScenario(
@@ -606,6 +625,8 @@
}
},
firstScroll = 0.5f, // Scroll 50%
+ animatedFloatRange = 0f..100f,
+ onAnimatedFloat = { animatedFloat = it },
)
val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
@@ -613,6 +634,7 @@
val transition = state.currentTransition
assertThat(transition).isNotNull()
assertThat(transition!!.progress).isEqualTo(0.5f)
+ assertThat(animatedFloat).isEqualTo(50f)
rule.onRoot().performTouchInput {
// Scroll another 100%
@@ -623,6 +645,8 @@
assertThat(transition.progress).isEqualTo(1.5f)
assertThat(state.currentOverscrollSpec).isNotNull()
fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f)
+ // animatedFloat cannot overflow (canOverflow = false)
+ assertThat(animatedFloat).isEqualTo(100f)
rule.onRoot().performTouchInput {
// Scroll another 100%
@@ -633,6 +657,7 @@
assertThat(transition.progress).isEqualTo(2.5f)
assertThat(state.currentOverscrollSpec).isNotNull()
fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f)
+ assertThat(animatedFloat).isEqualTo(100f)
}
@Test
@@ -715,6 +740,7 @@
fun elementTransitionWithDistanceDuringOverscroll() {
val layoutWidth = 200.dp
val layoutHeight = 400.dp
+ var animatedFloat = 0f
val state =
setupOverscrollScenario(
layoutWidth = layoutWidth,
@@ -726,10 +752,13 @@
}
},
firstScroll = 1f, // 100% scroll
+ animatedFloatRange = 0f..100f,
+ onAnimatedFloat = { animatedFloat = it },
)
val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
fooElement.assertTopPositionInRootIsEqualTo(0.dp)
+ assertThat(animatedFloat).isEqualTo(100f)
rule.onRoot().performTouchInput {
// Scroll another 50%
@@ -738,11 +767,13 @@
val transition = state.currentTransition
assertThat(transition).isNotNull()
+ assertThat(animatedFloat).isEqualTo(100f)
// Scroll 150% (100% scroll + 50% overscroll)
assertThat(transition!!.progress).isEqualTo(1.5f)
assertThat(state.currentOverscrollSpec).isNotNull()
fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f)
+ assertThat(animatedFloat).isEqualTo(100f)
rule.onRoot().performTouchInput {
// Scroll another 100%
@@ -753,12 +784,14 @@
assertThat(transition.progress).isEqualTo(2.5f)
assertThat(state.currentOverscrollSpec).isNotNull()
fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f)
+ assertThat(animatedFloat).isEqualTo(100f)
}
@Test
fun elementTransitionWithDistanceDuringOverscrollBouncing() {
val layoutWidth = 200.dp
val layoutHeight = 400.dp
+ var animatedFloat = 0f
val state =
setupOverscrollScenario(
layoutWidth = layoutWidth,
@@ -776,10 +809,13 @@
}
},
firstScroll = 1f, // 100% scroll
+ animatedFloatRange = 0f..100f,
+ onAnimatedFloat = { animatedFloat = it },
)
val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
fooElement.assertTopPositionInRootIsEqualTo(0.dp)
+ assertThat(animatedFloat).isEqualTo(100f)
rule.onRoot().performTouchInput {
// Scroll another 50%
@@ -794,6 +830,7 @@
assertThat(transition.progress).isEqualTo(1.5f)
assertThat(state.currentOverscrollSpec).isNotNull()
fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * (transition.progress - 1f))
+ assertThat(animatedFloat).isEqualTo(100f)
// finger raised
rule.onRoot().performTouchInput { up() }
@@ -802,7 +839,9 @@
// value.
rule.waitUntil(timeoutMillis = 10_000) { transition.progress < 1f }
+ assertThat(transition.progress).isLessThan(1f)
assertThat(state.currentOverscrollSpec).isNotNull()
assertThat(transition.bouncingScene).isEqualTo(transition.toScene)
+ assertThat(animatedFloat).isEqualTo(100f)
}
}