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