1 /*
2 * Copyright 2020 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 androidx.compose.foundation.gestures
18
19 import androidx.compose.foundation.MutatePriority
20 import androidx.compose.foundation.MutatorMutex
21 import androidx.compose.foundation.internal.JvmDefaultWithCompatibility
22 import androidx.compose.runtime.Composable
23 import androidx.compose.runtime.mutableStateOf
24 import androidx.compose.runtime.remember
25 import androidx.compose.runtime.rememberUpdatedState
26 import kotlinx.coroutines.coroutineScope
27
28 /**
29 * An object representing something that can be scrolled. This interface is implemented by states of
30 * scrollable containers such as [androidx.compose.foundation.lazy.LazyListState] or
31 * [androidx.compose.foundation.ScrollState] in order to provide low-level scrolling control via
32 * [scroll], as well as allowing for higher-level scrolling functions like [animateScrollBy] to be
33 * implemented as extension functions on [ScrollableState].
34 *
35 * Subclasses may also have their own methods that are specific to their interaction paradigm, such
36 * as [androidx.compose.foundation.lazy.LazyListState.scrollToItem].
37 *
38 * @see androidx.compose.foundation.gestures.animateScrollBy
39 * @see androidx.compose.foundation.gestures.scrollable
40 */
41 @JvmDefaultWithCompatibility
42 interface ScrollableState {
43 /**
44 * Call this function to take control of scrolling and gain the ability to send scroll events
45 * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
46 * performed within a [scroll] block (even if they don't call any other methods on this object)
47 * in order to guarantee that mutual exclusion is enforced.
48 *
49 * If [scroll] is called from elsewhere with the [scrollPriority] higher or equal to ongoing
50 * scroll, ongoing scroll will be canceled.
51 */
scrollnull52 suspend fun scroll(
53 scrollPriority: MutatePriority = MutatePriority.Default,
54 block: suspend ScrollScope.() -> Unit
55 )
56
57 /**
58 * Dispatch scroll delta in pixels avoiding all scroll related mechanisms.
59 *
60 * **NOTE:** unlike [scroll], dispatching any delta with this method won't trigger nested
61 * scroll, won't stop ongoing scroll/drag animation and will bypass scrolling of any priority.
62 * This method will also ignore `reverseDirection` and other parameters set in scrollable.
63 *
64 * This method is used internally for nested scrolling dispatch and other low level operations,
65 * allowing implementers of [ScrollableState] influence the consumption as suits them. Manually
66 * dispatching delta via this method will likely result in a bad user experience, you must
67 * prefer [scroll] method over this one.
68 *
69 * @param delta amount of scroll dispatched in the nested scroll process
70 * @return the amount of delta consumed
71 */
72 fun dispatchRawDelta(delta: Float): Float
73
74 /**
75 * Whether this [ScrollableState] is currently scrolling by gesture, fling or programmatically
76 * or not.
77 */
78 val isScrollInProgress: Boolean
79
80 /**
81 * Whether this [ScrollableState] can scroll forward (consume a positive delta). This is
82 * typically false if the scroll position is equal to its maximum value, and true otherwise.
83 *
84 * Note that `true` here does not imply that delta *will* be consumed - the ScrollableState may
85 * decide not to handle the incoming delta (such as if it is already being scrolled separately).
86 * Additionally, for backwards compatibility with previous versions of ScrollableState this
87 * value defaults to `true`.
88 *
89 * @sample androidx.compose.foundation.samples.CanScrollSample
90 */
91 val canScrollForward: Boolean
92 get() = true
93
94 /**
95 * Whether this [ScrollableState] can scroll backward (consume a negative delta). This is
96 * typically false if the scroll position is equal to its minimum value, and true otherwise.
97 *
98 * Note that `true` here does not imply that delta *will* be consumed - the ScrollableState may
99 * decide not to handle the incoming delta (such as if it is already being scrolled separately).
100 * Additionally, for backwards compatibility with previous versions of ScrollableState this
101 * value defaults to `true`.
102 *
103 * @sample androidx.compose.foundation.samples.CanScrollSample
104 */
105 val canScrollBackward: Boolean
106 get() = true
107
108 /**
109 * The value of this property is true under the following scenarios, otherwise it's false.
110 * - This [ScrollableState] is currently scrolling forward.
111 * - This [ScrollableState] was scrolling forward in its last scroll action.
112 */
113 @get:Suppress("GetterSetterNames")
114 val lastScrolledForward: Boolean
115 get() = false
116
117 /**
118 * The value of this property is true under the following scenarios, otherwise it's false.
119 * - This [ScrollableState] is currently scrolling backward.
120 * - This [ScrollableState] was scrolling backward in its last scroll action.
121 */
122 @get:Suppress("GetterSetterNames")
123 val lastScrolledBackward: Boolean
124 get() = false
125 }
126
127 /**
128 * Default implementation of [ScrollableState] interface that contains necessary information about
129 * the ongoing fling and provides smooth scrolling capabilities.
130 *
131 * This is the simplest way to set up a [scrollable] modifier. When constructing this
132 * [ScrollableState], you must provide a [consumeScrollDelta] lambda, which will be invoked whenever
133 * scroll happens (by gesture input, by smooth scrolling, by flinging or nested scroll) with the
134 * delta in pixels. The amount of scrolling delta consumed must be returned from this lambda to
135 * ensure proper nested scrolling behaviour.
136 *
137 * @param consumeScrollDelta callback invoked when drag/fling/smooth scrolling occurs. The callback
138 * receives the delta in pixels. Callers should update their state in this lambda and return the
139 * amount of delta consumed
140 */
141 fun ScrollableState(consumeScrollDelta: (Float) -> Float): ScrollableState {
142 return DefaultScrollableState(consumeScrollDelta)
143 }
144
145 /**
146 * Create and remember the default implementation of [ScrollableState] interface that contains
147 * necessary information about the ongoing fling and provides smooth scrolling capabilities.
148 *
149 * This is the simplest way to set up a [scrollable] modifier. When constructing this
150 * [ScrollableState], you must provide a [consumeScrollDelta] lambda, which will be invoked whenever
151 * scroll happens (by gesture input, by smooth scrolling, by flinging or nested scroll) with the
152 * delta in pixels. The amount of scrolling delta consumed must be returned from this lambda to
153 * ensure proper nested scrolling behaviour.
154 *
155 * @param consumeScrollDelta callback invoked when drag/fling/smooth scrolling occurs. The callback
156 * receives the delta in pixels. Callers should update their state in this lambda and return the
157 * amount of delta consumed
158 */
159 @Composable
rememberScrollableStatenull160 fun rememberScrollableState(consumeScrollDelta: (Float) -> Float): ScrollableState {
161 val lambdaState = rememberUpdatedState(consumeScrollDelta)
162 return remember { ScrollableState { lambdaState.value.invoke(it) } }
163 }
164
165 /** Scope used for suspending scroll blocks */
166 interface ScrollScope {
167 /**
168 * Attempts to scroll forward by [pixels] px.
169 *
170 * @return the amount of the requested scroll that was consumed (that is, how far it scrolled)
171 */
scrollBynull172 fun scrollBy(pixels: Float): Float
173 }
174
175 private class DefaultScrollableState(val onDelta: (Float) -> Float) : ScrollableState {
176
177 private val scrollScope: ScrollScope =
178 object : ScrollScope {
179 override fun scrollBy(pixels: Float): Float {
180 if (pixels.isNaN()) return 0f
181 val delta = onDelta(pixels)
182 isLastScrollForwardState.value = delta > 0
183 isLastScrollBackwardState.value = delta < 0
184 return delta
185 }
186 }
187
188 private val scrollMutex = MutatorMutex()
189
190 private val isScrollingState = mutableStateOf(false)
191 private val isLastScrollForwardState = mutableStateOf(false)
192 private val isLastScrollBackwardState = mutableStateOf(false)
193
194 override suspend fun scroll(
195 scrollPriority: MutatePriority,
196 block: suspend ScrollScope.() -> Unit
197 ): Unit = coroutineScope {
198 scrollMutex.mutateWith(scrollScope, scrollPriority) {
199 isScrollingState.value = true
200 try {
201 block()
202 } finally {
203 isScrollingState.value = false
204 }
205 }
206 }
207
208 override fun dispatchRawDelta(delta: Float): Float {
209 return onDelta(delta)
210 }
211
212 override val isScrollInProgress: Boolean
213 get() = isScrollingState.value
214
215 override val lastScrolledForward: Boolean
216 get() = isLastScrollForwardState.value
217
218 override val lastScrolledBackward: Boolean
219 get() = isLastScrollBackwardState.value
220 }
221