diff options
| author | 2024-11-01 17:49:15 +0000 | |
|---|---|---|
| committer | 2024-11-01 17:49:15 +0000 | |
| commit | 9ad90b8d8032ed973aaa8364d93cde9d56ea9a06 (patch) | |
| tree | 150c4e889dbf0942198e87fe2924ed0ccfaa9fdd | |
| parent | 5e4f136bd493f35002c2228e88cb33d5fe78cade (diff) | |
| parent | 957f7c9970b95b9758122df61db2add43caf1808 (diff) | |
Merge "PriorityNestedScrollConnection: Add flingToScroll [1/2]" into main
9 files changed, 153 insertions, 40 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt index 5cb45e5bd914..94c18cdbef5a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt @@ -16,11 +16,13 @@ package com.android.systemui.notifications.ui.composable +import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.util.fastCoerceAtLeast import androidx.compose.ui.util.fastCoerceAtMost +import com.android.compose.nestedscroll.OnStopScope import com.android.compose.nestedscroll.PriorityNestedScrollConnection import com.android.compose.nestedscroll.ScrollController @@ -43,6 +45,7 @@ fun NotificationScrimNestedScrollConnection( isCurrentGestureOverscroll: () -> Boolean, onStart: (Float) -> Unit = {}, onStop: (Float) -> Unit = {}, + flingBehavior: FlingBehavior, ): PriorityNestedScrollConnection { return PriorityNestedScrollConnection( orientation = Orientation.Vertical, @@ -77,8 +80,9 @@ fun NotificationScrimNestedScrollConnection( return amountConsumed } - override suspend fun onStop(initialVelocity: Float): Float { - onStop(initialVelocity) + override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { + val consumedByScroll = flingToScroll(initialVelocity, flingBehavior) + onStop(initialVelocity - consumedByScroll) if (scrimOffset() < minScrimOffset()) { animateScrimOffset(minScrimOffset()) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt index e1b74a968caa..d8abfd7a4b94 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt @@ -18,7 +18,9 @@ package com.android.systemui.notifications.ui.composable import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween +import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.layout.offset import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -30,6 +32,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastCoerceAtLeast +import com.android.compose.nestedscroll.OnStopScope import com.android.compose.nestedscroll.PriorityNestedScrollConnection import com.android.compose.nestedscroll.ScrollController import kotlin.math.max @@ -46,32 +49,35 @@ fun Modifier.stackVerticalOverscroll( val screenHeight = with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.toPx() } val overscrollOffset = remember { Animatable(0f) } - val stackNestedScrollConnection = remember { - NotificationStackNestedScrollConnection( - stackOffset = { overscrollOffset.value }, - canScrollForward = canScrollForward, - onScroll = { offsetAvailable -> - coroutineScope.launch { - val maxProgress = screenHeight * 0.2f - val tilt = 3f - var offset = - overscrollOffset.value + - maxProgress * tanh(x = offsetAvailable / (maxProgress * tilt)) - offset = max(offset, -1f * maxProgress) - overscrollOffset.snapTo(offset) - } - }, - onStop = { velocityAvailable -> - coroutineScope.launch { - overscrollOffset.animateTo( - targetValue = 0f, - initialVelocity = velocityAvailable, - animationSpec = tween(), - ) - } - }, - ) - } + val flingBehavior = ScrollableDefaults.flingBehavior() + val stackNestedScrollConnection = + remember(flingBehavior) { + NotificationStackNestedScrollConnection( + stackOffset = { overscrollOffset.value }, + canScrollForward = canScrollForward, + onScroll = { offsetAvailable -> + coroutineScope.launch { + val maxProgress = screenHeight * 0.2f + val tilt = 3f + var offset = + overscrollOffset.value + + maxProgress * tanh(x = offsetAvailable / (maxProgress * tilt)) + offset = max(offset, -1f * maxProgress) + overscrollOffset.snapTo(offset) + } + }, + onStop = { velocityAvailable -> + coroutineScope.launch { + overscrollOffset.animateTo( + targetValue = 0f, + initialVelocity = velocityAvailable, + animationSpec = tween(), + ) + } + }, + flingBehavior = flingBehavior, + ) + } return this.then( Modifier.nestedScroll(stackNestedScrollConnection).offset { @@ -86,6 +92,7 @@ fun NotificationStackNestedScrollConnection( onStart: (Float) -> Unit = {}, onScroll: (Float) -> Unit, onStop: (Float) -> Unit = {}, + flingBehavior: FlingBehavior, ): PriorityNestedScrollConnection { return PriorityNestedScrollConnection( orientation = Orientation.Vertical, @@ -106,8 +113,9 @@ fun NotificationStackNestedScrollConnection( return consumed } - override suspend fun onStop(initialVelocity: Float): Float { - onStop(initialVelocity) + override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { + val consumedByScroll = flingToScroll(initialVelocity, flingBehavior) + onStop(initialVelocity - consumedByScroll) return initialVelocity } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 4fa1984661da..ae273d8e2ad9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollBy @@ -451,12 +452,14 @@ fun SceneScope.NotificationScrollingStack( } } + val flingBehavior = ScrollableDefaults.flingBehavior() val scrimNestedScrollConnection = shadeSession.rememberSession( scrimOffset, maxScrimTop, minScrimTop, isCurrentGestureOverscroll, + flingBehavior, ) { NotificationScrimNestedScrollConnection( scrimOffset = { scrimOffset.value }, @@ -469,6 +472,7 @@ fun SceneScope.NotificationScrollingStack( contentHeight = { stackHeight.intValue.toFloat() }, minVisibleScrimHeight = minVisibleScrimHeight, isCurrentGestureOverscroll = { isCurrentGestureOverscroll.value }, + flingBehavior = flingBehavior, ) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 7c7202a5c7f2..7872ffad6cec 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.util.fastCoerceIn import com.android.compose.animation.scene.content.Content import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified +import com.android.compose.nestedscroll.OnStopScope import com.android.compose.nestedscroll.PriorityNestedScrollConnection import com.android.compose.nestedscroll.ScrollController import kotlin.math.absoluteValue @@ -749,7 +750,7 @@ private fun scrollController( return dragController.onDrag(delta = deltaScroll) } - override suspend fun onStop(initialVelocity: Float): Float { + override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { return dragController .onStop(velocity = initialVelocity, canChangeContent = canChangeScene) .invoke() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt index 255da31719f3..a5be4dc195bc 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt @@ -16,6 +16,7 @@ package com.android.compose.nestedscroll +import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource @@ -41,6 +42,7 @@ fun LargeTopAppBarNestedScrollConnection( onHeightChanged: (Float) -> Unit, minHeight: () -> Float, maxHeight: () -> Float, + flingBehavior: FlingBehavior, ): PriorityNestedScrollConnection { return PriorityNestedScrollConnection( orientation = Orientation.Vertical, @@ -55,7 +57,15 @@ fun LargeTopAppBarNestedScrollConnection( offsetAvailable > 0 && height() < maxHeight() }, canStartPostFling = { false }, - onStart = { LargeTopAppBarScrollController(height, maxHeight, minHeight, onHeightChanged) }, + onStart = { + LargeTopAppBarScrollController( + height = height, + maxHeight = maxHeight, + minHeight = minHeight, + onHeightChanged = onHeightChanged, + flingBehavior = flingBehavior, + ) + }, ) } @@ -64,6 +74,7 @@ private class LargeTopAppBarScrollController( val maxHeight: () -> Float, val minHeight: () -> Float, val onHeightChanged: (Float) -> Unit, + val flingBehavior: FlingBehavior, ) : ScrollController { override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float { val currentHeight = height() @@ -79,9 +90,8 @@ private class LargeTopAppBarScrollController( return amountConsumed } - override suspend fun onStop(initialVelocity: Float): Float { - // Don't consume the velocity on pre/post fling - return 0f + override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { + return flingToScroll(initialVelocity, flingBehavior) } override fun onCancel() { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt index ca44a5c21cab..e924ebfd2a8d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt @@ -16,7 +16,9 @@ package com.android.compose.nestedscroll +import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource @@ -83,7 +85,16 @@ interface ScrollController { * @param initialVelocity The initial velocity of the scroll when stopping. * @return The consumed [initialVelocity] when the animation completes. */ - suspend fun onStop(initialVelocity: Float): Float + suspend fun OnStopScope.onStop(initialVelocity: Float): Float +} + +interface OnStopScope { + /** + * Emits scroll events by using the [initialVelocity] and the [FlingBehavior]. + * + * @return consumed velocity + */ + suspend fun flingToScroll(initialVelocity: Float, flingBehavior: FlingBehavior): Float } /** @@ -307,7 +318,11 @@ class PriorityNestedScrollConnection( val controller = requireController(isStopping = false) return coroutineScope { try { - async { controller.onStop(velocity) } + async { + with(controller) { + OnStopScopeImpl(controller = controller).onStop(velocity) + } + } // Allows others to interrupt the job. .also { stoppingJob = it } // Note: this can be cancelled by [interruptStopping] @@ -336,3 +351,19 @@ class PriorityNestedScrollConnection( offsetScrolledBeforePriorityMode = 0f } } + +private class OnStopScopeImpl(private val controller: ScrollController) : OnStopScope { + override suspend fun flingToScroll( + initialVelocity: Float, + flingBehavior: FlingBehavior, + ): Float { + return with(flingBehavior) { + object : ScrollScope { + override fun scrollBy(pixels: Float): Float { + return controller.onScroll(pixels, NestedScrollSource.SideEffect) + } + } + .performFling(initialVelocity) + } + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt index a406e13904f5..e27f9b52153d 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt @@ -16,6 +16,8 @@ package com.android.compose.nestedscroll +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource @@ -29,6 +31,13 @@ class LargeTopAppBarNestedScrollConnectionTest(testCase: TestCase) { val scrollSource = testCase.scrollSource private var height = 0f + private val customFlingBehavior = + object : FlingBehavior { + override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { + scrollBy(initialVelocity) + return initialVelocity / 2f + } + } private fun buildScrollConnection(heightRange: ClosedFloatingPointRange<Float>) = LargeTopAppBarNestedScrollConnection( @@ -36,6 +45,7 @@ class LargeTopAppBarNestedScrollConnectionTest(testCase: TestCase) { onHeightChanged = { height = it }, minHeight = { heightRange.start }, maxHeight = { heightRange.endInclusive }, + flingBehavior = customFlingBehavior, ) private fun NestedScrollConnection.scroll( diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt index 0364cdc4166e..54428404bd0c 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt @@ -18,12 +18,15 @@ package com.android.compose.nestedscroll +import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput import androidx.compose.ui.unit.Velocity import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.test.runMonotonicClockTest import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -41,7 +44,16 @@ class PriorityNestedScrollConnectionTest { private var consumeScroll = true private var lastStop: Float? = null private var isCancelled: Boolean = false - private var consumeStop = true + private var onStopConsumeFlingToScroll = false + private var onStopConsumeAll = true + + private val customFlingBehavior = + object : FlingBehavior { + override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { + scrollBy(initialVelocity) + return initialVelocity / 2f + } + } private val scrollConnection = PriorityNestedScrollConnection( @@ -57,9 +69,16 @@ class PriorityNestedScrollConnectionTest { return if (consumeScroll) deltaScroll else 0f } - override suspend fun onStop(initialVelocity: Float): Float { + override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { lastStop = initialVelocity - return if (consumeStop) initialVelocity else 0f + var velocityConsumed = 0f + if (onStopConsumeFlingToScroll) { + velocityConsumed = flingToScroll(initialVelocity, customFlingBehavior) + } + if (onStopConsumeAll) { + velocityConsumed = initialVelocity + } + return velocityConsumed } override fun onCancel() { @@ -178,6 +197,22 @@ class PriorityNestedScrollConnectionTest { } @Test + fun onStopScrollUsingFlingToScroll() = runMonotonicClockTest { + startPriorityModePostScroll() + onStopConsumeFlingToScroll = true + onStopConsumeAll = false + lastScroll = Float.NaN + + val consumed = scrollConnection.onPreFling(available = Velocity(2f, 2f)) + + assertThat(lastStop).isEqualTo(2f) + // flingToScroll should try to scroll the content, customFlingBehavior uses the velocity. + assertThat(lastScroll).isEqualTo(2f) + // customFlingBehavior returns half of the vertical velocity. + assertThat(consumed).isEqualTo(Velocity(0f, 1f)) + } + + @Test fun ifCannotStopOnPreFling_shouldStopOnPostFling() = runTest { startPriorityModePostScroll() canStopOnPreFling = false diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt index 75479ad35725..60b95ad632e1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -35,6 +37,13 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { private var scrimOffset = 0f private var contentHeight = 0f private var isCurrentGestureOverscroll = false + private val customFlingBehavior = + object : FlingBehavior { + override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { + scrollBy(initialVelocity) + return initialVelocity / 2f + } + } private val scrollConnection = NotificationScrimNestedScrollConnection( @@ -51,6 +60,7 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { wasStarted = true isStarted = false }, + flingBehavior = customFlingBehavior, ) @Test |