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