1 /*
2 * Copyright 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.compose.animation.scene
18
19 import androidx.compose.foundation.gestures.Orientation
20 import androidx.compose.runtime.Stable
21 import androidx.compose.ui.Modifier
22 import androidx.compose.ui.geometry.Offset
23 import androidx.compose.ui.input.pointer.PointerEvent
24 import androidx.compose.ui.input.pointer.PointerEventPass
25 import androidx.compose.ui.node.DelegatingNode
26 import androidx.compose.ui.node.ModifierNodeElement
27 import androidx.compose.ui.node.PointerInputModifierNode
28 import androidx.compose.ui.unit.IntSize
29
30 /**
31 * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
32 */
33 @Stable
swipeToScenenull34 internal fun Modifier.swipeToScene(
35 draggableHandler: DraggableHandlerImpl,
36 swipeDetector: SwipeDetector
37 ): Modifier {
38 return this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
39 }
40
41 private data class SwipeToSceneElement(
42 val draggableHandler: DraggableHandlerImpl,
43 val swipeDetector: SwipeDetector
44 ) : ModifierNodeElement<SwipeToSceneNode>() {
createnull45 override fun create(): SwipeToSceneNode = SwipeToSceneNode(draggableHandler, swipeDetector)
46
47 override fun update(node: SwipeToSceneNode) {
48 node.draggableHandler = draggableHandler
49 }
50 }
51
52 private class SwipeToSceneNode(
53 draggableHandler: DraggableHandlerImpl,
54 swipeDetector: SwipeDetector,
55 ) : DelegatingNode(), PointerInputModifierNode {
56 private val delegate =
57 delegate(
58 MultiPointerDraggableNode(
59 orientation = draggableHandler.orientation,
60 enabled = ::enabled,
61 startDragImmediately = ::startDragImmediately,
62 onDragStarted = draggableHandler::onDragStarted,
63 swipeDetector = swipeDetector,
64 )
65 )
66
67 private var _draggableHandler = draggableHandler
68 var draggableHandler: DraggableHandlerImpl
69 get() = _draggableHandler
70 set(value) {
71 if (_draggableHandler != value) {
72 _draggableHandler = value
73
74 // Make sure to update the delegate orientation. Note that this will automatically
75 // reset the underlying pointer input handler, so previous gestures will be
76 // cancelled.
77 delegate.orientation = value.orientation
78 }
79 }
80
onPointerEventnull81 override fun onPointerEvent(
82 pointerEvent: PointerEvent,
83 pass: PointerEventPass,
84 bounds: IntSize,
85 ) = delegate.onPointerEvent(pointerEvent, pass, bounds)
86
87 override fun onCancelPointerInput() = delegate.onCancelPointerInput()
88
89 private fun enabled(): Boolean {
90 return draggableHandler.isDrivingTransition ||
91 currentScene().shouldEnableSwipes(delegate.orientation)
92 }
93
currentScenenull94 private fun currentScene(): Scene {
95 val layoutImpl = draggableHandler.layoutImpl
96 return layoutImpl.scene(layoutImpl.state.transitionState.currentScene)
97 }
98
99 /** Whether swipe should be enabled in the given [orientation]. */
Scenenull100 private fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean {
101 return userActions.keys.any { it is Swipe && it.direction.orientation == orientation }
102 }
103
startDragImmediatelynull104 private fun startDragImmediately(startedPosition: Offset): Boolean {
105 // Immediately start the drag if the user can't swipe in the other direction and the gesture
106 // handler can intercept it.
107 return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(startedPosition)
108 }
109
canOppositeSwipenull110 private fun canOppositeSwipe(): Boolean {
111 val oppositeOrientation =
112 when (draggableHandler.orientation) {
113 Orientation.Vertical -> Orientation.Horizontal
114 Orientation.Horizontal -> Orientation.Vertical
115 }
116 return currentScene().shouldEnableSwipes(oppositeOrientation)
117 }
118 }
119