Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +52 −0 Original line number Diff line number Diff line Loading @@ -428,6 +428,58 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol return upOrLeftResult to downOrRightResult } /** * Finds the best matching [UserActionResult] for the given [swipe] within this [Content]. * Prioritizes actions with matching [Swipe.Resolved.fromSource]. * * @param swipe The swipe to match against. * @return The best matching [UserActionResult], or `null` if no match is found. */ private fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { if (!areSwipesAllowed()) { return null } var bestPoints = Int.MIN_VALUE var bestMatch: UserActionResult? = null userActions.forEach { (actionSwipe, actionResult) -> if ( actionSwipe !is Swipe.Resolved || // The direction must match. actionSwipe.direction != swipe.direction || // The number of pointers down must match. actionSwipe.pointerCount != swipe.pointerCount || // The action requires a specific fromSource. (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) || // The action requires a specific pointerType. (actionSwipe.pointersType != null && actionSwipe.pointersType != swipe.pointersType) ) { // This action is not eligible. return@forEach } val sameFromSource = actionSwipe.fromSource == swipe.fromSource val samePointerType = actionSwipe.pointersType == swipe.pointersType // Prioritize actions with a perfect match. if (sameFromSource && samePointerType) { return actionResult } var points = 0 if (sameFromSource) points++ if (samePointerType) points++ // Otherwise, keep track of the best eligible action. if (points > bestPoints) { bestPoints = points bestMatch = actionResult } } return bestMatch } /** * Update the swipes results. * Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +37 −63 Original line number Diff line number Diff line Loading @@ -37,11 +37,7 @@ internal fun Modifier.swipeToScene( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, ): Modifier { return if (draggableHandler.enabled()) { this.then(SwipeToSceneElement(draggableHandler, swipeDetector)) } else { this } return then(SwipeToSceneElement(draggableHandler, swipeDetector, draggableHandler.enabled())) } private fun DraggableHandlerImpl.enabled(): Boolean { Loading @@ -61,83 +57,61 @@ internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean { return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation } } /** * Finds the best matching [UserActionResult] for the given [swipe] within this [Content]. * Prioritizes actions with matching [Swipe.Resolved.fromSource]. * * @param swipe The swipe to match against. * @return The best matching [UserActionResult], or `null` if no match is found. */ internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { if (!areSwipesAllowed()) { return null } var bestPoints = Int.MIN_VALUE var bestMatch: UserActionResult? = null userActions.forEach { (actionSwipe, actionResult) -> if ( actionSwipe !is Swipe.Resolved || // The direction must match. actionSwipe.direction != swipe.direction || // The number of pointers down must match. actionSwipe.pointerCount != swipe.pointerCount || // The action requires a specific fromSource. (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) || // The action requires a specific pointerType. (actionSwipe.pointersType != null && actionSwipe.pointersType != swipe.pointersType) ) { // This action is not eligible. return@forEach } val sameFromSource = actionSwipe.fromSource == swipe.fromSource val samePointerType = actionSwipe.pointersType == swipe.pointersType // Prioritize actions with a perfect match. if (sameFromSource && samePointerType) { return actionResult } var points = 0 if (sameFromSource) points++ if (samePointerType) points++ // Otherwise, keep track of the best eligible action. if (points > bestPoints) { bestPoints = points bestMatch = actionResult } } return bestMatch } private data class SwipeToSceneElement( val draggableHandler: DraggableHandlerImpl, val swipeDetector: SwipeDetector, val enabled: Boolean, ) : ModifierNodeElement<SwipeToSceneRootNode>() { override fun create(): SwipeToSceneRootNode = SwipeToSceneRootNode(draggableHandler, swipeDetector) SwipeToSceneRootNode(draggableHandler, swipeDetector, enabled) override fun update(node: SwipeToSceneRootNode) { node.update(draggableHandler, swipeDetector) node.update(draggableHandler, swipeDetector, enabled) } } private class SwipeToSceneRootNode( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, enabled: Boolean, ) : DelegatingNode() { private var delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) private var delegateNode = if (enabled) create(draggableHandler, swipeDetector) else null fun update( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, enabled: Boolean, ) { // Disabled. if (!enabled) { delegateNode?.let { undelegate(it) } delegateNode = null return } fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) { if (draggableHandler == delegateNode.draggableHandler) { // Disabled => Enabled. val nullableDelegate = delegateNode if (nullableDelegate == null) { delegateNode = create(draggableHandler, swipeDetector) return } // Enabled => Enabled (update). if (draggableHandler == nullableDelegate.draggableHandler) { // Simple update, just update the swipe detector directly and keep the node. delegateNode.swipeDetector = swipeDetector nullableDelegate.swipeDetector = swipeDetector } else { // The draggableHandler changed, force recreate the underlying SwipeToSceneNode. undelegate(delegateNode) delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) undelegate(nullableDelegate) delegateNode = create(draggableHandler, swipeDetector) } } private fun create( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, ): SwipeToSceneNode { return delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) } } Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +41 −0 Original line number Diff line number Diff line Loading @@ -936,4 +936,45 @@ class SwipeToSceneTest { assertThat(state.transitionState).isIdle() assertThat(state.transitionState).hasCurrentScene(SceneC) } @Test fun swipeToSceneNodeIsKeptWhenDisabled() { var hasHorizontalActions by mutableStateOf(false) val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } var touchSlop = 0f rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout(state) { scene( SceneA, userActions = buildList { add(Swipe.Down to SceneB) if (hasHorizontalActions) { add(Swipe.Left to SceneC) } } .toMap(), ) { Box(Modifier.fillMaxSize()) } scene(SceneB) { Box(Modifier.fillMaxSize()) } } } // Swipe down to start a transition to B. rule.onRoot().performTouchInput { down(middle) moveBy(Offset(0f, touchSlop)) } assertThat(state.transitionState).isSceneTransition() // Add new horizontal user actions. This should not stop the current transition, even if a // new horizontal Modifier.swipeToScene() handler is introduced where the vertical one was. hasHorizontalActions = true rule.waitForIdle() assertThat(state.transitionState).isSceneTransition() } } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +52 −0 Original line number Diff line number Diff line Loading @@ -428,6 +428,58 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol return upOrLeftResult to downOrRightResult } /** * Finds the best matching [UserActionResult] for the given [swipe] within this [Content]. * Prioritizes actions with matching [Swipe.Resolved.fromSource]. * * @param swipe The swipe to match against. * @return The best matching [UserActionResult], or `null` if no match is found. */ private fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { if (!areSwipesAllowed()) { return null } var bestPoints = Int.MIN_VALUE var bestMatch: UserActionResult? = null userActions.forEach { (actionSwipe, actionResult) -> if ( actionSwipe !is Swipe.Resolved || // The direction must match. actionSwipe.direction != swipe.direction || // The number of pointers down must match. actionSwipe.pointerCount != swipe.pointerCount || // The action requires a specific fromSource. (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) || // The action requires a specific pointerType. (actionSwipe.pointersType != null && actionSwipe.pointersType != swipe.pointersType) ) { // This action is not eligible. return@forEach } val sameFromSource = actionSwipe.fromSource == swipe.fromSource val samePointerType = actionSwipe.pointersType == swipe.pointersType // Prioritize actions with a perfect match. if (sameFromSource && samePointerType) { return actionResult } var points = 0 if (sameFromSource) points++ if (samePointerType) points++ // Otherwise, keep track of the best eligible action. if (points > bestPoints) { bestPoints = points bestMatch = actionResult } } return bestMatch } /** * Update the swipes results. * Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +37 −63 Original line number Diff line number Diff line Loading @@ -37,11 +37,7 @@ internal fun Modifier.swipeToScene( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, ): Modifier { return if (draggableHandler.enabled()) { this.then(SwipeToSceneElement(draggableHandler, swipeDetector)) } else { this } return then(SwipeToSceneElement(draggableHandler, swipeDetector, draggableHandler.enabled())) } private fun DraggableHandlerImpl.enabled(): Boolean { Loading @@ -61,83 +57,61 @@ internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean { return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation } } /** * Finds the best matching [UserActionResult] for the given [swipe] within this [Content]. * Prioritizes actions with matching [Swipe.Resolved.fromSource]. * * @param swipe The swipe to match against. * @return The best matching [UserActionResult], or `null` if no match is found. */ internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { if (!areSwipesAllowed()) { return null } var bestPoints = Int.MIN_VALUE var bestMatch: UserActionResult? = null userActions.forEach { (actionSwipe, actionResult) -> if ( actionSwipe !is Swipe.Resolved || // The direction must match. actionSwipe.direction != swipe.direction || // The number of pointers down must match. actionSwipe.pointerCount != swipe.pointerCount || // The action requires a specific fromSource. (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) || // The action requires a specific pointerType. (actionSwipe.pointersType != null && actionSwipe.pointersType != swipe.pointersType) ) { // This action is not eligible. return@forEach } val sameFromSource = actionSwipe.fromSource == swipe.fromSource val samePointerType = actionSwipe.pointersType == swipe.pointersType // Prioritize actions with a perfect match. if (sameFromSource && samePointerType) { return actionResult } var points = 0 if (sameFromSource) points++ if (samePointerType) points++ // Otherwise, keep track of the best eligible action. if (points > bestPoints) { bestPoints = points bestMatch = actionResult } } return bestMatch } private data class SwipeToSceneElement( val draggableHandler: DraggableHandlerImpl, val swipeDetector: SwipeDetector, val enabled: Boolean, ) : ModifierNodeElement<SwipeToSceneRootNode>() { override fun create(): SwipeToSceneRootNode = SwipeToSceneRootNode(draggableHandler, swipeDetector) SwipeToSceneRootNode(draggableHandler, swipeDetector, enabled) override fun update(node: SwipeToSceneRootNode) { node.update(draggableHandler, swipeDetector) node.update(draggableHandler, swipeDetector, enabled) } } private class SwipeToSceneRootNode( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, enabled: Boolean, ) : DelegatingNode() { private var delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) private var delegateNode = if (enabled) create(draggableHandler, swipeDetector) else null fun update( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, enabled: Boolean, ) { // Disabled. if (!enabled) { delegateNode?.let { undelegate(it) } delegateNode = null return } fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) { if (draggableHandler == delegateNode.draggableHandler) { // Disabled => Enabled. val nullableDelegate = delegateNode if (nullableDelegate == null) { delegateNode = create(draggableHandler, swipeDetector) return } // Enabled => Enabled (update). if (draggableHandler == nullableDelegate.draggableHandler) { // Simple update, just update the swipe detector directly and keep the node. delegateNode.swipeDetector = swipeDetector nullableDelegate.swipeDetector = swipeDetector } else { // The draggableHandler changed, force recreate the underlying SwipeToSceneNode. undelegate(delegateNode) delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) undelegate(nullableDelegate) delegateNode = create(draggableHandler, swipeDetector) } } private fun create( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, ): SwipeToSceneNode { return delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) } } Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +41 −0 Original line number Diff line number Diff line Loading @@ -936,4 +936,45 @@ class SwipeToSceneTest { assertThat(state.transitionState).isIdle() assertThat(state.transitionState).hasCurrentScene(SceneC) } @Test fun swipeToSceneNodeIsKeptWhenDisabled() { var hasHorizontalActions by mutableStateOf(false) val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } var touchSlop = 0f rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout(state) { scene( SceneA, userActions = buildList { add(Swipe.Down to SceneB) if (hasHorizontalActions) { add(Swipe.Left to SceneC) } } .toMap(), ) { Box(Modifier.fillMaxSize()) } scene(SceneB) { Box(Modifier.fillMaxSize()) } } } // Swipe down to start a transition to B. rule.onRoot().performTouchInput { down(middle) moveBy(Offset(0f, touchSlop)) } assertThat(state.transitionState).isSceneTransition() // Add new horizontal user actions. This should not stop the current transition, even if a // new horizontal Modifier.swipeToScene() handler is introduced where the vertical one was. hasHorizontalActions = true rule.waitForIdle() assertThat(state.transitionState).isSceneTransition() } }