Merge "Add SceneScope.nestedScrollToScene() modifier" into main
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
index ae7d8f5..216608a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
@@ -3,11 +3,6 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-interface GestureHandler {
- val draggable: DraggableHandler
- val nestedScroll: NestedScrollHandler
-}
-
interface DraggableHandler {
fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1)
fun onDelta(pixels: Float)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
new file mode 100644
index 0000000..658b45f
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 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.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+
+/**
+ * Defines the behavior of the [SceneTransitionLayout] when a scrollable component is scrolled.
+ *
+ * By default, scrollable elements within the scene have priority during the user's gesture and are
+ * not consumed by the [SceneTransitionLayout] unless specifically requested via
+ * [nestedScrollToScene].
+ */
+enum class NestedScrollBehavior(val canStartOnPostFling: Boolean) {
+ /**
+ * During scene transitions, scroll events are consumed by the [SceneTransitionLayout] instead
+ * of the scrollable component.
+ */
+ DuringTransitionBetweenScenes(canStartOnPostFling = false),
+
+ /**
+ * Overscroll will only be used by the [SceneTransitionLayout] to move to the next scene if the
+ * gesture begins at the edge of the scrollable component (so that a scroll in that direction
+ * can no longer be consumed). If the gesture is partially consumed by the scrollable component,
+ * there will be NO overscroll effect between scenes.
+ *
+ * In addition, during scene transitions, scroll events are consumed by the
+ * [SceneTransitionLayout] instead of the scrollable component.
+ */
+ EdgeNoOverscroll(canStartOnPostFling = false),
+
+ /**
+ * Overscroll will only be used by the [SceneTransitionLayout] to move to the next scene if the
+ * gesture begins at the edge of the scrollable component. If the gesture is partially consumed
+ * by the scrollable component, there will be an overscroll effect between scenes.
+ *
+ * In addition, during scene transitions, scroll events are consumed by the
+ * [SceneTransitionLayout] instead of the scrollable component.
+ */
+ EdgeWithOverscroll(canStartOnPostFling = true),
+
+ /**
+ * Any overscroll will be used by the [SceneTransitionLayout] to move to the next scene.
+ *
+ * In addition, during scene transitions, scroll events are consumed by the
+ * [SceneTransitionLayout] instead of the scrollable component.
+ */
+ Always(canStartOnPostFling = true),
+}
+
+internal fun Modifier.nestedScrollToScene(
+ layoutImpl: SceneTransitionLayoutImpl,
+ orientation: Orientation,
+ startBehavior: NestedScrollBehavior,
+ endBehavior: NestedScrollBehavior,
+): Modifier = composed {
+ val connection =
+ remember(layoutImpl, orientation, startBehavior, endBehavior) {
+ scenePriorityNestedScrollConnection(
+ layoutImpl = layoutImpl,
+ orientation = orientation,
+ startBehavior = startBehavior,
+ endBehavior = endBehavior
+ )
+ }
+
+ // Make sure we reset the scroll connection when this modifier is removed from composition
+ DisposableEffect(connection) { onDispose { connection.reset() } }
+
+ nestedScroll(connection = connection)
+}
+
+private fun scenePriorityNestedScrollConnection(
+ layoutImpl: SceneTransitionLayoutImpl,
+ orientation: Orientation,
+ startBehavior: NestedScrollBehavior,
+ endBehavior: NestedScrollBehavior,
+) =
+ SceneNestedScrollHandler(
+ gestureHandler = layoutImpl.gestureHandler(orientation = orientation),
+ startBehavior = startBehavior,
+ endBehavior = endBehavior,
+ )
+ .connection
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 2e50a71..eb5168b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -16,6 +16,7 @@
package com.android.compose.animation.scene
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -68,6 +69,18 @@
return element(layoutImpl, scene, key)
}
+ override fun Modifier.nestedScrollToScene(
+ orientation: Orientation,
+ startBehavior: NestedScrollBehavior,
+ endBehavior: NestedScrollBehavior,
+ ): Modifier =
+ nestedScrollToScene(
+ layoutImpl = layoutImpl,
+ orientation = orientation,
+ startBehavior = startBehavior,
+ endBehavior = endBehavior,
+ )
+
@Composable
override fun <T> animateSharedValueAsState(
value: T,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 7563e27..9a3a0ae 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 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.compose.animation.scene
import android.util.Log
@@ -25,10 +41,8 @@
private val layoutImpl: SceneTransitionLayoutImpl,
internal val orientation: Orientation,
private val coroutineScope: CoroutineScope,
-) : GestureHandler {
- override val draggable: DraggableHandler = SceneDraggableHandler(this)
-
- override val nestedScroll: SceneNestedScrollHandler = SceneNestedScrollHandler(this)
+) {
+ val draggable: DraggableHandler = SceneDraggableHandler(this)
private var transitionState
get() = layoutImpl.state.transitionState
@@ -521,6 +535,8 @@
@VisibleForTesting
class SceneNestedScrollHandler(
private val gestureHandler: SceneGestureHandler,
+ private val startBehavior: NestedScrollBehavior,
+ private val endBehavior: NestedScrollBehavior,
) : NestedScrollHandler {
override val connection: PriorityNestedScrollConnection = nestedScrollConnection()
@@ -543,15 +559,9 @@
}
private fun nestedScrollConnection(): PriorityNestedScrollConnection {
- // The next potential scene is calculated during the canStart
- var nextScene: SceneKey? = null
-
- // This is the scene on which we will have priority during the scroll gesture.
- var priorityScene: SceneKey? = null
-
// If we performed a long gesture before entering priority mode, we would have to avoid
// moving on to the next scene.
- var gestureStartedOnNestedChild = false
+ var canChangeScene = false
val actionUpOrLeft =
Swipe(
@@ -573,51 +583,70 @@
pointerCount = 1,
)
- fun findNextScene(amount: Float): SceneKey? {
+ fun hasNextScene(amount: Float): Boolean {
val fromScene = gestureHandler.currentScene
- return when {
- amount < 0f -> fromScene.userActions[actionUpOrLeft]
- amount > 0f -> fromScene.userActions[actionDownOrRight]
- else -> null
- }
+ val nextScene =
+ when {
+ amount < 0f -> fromScene.userActions[actionUpOrLeft]
+ amount > 0f -> fromScene.userActions[actionDownOrRight]
+ else -> null
+ }
+ return nextScene != null
}
return PriorityNestedScrollConnection(
canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
- gestureStartedOnNestedChild = offsetBeforeStart != Offset.Zero
-
- val canInterceptPreScroll =
- gestureHandler.isDrivingTransition &&
- !gestureStartedOnNestedChild &&
- offsetAvailable.toAmount() != 0f
-
- if (!canInterceptPreScroll) return@PriorityNestedScrollConnection false
-
- nextScene = gestureHandler.swipeTransitionToScene.key
-
- true
+ canChangeScene = offsetBeforeStart == Offset.Zero
+ gestureHandler.isDrivingTransition &&
+ canChangeScene &&
+ offsetAvailable.toAmount() != 0f
},
canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
val amount = offsetAvailable.toAmount()
- if (amount == 0f) return@PriorityNestedScrollConnection false
+ val behavior: NestedScrollBehavior =
+ when {
+ amount > 0 -> startBehavior
+ amount < 0 -> endBehavior
+ else -> return@PriorityNestedScrollConnection false
+ }
- gestureStartedOnNestedChild = offsetBeforeStart != Offset.Zero
- nextScene = findNextScene(amount)
- nextScene != null
+ val isZeroOffset = offsetBeforeStart == Offset.Zero
+
+ when (behavior) {
+ NestedScrollBehavior.DuringTransitionBetweenScenes -> {
+ canChangeScene = false // unused: added for consistency
+ false
+ }
+ NestedScrollBehavior.EdgeNoOverscroll -> {
+ canChangeScene = isZeroOffset
+ isZeroOffset && hasNextScene(amount)
+ }
+ NestedScrollBehavior.EdgeWithOverscroll -> {
+ canChangeScene = isZeroOffset
+ hasNextScene(amount)
+ }
+ NestedScrollBehavior.Always -> {
+ canChangeScene = true
+ hasNextScene(amount)
+ }
+ }
},
canStartPostFling = { velocityAvailable ->
val amount = velocityAvailable.toAmount()
- if (amount == 0f) return@PriorityNestedScrollConnection false
+ val behavior: NestedScrollBehavior =
+ when {
+ amount > 0 -> startBehavior
+ amount < 0 -> endBehavior
+ else -> return@PriorityNestedScrollConnection false
+ }
// We could start an overscroll animation
- gestureStartedOnNestedChild = true
- nextScene = findNextScene(amount)
- nextScene != null
+ canChangeScene = false
+ behavior.canStartOnPostFling && hasNextScene(amount)
},
- canContinueScroll = { priorityScene == gestureHandler.swipeTransitionToScene.key },
+ canContinueScroll = { true },
onStart = {
gestureHandler.gestureWithPriority = this
- priorityScene = nextScene
gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null)
},
onScroll = { offsetAvailable ->
@@ -638,11 +667,9 @@
return@PriorityNestedScrollConnection Velocity.Zero
}
- priorityScene = null
-
gestureHandler.onDragStopped(
velocity = velocityAvailable.toAmount(),
- canChangeScene = !gestureStartedOnNestedChild
+ canChangeScene = canChangeScene
)
// The onDragStopped animation consumes any remaining velocity.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 35d1f90..efdfe7a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -20,7 +20,9 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.platform.LocalDensity
/**
@@ -52,14 +54,16 @@
scenes: SceneTransitionLayoutScope.() -> Unit,
) {
val density = LocalDensity.current
+ val coroutineScope = rememberCoroutineScope()
val layoutImpl = remember {
SceneTransitionLayoutImpl(
- onChangeScene,
- scenes,
- transitions,
- state,
- density,
- edgeDetector,
+ onChangeScene = onChangeScene,
+ builder = scenes,
+ transitions = transitions,
+ state = state,
+ density = density,
+ edgeDetector = edgeDetector,
+ coroutineScope = coroutineScope,
)
}
@@ -120,6 +124,20 @@
fun Modifier.element(key: ElementKey): Modifier
/**
+ * Adds a [NestedScrollConnection] to intercept scroll events not handled by the scrollable
+ * component.
+ *
+ * @param orientation is used to determine if we handle top/bottom or left/right events.
+ * @param startBehavior when we should perform the overscroll animation at the top/left.
+ * @param endBehavior when we should perform the overscroll animation at the bottom/right.
+ */
+ fun Modifier.nestedScrollToScene(
+ orientation: Orientation,
+ startBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoOverscroll,
+ endBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoOverscroll,
+ ): Modifier
+
+ /**
* Create a *movable* element identified by [key].
*
* This creates an element that will be automatically shared when present in multiple scenes and
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 6618eb0..6edd1b6 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -37,6 +37,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
@VisibleForTesting
@@ -47,6 +48,7 @@
internal val state: SceneTransitionLayoutState,
density: Density,
edgeDetector: EdgeDetector,
+ coroutineScope: CoroutineScope,
) {
internal val scenes = SnapshotStateMap<SceneKey, Scene>()
internal val elements = SnapshotStateMap<ElementKey, Element>()
@@ -59,6 +61,9 @@
internal var density: Density by mutableStateOf(density)
internal var edgeDetector by mutableStateOf(edgeDetector)
+ private val horizontalGestureHandler: SceneGestureHandler
+ private val verticalGestureHandler: SceneGestureHandler
+
/**
* The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have
* any scene configured or right before the first measure pass of the layout.
@@ -67,8 +72,30 @@
init {
setScenes(builder)
+
+ // SceneGestureHandler must wait for the scenes to be initialized, in order to access the
+ // current scene (required for SwipeTransition).
+ horizontalGestureHandler =
+ SceneGestureHandler(
+ layoutImpl = this,
+ orientation = Orientation.Horizontal,
+ coroutineScope = coroutineScope,
+ )
+
+ verticalGestureHandler =
+ SceneGestureHandler(
+ layoutImpl = this,
+ orientation = Orientation.Vertical,
+ coroutineScope = coroutineScope,
+ )
}
+ internal fun gestureHandler(orientation: Orientation): SceneGestureHandler =
+ when (orientation) {
+ Orientation.Vertical -> verticalGestureHandler
+ Orientation.Horizontal -> horizontalGestureHandler
+ }
+
internal fun scene(key: SceneKey): Scene {
return scenes[key] ?: error("Scene $key is not configured")
}
@@ -131,18 +158,13 @@
@Composable
internal fun Content(modifier: Modifier) {
- val horizontalGestureHandler =
- rememberSceneGestureHandler(layoutImpl = this, Orientation.Horizontal)
- val verticalGestureHandler =
- rememberSceneGestureHandler(layoutImpl = this, Orientation.Vertical)
-
Box(
modifier
// Handle horizontal and vertical swipes on this layout.
// Note: order here is important and will give a slight priority to the vertical
// swipes.
- .swipeToScene(horizontalGestureHandler)
- .swipeToScene(verticalGestureHandler)
+ .swipeToScene(gestureHandler(Orientation.Horizontal))
+ .swipeToScene(gestureHandler(Orientation.Vertical))
.onSizeChanged { size = it }
) {
LookaheadScope {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index 8980df8..2c78dee 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -17,12 +17,7 @@
package com.android.compose.animation.scene
import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.nestedscroll.nestedScroll
/**
* Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
@@ -43,38 +38,18 @@
}
)
- return nestedScroll(connection = gestureHandler.nestedScroll.connection)
- .multiPointerDraggable(
- orientation = orientation,
- enabled = gestureHandler.isDrivingTransition || canSwipe,
- // Immediately start the drag if this our [transition] is currently animating to a scene
- // (i.e. the user released their input pointer after swiping in this orientation) and
- // the user can't swipe in the other direction.
- startDragImmediately =
- gestureHandler.isDrivingTransition &&
- gestureHandler.isAnimatingOffset &&
- !canOppositeSwipe,
- onDragStarted = gestureHandler.draggable::onDragStarted,
- onDragDelta = gestureHandler.draggable::onDelta,
- onDragStopped = gestureHandler.draggable::onDragStopped,
- )
-}
-
-@Composable
-internal fun rememberSceneGestureHandler(
- layoutImpl: SceneTransitionLayoutImpl,
- orientation: Orientation,
-): SceneGestureHandler {
- val coroutineScope = rememberCoroutineScope()
-
- val gestureHandler =
- remember(layoutImpl, orientation, coroutineScope) {
- SceneGestureHandler(layoutImpl, orientation, coroutineScope)
- }
-
- // Make sure we reset the scroll connection when this handler is removed from composition
- val connection = gestureHandler.nestedScroll.connection
- DisposableEffect(connection) { onDispose { connection.reset() } }
-
- return gestureHandler
+ return multiPointerDraggable(
+ orientation = orientation,
+ enabled = gestureHandler.isDrivingTransition || canSwipe,
+ // Immediately start the drag if this our [transition] is currently animating to a scene
+ // (i.e. the user released their input pointer after swiping in this orientation) and the
+ // user can't swipe in the other direction.
+ startDragImmediately =
+ gestureHandler.isDrivingTransition &&
+ gestureHandler.isAnimatingOffset &&
+ !canOppositeSwipe,
+ onDragStarted = gestureHandler.draggable::onDragStarted,
+ onDragDelta = gestureHandler.draggable::onDelta,
+ onDragStopped = gestureHandler.draggable::onDragStopped,
+ )
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 1eb3392..1e3d011 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 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.compose.animation.scene
import androidx.compose.foundation.gestures.Orientation
@@ -6,12 +22,17 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.NestedScrollBehavior.Always
+import com.android.compose.animation.scene.NestedScrollBehavior.DuringTransitionBetweenScenes
+import com.android.compose.animation.scene.NestedScrollBehavior.EdgeNoOverscroll
+import com.android.compose.animation.scene.NestedScrollBehavior.EdgeWithOverscroll
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
@@ -57,6 +78,7 @@
state = layoutState,
density = Density(1f),
edgeDetector = DefaultEdgeDetector,
+ coroutineScope = coroutineScope,
)
.also { it.size = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) },
orientation = Orientation.Vertical,
@@ -65,7 +87,13 @@
val draggable = sceneGestureHandler.draggable
- val nestedScroll = sceneGestureHandler.nestedScroll.connection
+ fun nestedScrollConnection(nestedScrollBehavior: NestedScrollBehavior) =
+ SceneNestedScrollHandler(
+ gestureHandler = sceneGestureHandler,
+ startBehavior = nestedScrollBehavior,
+ endBehavior = nestedScrollBehavior,
+ )
+ .connection
val velocityThreshold = sceneGestureHandler.velocityThreshold
@@ -194,13 +222,15 @@
}
@Test
- fun onInitialPreScroll_doNotChangeState() = runGestureTest {
+ fun onInitialPreScroll_EdgeWithOverscroll_doNotChangeState() = runGestureTest {
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag)
assertScene(currentScene = SceneA, isIdle = true)
}
@Test
- fun onPostScrollWithNothingAvailable_doNotChangeState() = runGestureTest {
+ fun onPostScrollWithNothingAvailable_EdgeWithOverscroll_doNotChangeState() = runGestureTest {
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
val consumed =
nestedScroll.onPostScroll(
consumed = Offset.Zero,
@@ -214,6 +244,7 @@
@Test
fun onPostScrollWithSomethingAvailable_startSceneTransition() = runGestureTest {
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
val consumed =
nestedScroll.onPostScroll(
consumed = Offset.Zero,
@@ -227,14 +258,14 @@
assertThat(consumed).isEqualTo(offsetY10)
}
- private fun TestGestureScope.nestedScrollEvents(
+ private fun NestedScrollConnection.scroll(
available: Offset,
consumedByScroll: Offset = Offset.Zero,
) {
val consumedByPreScroll =
- nestedScroll.onPreScroll(available = available, source = NestedScrollSource.Drag)
+ onPreScroll(available = available, source = NestedScrollSource.Drag)
val consumed = consumedByPreScroll + consumedByScroll
- nestedScroll.onPostScroll(
+ onPostScroll(
consumed = consumed,
available = available - consumed,
source = NestedScrollSource.Drag
@@ -243,7 +274,8 @@
@Test
fun afterSceneTransitionIsStarted_interceptPreScrollEvents() = runGestureTest {
- nestedScrollEvents(available = offsetY10)
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
+ nestedScroll.scroll(available = offsetY10)
assertScene(currentScene = SceneA, isIdle = false)
val transition = transitionState as Transition
@@ -262,14 +294,15 @@
)
assertThat(transition.progress).isEqualTo(0.2f)
- nestedScrollEvents(available = offsetY10)
+ nestedScroll.scroll(available = offsetY10)
assertThat(transition.progress).isEqualTo(0.3f)
assertScene(currentScene = SceneA, isIdle = false)
}
@Test
fun onPreFling_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
- nestedScrollEvents(available = offsetY10)
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
+ nestedScroll.scroll(available = offsetY10)
assertScene(currentScene = SceneA, isIdle = false)
nestedScroll.onPreFling(available = Velocity.Zero)
@@ -280,12 +313,28 @@
assertScene(currentScene = SceneA, isIdle = true)
}
- @Test
- fun onPreFling_velocityAtLeastThreshold_goToNextScene() = runGestureTest {
- nestedScrollEvents(available = offsetY10)
- assertScene(currentScene = SceneA, isIdle = false)
+ private suspend fun TestGestureScope.flingAfterScroll(
+ use: NestedScrollBehavior,
+ idleAfterScroll: Boolean,
+ ) {
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = use)
+ nestedScroll.scroll(available = offsetY10)
+ assertScene(currentScene = SceneA, isIdle = idleAfterScroll)
nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
+ }
+
+ @Test
+ fun flingAfterScroll_DuringTransitionBetweenScenes_doNothing() = runGestureTest {
+ flingAfterScroll(use = DuringTransitionBetweenScenes, idleAfterScroll = true)
+
+ assertScene(currentScene = SceneA, isIdle = true)
+ }
+
+ @Test
+ fun flingAfterScroll_EdgeNoOverscroll_goToNextScene() = runGestureTest {
+ flingAfterScroll(use = EdgeNoOverscroll, idleAfterScroll = false)
+
assertScene(currentScene = SceneC, isIdle = false)
// wait for the stop animation
@@ -294,16 +343,61 @@
}
@Test
- fun scrollStartedInScene_doOverscrollAnimation() = runGestureTest {
- // we started the scroll in the scene
- nestedScrollEvents(available = offsetY10, consumedByScroll = offsetY10)
+ fun flingAfterScroll_EdgeWithOverscroll_goToNextScene() = runGestureTest {
+ flingAfterScroll(use = EdgeWithOverscroll, idleAfterScroll = false)
- // now we can intercept the scroll events
- nestedScrollEvents(available = offsetY10)
- assertScene(currentScene = SceneA, isIdle = false)
+ assertScene(currentScene = SceneC, isIdle = false)
+
+ // wait for the stop animation
+ advanceUntilIdle()
+ assertScene(currentScene = SceneC, isIdle = true)
+ }
+
+ @Test
+ fun flingAfterScroll_Always_goToNextScene() = runGestureTest {
+ flingAfterScroll(use = Always, idleAfterScroll = false)
+
+ assertScene(currentScene = SceneC, isIdle = false)
+
+ // wait for the stop animation
+ advanceUntilIdle()
+ assertScene(currentScene = SceneC, isIdle = true)
+ }
+
+ /** we started the scroll in the scene, then fling with the velocityThreshold */
+ private suspend fun TestGestureScope.flingAfterScrollStartedInScene(
+ use: NestedScrollBehavior,
+ idleAfterScroll: Boolean,
+ ) {
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = use)
+ // scroll consumed in child
+ nestedScroll.scroll(available = offsetY10, consumedByScroll = offsetY10)
+
+ // scroll offsetY10 is all available for parents
+ nestedScroll.scroll(available = offsetY10)
+ assertScene(currentScene = SceneA, isIdle = idleAfterScroll)
nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
- // should start an overscroll animation (the gesture started in the scene)
+ }
+
+ @Test
+ fun flingAfterScrollStartedInScene_DuringTransitionBetweenScenes_doNothing() = runGestureTest {
+ flingAfterScrollStartedInScene(use = DuringTransitionBetweenScenes, idleAfterScroll = true)
+
+ assertScene(currentScene = SceneA, isIdle = true)
+ }
+
+ @Test
+ fun flingAfterScrollStartedInScene_EdgeNoOverscroll_doNothing() = runGestureTest {
+ flingAfterScrollStartedInScene(use = EdgeNoOverscroll, idleAfterScroll = true)
+
+ assertScene(currentScene = SceneA, isIdle = true)
+ }
+
+ @Test
+ fun flingAfterScrollStartedInScene_EdgeWithOverscroll_doOverscrollAnimation() = runGestureTest {
+ flingAfterScrollStartedInScene(use = EdgeWithOverscroll, idleAfterScroll = false)
+
assertScene(currentScene = SceneA, isIdle = false)
// wait for the stop animation
@@ -312,6 +406,17 @@
}
@Test
+ fun flingAfterScrollStartedInScene_Always_goToNextScene() = runGestureTest {
+ flingAfterScrollStartedInScene(use = Always, idleAfterScroll = false)
+
+ assertScene(currentScene = SceneC, isIdle = false)
+
+ // wait for the stop animation
+ advanceUntilIdle()
+ assertScene(currentScene = SceneC, isIdle = true)
+ }
+
+ @Test
fun beforeDraggableStart_drag_shouldBeIgnored() = runGestureTest {
draggable.onDelta(deltaInPixels10)
assertScene(currentScene = SceneA, isIdle = true)
@@ -324,12 +429,14 @@
@Test
fun beforeNestedScrollStart_stop_shouldBeIgnored() = runGestureTest {
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
nestedScroll.onPreFling(Velocity(0f, velocityThreshold))
assertScene(currentScene = SceneA, isIdle = true)
}
@Test
fun startNestedScrollWhileDragging() = runGestureTest {
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = Always)
draggable.onDragStarted(Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
val transition = transitionState as Transition
@@ -338,17 +445,17 @@
assertThat(transition.progress).isEqualTo(0.1f)
// now we can intercept the scroll events
- nestedScrollEvents(available = offsetY10)
+ nestedScroll.scroll(available = offsetY10)
assertThat(transition.progress).isEqualTo(0.2f)
// this should be ignored, we are scrolling now!
draggable.onDragStopped(velocityThreshold)
assertScene(currentScene = SceneA, isIdle = false)
- nestedScrollEvents(available = offsetY10)
+ nestedScroll.scroll(available = offsetY10)
assertThat(transition.progress).isEqualTo(0.3f)
- nestedScrollEvents(available = offsetY10)
+ nestedScroll.scroll(available = offsetY10)
assertThat(transition.progress).isEqualTo(0.4f)
nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))