Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt +10 −9 Original line number Diff line number Diff line Loading @@ -18,7 +18,7 @@ package com.android.compose.animation.scene import androidx.activity.BackEventCompat import androidx.activity.compose.PredictiveBackHandler import androidx.compose.animation.core.spring import androidx.compose.animation.core.AnimationSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Composable import kotlin.coroutines.cancellation.CancellationException Loading Loading @@ -62,7 +62,7 @@ private suspend fun <T : ContentKey> animate( animation: SwipeAnimation<T>, progress: Flow<BackEventCompat>, ) { fun animateOffset(targetContent: T) { fun animateOffset(targetContent: T, spec: AnimationSpec<Float>? = null) { if ( layoutImpl.state.transitionState != animation.contentTransition || animation.isFinishing ) { Loading @@ -72,12 +72,7 @@ private suspend fun <T : ContentKey> animate( animation.animateOffset( initialVelocity = 0f, targetContent = targetContent, // TODO(b/350705972): Allow to customize or reuse the same customization endpoints as // the normal swipe transitions. We can't just reuse them here because other swipe // transitions animate pixels while this transition animates progress, so the visibility // thresholds will be completely different. spec = spring(), spec = spec, ) } Loading @@ -86,9 +81,15 @@ private suspend fun <T : ContentKey> animate( progress.collect { backEvent -> animation.dragOffset = backEvent.progress } // Back gesture successful. animateOffset(animation.toContent) animateOffset( animation.toContent, animation.contentTransition.transformationSpec.progressSpec ) } catch (e: CancellationException) { // Back gesture cancelled. // If the back gesture is cancelled, the progress is animated back to 0f by the system. // Since the remaining change in progress is usually very small, the progressSpec is omitted // and the default spring spec used instead. animateOffset(animation.fromContent) } } packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +2 −2 Original line number Diff line number Diff line Loading @@ -17,8 +17,8 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf Loading Loading @@ -320,7 +320,7 @@ internal class SwipeAnimation<T : ContentKey>( fun animateOffset( initialVelocity: Float, targetContent: T, spec: SpringSpec<Float>? = null, spec: AnimationSpec<Float>? = null, ): OffsetAnimation { val initialProgress = progress // Skip the animation if we have already reached the target content and the overscroll does Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt +34 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.compose.animation.scene import androidx.activity.BackEventCompat import androidx.activity.ComponentActivity import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size Loading Loading @@ -65,7 +67,23 @@ class PredictiveBackHandlerTest { @Test fun testPredictiveBack() { val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } val transitionFrames = 2 val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState( SceneA, transitions = transitions { from(SceneA, to = SceneB) { spec = tween( durationMillis = transitionFrames * 16, easing = LinearEasing ) } } ) } rule.setContent { SceneTransitionLayout(layoutState) { scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) } Loading Loading @@ -94,12 +112,27 @@ class PredictiveBackHandlerTest { assertThat(layoutState.transitionState).hasCurrentScene(SceneA) assertThat(layoutState.transitionState).isIdle() rule.mainClock.autoAdvance = false // Start again and commit it. rule.runOnUiThread { dispatcher.dispatchOnBackStarted(backEvent()) dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f)) dispatcher.onBackPressed() } rule.mainClock.advanceTimeByFrame() rule.waitForIdle() val transition2 = assertThat(layoutState.transitionState).isSceneTransition() // verify that transition picks up progress from preview assertThat(transition2).hasProgress(0.4f, tolerance = 0.0001f) rule.mainClock.advanceTimeByFrame() rule.waitForIdle() // verify that transition is half way between preview-end-state (0.4f) and target-state (1f) // after one frame assertThat(transition2).hasProgress(0.7f, tolerance = 0.0001f) rule.mainClock.autoAdvance = true rule.waitForIdle() assertThat(layoutState.transitionState).hasCurrentScene(SceneB) assertThat(layoutState.transitionState).isIdle() Loading Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt +10 −9 Original line number Diff line number Diff line Loading @@ -18,7 +18,7 @@ package com.android.compose.animation.scene import androidx.activity.BackEventCompat import androidx.activity.compose.PredictiveBackHandler import androidx.compose.animation.core.spring import androidx.compose.animation.core.AnimationSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Composable import kotlin.coroutines.cancellation.CancellationException Loading Loading @@ -62,7 +62,7 @@ private suspend fun <T : ContentKey> animate( animation: SwipeAnimation<T>, progress: Flow<BackEventCompat>, ) { fun animateOffset(targetContent: T) { fun animateOffset(targetContent: T, spec: AnimationSpec<Float>? = null) { if ( layoutImpl.state.transitionState != animation.contentTransition || animation.isFinishing ) { Loading @@ -72,12 +72,7 @@ private suspend fun <T : ContentKey> animate( animation.animateOffset( initialVelocity = 0f, targetContent = targetContent, // TODO(b/350705972): Allow to customize or reuse the same customization endpoints as // the normal swipe transitions. We can't just reuse them here because other swipe // transitions animate pixels while this transition animates progress, so the visibility // thresholds will be completely different. spec = spring(), spec = spec, ) } Loading @@ -86,9 +81,15 @@ private suspend fun <T : ContentKey> animate( progress.collect { backEvent -> animation.dragOffset = backEvent.progress } // Back gesture successful. animateOffset(animation.toContent) animateOffset( animation.toContent, animation.contentTransition.transformationSpec.progressSpec ) } catch (e: CancellationException) { // Back gesture cancelled. // If the back gesture is cancelled, the progress is animated back to 0f by the system. // Since the remaining change in progress is usually very small, the progressSpec is omitted // and the default spring spec used instead. animateOffset(animation.fromContent) } }
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +2 −2 Original line number Diff line number Diff line Loading @@ -17,8 +17,8 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf Loading Loading @@ -320,7 +320,7 @@ internal class SwipeAnimation<T : ContentKey>( fun animateOffset( initialVelocity: Float, targetContent: T, spec: SpringSpec<Float>? = null, spec: AnimationSpec<Float>? = null, ): OffsetAnimation { val initialProgress = progress // Skip the animation if we have already reached the target content and the overscroll does Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt +34 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.compose.animation.scene import androidx.activity.BackEventCompat import androidx.activity.ComponentActivity import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size Loading Loading @@ -65,7 +67,23 @@ class PredictiveBackHandlerTest { @Test fun testPredictiveBack() { val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } val transitionFrames = 2 val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState( SceneA, transitions = transitions { from(SceneA, to = SceneB) { spec = tween( durationMillis = transitionFrames * 16, easing = LinearEasing ) } } ) } rule.setContent { SceneTransitionLayout(layoutState) { scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) } Loading Loading @@ -94,12 +112,27 @@ class PredictiveBackHandlerTest { assertThat(layoutState.transitionState).hasCurrentScene(SceneA) assertThat(layoutState.transitionState).isIdle() rule.mainClock.autoAdvance = false // Start again and commit it. rule.runOnUiThread { dispatcher.dispatchOnBackStarted(backEvent()) dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f)) dispatcher.onBackPressed() } rule.mainClock.advanceTimeByFrame() rule.waitForIdle() val transition2 = assertThat(layoutState.transitionState).isSceneTransition() // verify that transition picks up progress from preview assertThat(transition2).hasProgress(0.4f, tolerance = 0.0001f) rule.mainClock.advanceTimeByFrame() rule.waitForIdle() // verify that transition is half way between preview-end-state (0.4f) and target-state (1f) // after one frame assertThat(transition2).hasProgress(0.7f, tolerance = 0.0001f) rule.mainClock.autoAdvance = true rule.waitForIdle() assertThat(layoutState.transitionState).hasCurrentScene(SceneB) assertThat(layoutState.transitionState).isIdle() Loading