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