1 /*
<lambda>null2 * 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.nestedscroll
18
19 import androidx.compose.foundation.gestures.Orientation
20 import androidx.compose.ui.geometry.Offset
21 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
22 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
23 import androidx.compose.ui.unit.Velocity
24 import com.android.compose.ui.util.SpaceVectorConverter
25 import kotlin.math.sign
26
27 /**
28 * This [NestedScrollConnection] waits for a child to scroll ([onPreScroll] or [onPostScroll]), and
29 * then decides (via [canStartPreScroll] or [canStartPostScroll]) if it should take over scrolling.
30 * If it does, it will scroll before its children, until [canContinueScroll] allows it.
31 *
32 * Note: Call [reset] before destroying this object to make sure you always get a call to [onStop]
33 * after [onStart].
34 *
35 * @sample com.android.compose.animation.scene.rememberSwipeToSceneNestedScrollConnection
36 */
37 class PriorityNestedScrollConnection(
38 private val canStartPreScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
39 private val canStartPostScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
40 private val canStartPostFling: (velocityAvailable: Velocity) -> Boolean,
41 private val canContinueScroll: () -> Boolean,
42 private val canScrollOnFling: Boolean,
43 private val onStart: (offsetAvailable: Offset) -> Unit,
44 private val onScroll: (offsetAvailable: Offset) -> Offset,
45 private val onStop: (velocityAvailable: Velocity) -> Velocity,
46 ) : NestedScrollConnection {
47
48 /** In priority mode [onPreScroll] events are first consumed by the parent, via [onScroll]. */
49 private var isPriorityMode = false
50
51 private var offsetScrolledBeforePriorityMode = Offset.Zero
52
53 override fun onPostScroll(
54 consumed: Offset,
55 available: Offset,
56 source: NestedScrollSource,
57 ): Offset {
58 // The offset before the start takes into account the up and down movements, starting from
59 // the beginning or from the last fling gesture.
60 val offsetBeforeStart = offsetScrolledBeforePriorityMode - available
61
62 if (
63 isPriorityMode ||
64 (source == NestedScrollSource.Fling && !canScrollOnFling) ||
65 !canStartPostScroll(available, offsetBeforeStart)
66 ) {
67 // The priority mode cannot start so we won't consume the available offset.
68 return Offset.Zero
69 }
70
71 return onPriorityStart(available)
72 }
73
74 override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
75 if (!isPriorityMode) {
76 if (source != NestedScrollSource.Fling || canScrollOnFling) {
77 if (canStartPreScroll(available, offsetScrolledBeforePriorityMode)) {
78 return onPriorityStart(available)
79 }
80 // We want to track the amount of offset consumed before entering priority mode
81 offsetScrolledBeforePriorityMode += available
82 }
83
84 return Offset.Zero
85 }
86
87 if (!canContinueScroll()) {
88 // Step 3a: We have lost priority and we no longer need to intercept scroll events.
89 onPriorityStop(velocity = Velocity.Zero)
90
91 // We've just reset offsetScrolledBeforePriorityMode to Offset.Zero
92 // We want to track the amount of offset consumed before entering priority mode
93 offsetScrolledBeforePriorityMode += available
94
95 return Offset.Zero
96 }
97
98 // Step 2: We have the priority and can consume the scroll events.
99 return onScroll(available)
100 }
101
102 override suspend fun onPreFling(available: Velocity): Velocity {
103 if (isPriorityMode && canScrollOnFling) {
104 // We don't want to consume the velocity, we prefer to continue receiving scroll events.
105 return Velocity.Zero
106 }
107 // Step 3b: The finger is lifted, we can stop intercepting scroll events and use the speed
108 // of the fling gesture.
109 return onPriorityStop(velocity = available)
110 }
111
112 override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
113 if (isPriorityMode) {
114 return onPriorityStop(velocity = available)
115 }
116
117 if (!canStartPostFling(available)) {
118 return Velocity.Zero
119 }
120
121 // The offset passed to onPriorityStart() must be != 0f, so we create a small offset of 1px
122 // given the available velocity.
123 // TODO(b/291053278): Remove canStartPostFling() and instead make it possible to define the
124 // overscroll behavior on the Scene level.
125 val smallOffset = Offset(available.x.sign, available.y.sign)
126 onPriorityStart(available = smallOffset)
127
128 // This is the last event of a scroll gesture.
129 return onPriorityStop(available)
130 }
131
132 /** Method to call before destroying the object or to reset the initial state. */
133 fun reset() {
134 // Step 3c: To ensure that an onStop is always called for every onStart.
135 onPriorityStop(velocity = Velocity.Zero)
136 }
137
138 private fun onPriorityStart(available: Offset): Offset {
139 if (isPriorityMode) {
140 error("This should never happen, onPriorityStart() was called when isPriorityMode")
141 }
142
143 // Step 1: It's our turn! We start capturing scroll events when one of our children has an
144 // available offset following a scroll event.
145 isPriorityMode = true
146
147 // Note: onStop will be called if we cannot continue to scroll (step 3a), or the finger is
148 // lifted (step 3b), or this object has been destroyed (step 3c).
149 onStart(available)
150
151 return onScroll(available)
152 }
153
154 private fun onPriorityStop(velocity: Velocity): Velocity {
155 // We can restart tracking the consumed offsets from scratch.
156 offsetScrolledBeforePriorityMode = Offset.Zero
157
158 if (!isPriorityMode) {
159 return Velocity.Zero
160 }
161
162 isPriorityMode = false
163
164 return onStop(velocity)
165 }
166 }
167
PriorityNestedScrollConnectionnull168 fun PriorityNestedScrollConnection(
169 orientation: Orientation,
170 canStartPreScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
171 canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
172 canStartPostFling: (velocityAvailable: Float) -> Boolean,
173 canContinueScroll: () -> Boolean,
174 canScrollOnFling: Boolean,
175 onStart: (offsetAvailable: Float) -> Unit,
176 onScroll: (offsetAvailable: Float) -> Float,
177 onStop: (velocityAvailable: Float) -> Float,
178 ) =
179 with(SpaceVectorConverter(orientation)) {
180 PriorityNestedScrollConnection(
181 canStartPreScroll = { offsetAvailable: Offset, offsetBeforeStart: Offset ->
182 canStartPreScroll(offsetAvailable.toFloat(), offsetBeforeStart.toFloat())
183 },
184 canStartPostScroll = { offsetAvailable: Offset, offsetBeforeStart: Offset ->
185 canStartPostScroll(offsetAvailable.toFloat(), offsetBeforeStart.toFloat())
186 },
187 canStartPostFling = { velocityAvailable: Velocity ->
188 canStartPostFling(velocityAvailable.toFloat())
189 },
190 canContinueScroll = canContinueScroll,
191 canScrollOnFling = canScrollOnFling,
192 onStart = { offsetAvailable -> onStart(offsetAvailable.toFloat()) },
193 onScroll = { offsetAvailable: Offset ->
194 onScroll(offsetAvailable.toFloat()).toOffset()
195 },
196 onStop = { velocityAvailable: Velocity ->
197 onStop(velocityAvailable.toFloat()).toVelocity()
198 },
199 )
200 }
201