1 /*
2  * Copyright (C) 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.ui.Modifier
21 import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
22 import androidx.compose.ui.node.DelegatableNode
23 import androidx.compose.ui.node.DelegatingNode
24 import androidx.compose.ui.node.ModifierNodeElement
25 import androidx.compose.ui.platform.InspectorInfo
26 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
27 
28 /**
29  * Defines the behavior of the [SceneTransitionLayout] when a scrollable component is scrolled.
30  *
31  * By default, scrollable elements within the scene have priority during the user's gesture and are
32  * not consumed by the [SceneTransitionLayout] unless specifically requested via
33  * [nestedScrollToScene].
34  */
35 enum class NestedScrollBehavior(val canStartOnPostFling: Boolean) {
36     /**
37      * During scene transitions, if we are within
38      * [SceneTransitionLayoutImpl.transitionInterceptionThreshold], the [SceneTransitionLayout]
39      * consumes scroll events instead of the scrollable component.
40      */
41     DuringTransitionBetweenScenes(canStartOnPostFling = false),
42 
43     /**
44      * Overscroll will only be used by the [SceneTransitionLayout] to move to the next scene if the
45      * gesture begins at the edge of the scrollable component (so that a scroll in that direction
46      * can no longer be consumed). If the gesture is partially consumed by the scrollable component,
47      * there will be NO preview of the next scene.
48      *
49      * In addition, during scene transitions, scroll events are consumed by the
50      * [SceneTransitionLayout] instead of the scrollable component.
51      */
52     EdgeNoPreview(canStartOnPostFling = false),
53 
54     /**
55      * Overscroll will only be used by the [SceneTransitionLayout] to move to the next scene if the
56      * gesture begins at the edge of the scrollable component. If the gesture is partially consumed
57      * by the scrollable component, there will be a preview of the next scene.
58      *
59      * In addition, during scene transitions, scroll events are consumed by the
60      * [SceneTransitionLayout] instead of the scrollable component.
61      */
62     EdgeWithPreview(canStartOnPostFling = true),
63 
64     /**
65      * Any overscroll will be used by the [SceneTransitionLayout] to move to the next scene.
66      *
67      * In addition, during scene transitions, scroll events are consumed by the
68      * [SceneTransitionLayout] instead of the scrollable component.
69      */
70     EdgeAlways(canStartOnPostFling = true),
71 }
72 
nestedScrollToScenenull73 internal fun Modifier.nestedScrollToScene(
74     layoutImpl: SceneTransitionLayoutImpl,
75     orientation: Orientation,
76     topOrLeftBehavior: NestedScrollBehavior,
77     bottomOrRightBehavior: NestedScrollBehavior,
78     isExternalOverscrollGesture: () -> Boolean,
79 ) =
80     this then
81         NestedScrollToSceneElement(
82             layoutImpl = layoutImpl,
83             orientation = orientation,
84             topOrLeftBehavior = topOrLeftBehavior,
85             bottomOrRightBehavior = bottomOrRightBehavior,
86             isExternalOverscrollGesture = isExternalOverscrollGesture,
87         )
88 
89 private data class NestedScrollToSceneElement(
90     private val layoutImpl: SceneTransitionLayoutImpl,
91     private val orientation: Orientation,
92     private val topOrLeftBehavior: NestedScrollBehavior,
93     private val bottomOrRightBehavior: NestedScrollBehavior,
94     private val isExternalOverscrollGesture: () -> Boolean,
95 ) : ModifierNodeElement<NestedScrollToSceneNode>() {
96     override fun create() =
97         NestedScrollToSceneNode(
98             layoutImpl = layoutImpl,
99             orientation = orientation,
100             topOrLeftBehavior = topOrLeftBehavior,
101             bottomOrRightBehavior = bottomOrRightBehavior,
102             isExternalOverscrollGesture = isExternalOverscrollGesture,
103         )
104 
105     override fun update(node: NestedScrollToSceneNode) {
106         node.update(
107             layoutImpl = layoutImpl,
108             orientation = orientation,
109             topOrLeftBehavior = topOrLeftBehavior,
110             bottomOrRightBehavior = bottomOrRightBehavior,
111             isExternalOverscrollGesture = isExternalOverscrollGesture,
112         )
113     }
114 
115     override fun InspectorInfo.inspectableProperties() {
116         name = "nestedScrollToScene"
117         properties["layoutImpl"] = layoutImpl
118         properties["orientation"] = orientation
119         properties["topOrLeftBehavior"] = topOrLeftBehavior
120         properties["bottomOrRightBehavior"] = bottomOrRightBehavior
121     }
122 }
123 
124 private class NestedScrollToSceneNode(
125     layoutImpl: SceneTransitionLayoutImpl,
126     orientation: Orientation,
127     topOrLeftBehavior: NestedScrollBehavior,
128     bottomOrRightBehavior: NestedScrollBehavior,
129     isExternalOverscrollGesture: () -> Boolean,
130 ) : DelegatingNode() {
131     private var priorityNestedScrollConnection: PriorityNestedScrollConnection =
132         scenePriorityNestedScrollConnection(
133             layoutImpl = layoutImpl,
134             orientation = orientation,
135             topOrLeftBehavior = topOrLeftBehavior,
136             bottomOrRightBehavior = bottomOrRightBehavior,
137             isExternalOverscrollGesture = isExternalOverscrollGesture,
138         )
139 
140     private var nestedScrollNode: DelegatableNode =
141         nestedScrollModifierNode(
142             connection = priorityNestedScrollConnection,
143             dispatcher = null,
144         )
145 
onAttachnull146     override fun onAttach() {
147         delegate(nestedScrollNode)
148     }
149 
onDetachnull150     override fun onDetach() {
151         // Make sure we reset the scroll connection when this modifier is removed from composition
152         priorityNestedScrollConnection.reset()
153     }
154 
updatenull155     fun update(
156         layoutImpl: SceneTransitionLayoutImpl,
157         orientation: Orientation,
158         topOrLeftBehavior: NestedScrollBehavior,
159         bottomOrRightBehavior: NestedScrollBehavior,
160         isExternalOverscrollGesture: () -> Boolean,
161     ) {
162         // Clean up the old nested scroll connection
163         priorityNestedScrollConnection.reset()
164         undelegate(nestedScrollNode)
165 
166         // Create a new nested scroll connection
167         priorityNestedScrollConnection =
168             scenePriorityNestedScrollConnection(
169                 layoutImpl = layoutImpl,
170                 orientation = orientation,
171                 topOrLeftBehavior = topOrLeftBehavior,
172                 bottomOrRightBehavior = bottomOrRightBehavior,
173                 isExternalOverscrollGesture = isExternalOverscrollGesture,
174             )
175         nestedScrollNode =
176             nestedScrollModifierNode(
177                 connection = priorityNestedScrollConnection,
178                 dispatcher = null,
179             )
180         delegate(nestedScrollNode)
181     }
182 }
183 
scenePriorityNestedScrollConnectionnull184 private fun scenePriorityNestedScrollConnection(
185     layoutImpl: SceneTransitionLayoutImpl,
186     orientation: Orientation,
187     topOrLeftBehavior: NestedScrollBehavior,
188     bottomOrRightBehavior: NestedScrollBehavior,
189     isExternalOverscrollGesture: () -> Boolean,
190 ) =
191     NestedScrollHandlerImpl(
192             layoutImpl = layoutImpl,
193             orientation = orientation,
194             topOrLeftBehavior = topOrLeftBehavior,
195             bottomOrRightBehavior = bottomOrRightBehavior,
196             isExternalOverscrollGesture = isExternalOverscrollGesture,
197         )
198         .connection
199