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