diff options
| author | 2024-10-09 16:31:02 +0000 | |
|---|---|---|
| committer | 2024-10-09 16:31:02 +0000 | |
| commit | f1d48b541c6fa825a5ee918f4943f033c269c4ef (patch) | |
| tree | a0d9a465af7b0753d0d340c0eb2526b90ceb90e9 | |
| parent | 65dc85ecd9ef0dac4fb045d371ef65e9fb9db27e (diff) | |
| parent | eb1be2089cb709cc9a34c5c6ded278cc655f245c (diff) | |
Merge "Derive currentOverscrollSpec to avoid unnecessary layouts/placements" into main
2 files changed, 82 insertions, 10 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt index 9d3f25e9af22..3bd59df16f12 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt @@ -21,6 +21,7 @@ import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Stable +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import com.android.compose.animation.scene.ContentKey @@ -249,18 +250,29 @@ sealed interface TransitionState { private var fromOverscrollSpec: OverscrollSpecImpl? = null private var toOverscrollSpec: OverscrollSpecImpl? = null - /** The current [OverscrollSpecImpl], if this transition is currently overscrolling. */ - internal val currentOverscrollSpec: OverscrollSpecImpl? - get() { - if (this !is HasOverscrollProperties) return null - val progress = progress - val bouncingContent = bouncingContent - return when { - progress < 0f || bouncingContent == fromContent -> fromOverscrollSpec - progress > 1f || bouncingContent == toContent -> toOverscrollSpec - else -> null + /** + * The current [OverscrollSpecImpl], if this transition is currently overscrolling. + * + * Note: This is backed by a State<OverscrollSpecImpl?> because the overscroll spec is + * derived from progress, and we don't want readers of currentOverscrollSpec to recompose + * every time progress is changed. + */ + private val _currentOverscrollSpec: State<OverscrollSpecImpl?>? = + if (this !is HasOverscrollProperties) { + null + } else { + derivedStateOf { + val progress = progress + val bouncingContent = bouncingContent + when { + progress < 0f || bouncingContent == fromContent -> fromOverscrollSpec + progress > 1f || bouncingContent == toContent -> toOverscrollSpec + else -> null + } } } + internal val currentOverscrollSpec: OverscrollSpecImpl? + get() = _currentOverscrollSpec?.value /** * An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden 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 a2c2729e09be..39d46990dc4b 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 @@ -46,6 +46,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.approachLayout +import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.assertIsDisplayed @@ -70,11 +71,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC +import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.subjects.assertThat import com.android.compose.test.assertSizeIsEqualTo import com.android.compose.test.setContentAndCreateMainScope import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.junit.Assert.assertThrows @@ -2581,4 +2584,61 @@ class ElementTest { } } } + + @Test + fun staticSharedElementShouldNotRemeasureOrReplaceDuringOverscrollableTransition() { + val size = 30.dp + var numberOfMeasurements = 0 + var numberOfPlacements = 0 + + // Foo is a simple element that does not move or resize during the transition. + @Composable + fun SceneScope.Foo(modifier: Modifier = Modifier) { + Box( + modifier + .element(TestElements.Foo) + .layout { measurable, constraints -> + numberOfMeasurements++ + measurable.measure(constraints).run { + numberOfPlacements++ + layout(width, height) { place(0, 0) } + } + } + .size(size) + ) + } + + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state) { + scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } } + scene(SceneB) { Box(Modifier.fillMaxSize()) { Foo() } } + } + } + + // Start an overscrollable transition driven by progress. + var progress by mutableFloatStateOf(0f) + val transition = transition(from = SceneA, to = SceneB, progress = { progress }) + assertThat(transition).isInstanceOf(TransitionState.HasOverscrollProperties::class.java) + scope.launch { state.startTransition(transition) } + + // Reset the counters after the first animation frame. + rule.waitForIdle() + numberOfMeasurements = 0 + numberOfPlacements = 0 + + // Change the progress a bunch of times. + val nFrames = 20 + repeat(nFrames) { i -> + progress = i / nFrames.toFloat() + rule.waitForIdle() + + // We shouldn't have remeasured or replaced Foo. + assertWithMessage("Frame $i didn't remeasure Foo") + .that(numberOfMeasurements) + .isEqualTo(0) + assertWithMessage("Frame $i didn't replace Foo").that(numberOfPlacements).isEqualTo(0) + } + } } |