1 /*
2  * Copyright 2021 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
18 
19 import androidx.compose.runtime.Composable
20 import androidx.compose.runtime.CompositionLocalAccessorScope
21 import androidx.compose.runtime.Immutable
22 import androidx.compose.runtime.ProvidableCompositionLocal
23 import androidx.compose.runtime.Stable
24 import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
25 import androidx.compose.runtime.remember
26 import androidx.compose.ui.Modifier
27 import androidx.compose.ui.geometry.Offset
28 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
29 import androidx.compose.ui.node.DelegatableNode
30 import androidx.compose.ui.node.DelegatingNode
31 import androidx.compose.ui.node.ModifierNodeElement
32 import androidx.compose.ui.platform.InspectorInfo
33 import androidx.compose.ui.unit.Velocity
34 
35 /**
36  * An OverscrollEffect represents a visual effect that displays when the edges of a scrolling
37  * container have been reached with a scroll or fling. To create an instance of the default /
38  * currently provided [OverscrollFactory], use [rememberOverscrollEffect].
39  *
40  * To implement, make sure to override [node] - this has a default implementation for compatibility
41  * reasons, but is required for an OverscrollEffect to render.
42  *
43  * OverscrollEffect conceptually 'decorates' scroll / fling events: consuming some of the delta or
44  * velocity before and/or after the event is consumed by the scrolling container. [applyToScroll]
45  * applies overscroll to a scroll event, and [applyToFling] applies overscroll to a fling.
46  *
47  * Higher level components such as [androidx.compose.foundation.lazy.LazyColumn] will automatically
48  * configure an OverscrollEffect for you. To use a custom OverscrollEffect you first need to provide
49  * it with scroll and/or fling events - usually by providing it to a
50  * [androidx.compose.foundation.gestures.scrollable]. Then you can draw the effect on top of the
51  * scrolling content using [Modifier.overscroll].
52  *
53  * @sample androidx.compose.foundation.samples.OverscrollSample
54  */
55 @Stable
56 interface OverscrollEffect {
57     /**
58      * Applies overscroll to [performScroll]. [performScroll] should represent a drag / scroll, and
59      * returns the amount of delta consumed, so in simple cases the amount of overscroll to show
60      * should be equal to `delta - performScroll(delta)`. The OverscrollEffect can optionally
61      * consume some delta before calling [performScroll], such as to release any existing tension.
62      * The implementation *must* call [performScroll] exactly once. This function should return the
63      * sum of all the delta that was consumed during this operation - both by the overscroll and
64      * [performScroll].
65      *
66      * For example, assume we want to apply overscroll to a custom component that isn't using
67      * [androidx.compose.foundation.gestures.scrollable]. Here is a simple example of a component
68      * using [androidx.compose.foundation.gestures.draggable] instead:
69      *
70      * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_Before
71      *
72      * To apply overscroll, we need to decorate the existing logic with applyToScroll, and return
73      * the amount of delta we have consumed when updating the drag position. Note that we also need
74      * to call applyToFling - this is used as an end signal for overscroll so that effects can
75      * correctly reset after any animations, when the gesture has stopped.
76      *
77      * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_After
78      * @param delta total scroll delta available
79      * @param source the source of the delta
80      * @param performScroll the scroll action that the overscroll is applied to. The [Offset]
81      *   parameter represents how much delta is available, and the return value is how much delta
82      *   was consumed. Any delta that was not consumed should be used to show the overscroll effect.
83      * @return the delta consumed from [delta] by the operation of this function - including that
84      *   consumed by [performScroll].
85      */
applyToScrollnull86     fun applyToScroll(
87         delta: Offset,
88         source: NestedScrollSource,
89         performScroll: (Offset) -> Offset
90     ): Offset
91 
92     /**
93      * Applies overscroll to [performFling]. [performFling] should represent a fling (the release of
94      * a drag or scroll), and returns the amount of [Velocity] consumed, so in simple cases the
95      * amount of overscroll to show should be equal to `velocity - performFling(velocity)`. The
96      * OverscrollEffect can optionally consume some [Velocity] before calling [performFling], such
97      * as to release any existing tension. The implementation *must* call [performFling] exactly
98      * once.
99      *
100      * For example, assume we want to apply overscroll to a custom component that isn't using
101      * [androidx.compose.foundation.gestures.scrollable]. Here is a simple example of a component
102      * using [androidx.compose.foundation.gestures.draggable] instead:
103      *
104      * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_Before
105      *
106      * To apply overscroll, we decorate the existing logic with applyToScroll, and return the amount
107      * of delta we have consumed when updating the drag position. We then call applyToFling using
108      * the velocity provided by onDragStopped.
109      *
110      * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_After
111      * @param velocity total [Velocity] available
112      * @param performFling the [Velocity] consuming lambda that the overscroll is applied to. The
113      *   [Velocity] parameter represents how much [Velocity] is available, and the return value is
114      *   how much [Velocity] was consumed. Any [Velocity] that was not consumed should be used to
115      *   show the overscroll effect.
116      */
117     suspend fun applyToFling(velocity: Velocity, performFling: suspend (Velocity) -> Velocity)
118 
119     /**
120      * Whether this OverscrollEffect is currently displaying overscroll.
121      *
122      * @return true if this OverscrollEffect is currently displaying overscroll
123      */
124     val isInProgress: Boolean
125 
126     /**
127      * A [Modifier] that will draw this OverscrollEffect
128      *
129      * This API is deprecated- implementers should instead override [node]. Callers should use
130      * [Modifier.overscroll].
131      */
132     @Deprecated(
133         "This has been replaced with `node`. If you are calling this property to render overscroll, use Modifier.overscroll() instead. If you are implementing OverscrollEffect, override `node` instead to render your overscroll.",
134         level = DeprecationLevel.ERROR,
135         replaceWith =
136             ReplaceWith("Modifier.overscroll(this)", "androidx.compose.foundation.overscroll")
137     )
138     val effectModifier: Modifier
139         get() = Modifier
140 
141     /**
142      * The [DelegatableNode] that will render this OverscrollEffect and provide any required size or
143      * other information to this effect.
144      *
145      * In most cases you should use [Modifier.overscroll] to render this OverscrollEffect, which
146      * will internally attach this node to the hierarchy. The node should be attached before
147      * [applyToScroll] or [applyToFling] is called to ensure correctness.
148      *
149      * This property should return a single instance, and can only be attached once, as with other
150      * [DelegatableNode]s.
151      */
152     val node: DelegatableNode
153         get() = object : Modifier.Node() {}
154 }
155 
156 /**
157  * Returns a wrapped version of [this] [OverscrollEffect] with an empty [OverscrollEffect.node].
158  * This prevents the overscroll effect from applying any visual effect, but it will still handle
159  * events.
160  *
161  * This can be used along with [withoutEventHandling] in cases where you wish to change where
162  * overscroll is rendered for a given component. Pass this wrapped instance that doesn't render to
163  * the component that handles events (such as [androidx.compose.foundation.lazy.LazyColumn]) to
164  * prevent it from drawing the overscroll effect. Then to separately render the original overscroll
165  * effect, you can directly pass it to [Modifier.overscroll] (since that modifier only renders, and
166  * does not handle events). If instead you want to draw the overscroll in another component that
167  * handles events, such as a different lazy list, you need to first wrap the original overscroll
168  * effect with [withoutEventHandling] to prevent it from also dispatching events.
169  *
170  * @sample androidx.compose.foundation.samples.OverscrollRenderedOnTopOfLazyListDecorations
171  * @see withoutEventHandling
172  */
173 @Stable
OverscrollEffectnull174 fun OverscrollEffect.withoutVisualEffect(): OverscrollEffect =
175     WrappedOverscrollEffect(
176         attachNode = false,
177         eventHandlingEnabled = true,
178         innerOverscrollEffect = this
179     )
180 
181 /**
182  * Returns a wrapped version of [this] [OverscrollEffect] that will not handle any incoming events.
183  * This means that calls to [OverscrollEffect.applyToScroll] / [OverscrollEffect.applyToFling] will
184  * directly execute the provided performScroll / performFling lambdas, without the
185  * [OverscrollEffect] ever seeing the incoming values. [OverscrollEffect.node] will still be
186  * attached, so that overscroll can render.
187  *
188  * This can be useful if you want to render an [OverscrollEffect] in a different component that
189  * normally provides events to overscroll, such as a [androidx.compose.foundation.lazy.LazyColumn].
190  * Use this along with [withoutVisualEffect] to create two wrapped instances: one that does not
191  * handle events, and one that does not draw, so you can ensure that the overscroll effect is only
192  * rendered once, and only receives events from one source.
193  *
194  * @see withoutVisualEffect
195  */
196 @Stable
197 fun OverscrollEffect.withoutEventHandling(): OverscrollEffect =
198     WrappedOverscrollEffect(
199         attachNode = true,
200         eventHandlingEnabled = false,
201         innerOverscrollEffect = this
202     )
203 
204 @Immutable
205 private class WrappedOverscrollEffect(
206     private val attachNode: Boolean,
207     private val eventHandlingEnabled: Boolean,
208     private val innerOverscrollEffect: OverscrollEffect
209 ) : OverscrollEffect {
210     override fun applyToScroll(
211         delta: Offset,
212         source: NestedScrollSource,
213         performScroll: (Offset) -> Offset
214     ): Offset {
215         return if (eventHandlingEnabled) {
216             innerOverscrollEffect.applyToScroll(delta, source, performScroll)
217         } else {
218             performScroll(delta)
219         }
220     }
221 
222     override suspend fun applyToFling(
223         velocity: Velocity,
224         performFling: suspend (Velocity) -> Velocity
225     ) {
226         if (eventHandlingEnabled) {
227             innerOverscrollEffect.applyToFling(velocity, performFling)
228         } else {
229             performFling(velocity)
230         }
231     }
232 
233     override val isInProgress: Boolean
234         get() = innerOverscrollEffect.isInProgress
235 
236     override val node: DelegatableNode =
237         if (attachNode) innerOverscrollEffect.node else object : Modifier.Node() {}
238 
239     override fun equals(other: Any?): Boolean {
240         if (this === other) return true
241         if (other !is WrappedOverscrollEffect) return false
242 
243         if (attachNode != other.attachNode) return false
244         if (eventHandlingEnabled != other.eventHandlingEnabled) return false
245         if (innerOverscrollEffect != other.innerOverscrollEffect) return false
246 
247         return true
248     }
249 
250     override fun hashCode(): Int {
251         var result = attachNode.hashCode()
252         result = 31 * result + eventHandlingEnabled.hashCode()
253         result = 31 * result + innerOverscrollEffect.hashCode()
254         return result
255     }
256 }
257 
258 /**
259  * Renders overscroll from the provided [overscrollEffect].
260  *
261  * This modifier attaches the provided [overscrollEffect]'s [OverscrollEffect.node] to the
262  * hierarchy, which renders the actual effect. Note that this modifier is only responsible for the
263  * visual part of overscroll - on its own it will not handle input events. In addition to using this
264  * modifier you also need to propagate events to the [overscrollEffect], most commonly by using a
265  * [androidx.compose.foundation.gestures.scrollable].
266  *
267  * Alternatively, you can use a higher level API such as [verticalScroll] or
268  * [androidx.compose.foundation.lazy.LazyColumn] and provide a custom [OverscrollEffect] - these
269  * components will both render and provide events to the [OverscrollEffect], so you do not need to
270  * manually render the effect with this modifier.
271  *
272  * @sample androidx.compose.foundation.samples.OverscrollSample
273  * @param overscrollEffect the [OverscrollEffect] to render
274  */
275 @Suppress("DEPRECATION_ERROR")
overscrollnull276 fun Modifier.overscroll(overscrollEffect: OverscrollEffect?): Modifier {
277     val effectModifier = overscrollEffect?.effectModifier ?: Modifier
278     val modifier =
279         if (effectModifier !== Modifier) effectModifier
280         else OverscrollModifierElement(overscrollEffect)
281     return this.then(modifier)
282 }
283 
284 private class OverscrollModifierElement(
285     private val overscrollEffect: OverscrollEffect?,
286 ) : ModifierNodeElement<OverscrollModifierNode>() {
createnull287     override fun create(): OverscrollModifierNode {
288         return OverscrollModifierNode(overscrollEffect?.node)
289     }
290 
updatenull291     override fun update(node: OverscrollModifierNode) {
292         node.update(overscrollEffect?.node)
293     }
294 
equalsnull295     override fun equals(other: Any?): Boolean {
296         if (this === other) return true
297         if (other !is OverscrollModifierElement) return false
298 
299         if (overscrollEffect != other.overscrollEffect) return false
300         return true
301     }
302 
hashCodenull303     override fun hashCode(): Int {
304         return overscrollEffect.hashCode()
305     }
306 
inspectablePropertiesnull307     override fun InspectorInfo.inspectableProperties() {
308         name = "overscroll"
309         properties["overscrollEffect"] = overscrollEffect
310     }
311 }
312 
313 private class OverscrollModifierNode(private var overscrollNode: DelegatableNode?) :
314     DelegatingNode() {
onAttachnull315     override fun onAttach() {
316         attachIfNeeded()
317     }
318 
onDetachnull319     override fun onDetach() {
320         overscrollNode?.let { undelegate(it) }
321     }
322 
updatenull323     fun update(overscrollNode: DelegatableNode?) {
324         this.overscrollNode?.let { undelegate(it) }
325         this.overscrollNode = overscrollNode
326         attachIfNeeded()
327     }
328 
attachIfNeedednull329     private fun attachIfNeeded() {
330         overscrollNode =
331             if (overscrollNode?.node?.isAttached == false) {
332                 delegate(overscrollNode!!)
333             } else {
334                 null
335             }
336     }
337 }
338 
339 /**
340  * Returns a remembered [OverscrollEffect] created from the current value of
341  * [LocalOverscrollFactory]. If [LocalOverscrollFactory] changes, a new [OverscrollEffect] will be
342  * returned. Returns `null` if `null` is provided to [LocalOverscrollFactory].
343  */
344 @Composable
rememberOverscrollEffectnull345 fun rememberOverscrollEffect(): OverscrollEffect? {
346     val overscrollFactory = LocalOverscrollFactory.current ?: return null
347     return remember(overscrollFactory) { overscrollFactory.createOverscrollEffect() }
348 }
349 
350 /**
351  * Needed for behavioral backwards compatibility for
352  * [androidx.compose.foundation.gestures.ScrollableDefaults.overscrollEffect]. New code should use
353  * [rememberOverscrollEffect] instead, which takes into account theme provided overscroll, rather
354  * than always using the platform default, without any customizations.
355  */
rememberPlatformOverscrollEffectnull356 @Composable internal expect fun rememberPlatformOverscrollEffect(): OverscrollEffect?
357 
358 /**
359  * A factory for creating [OverscrollEffect]s. You can provide a factory instance to
360  * [LocalOverscrollFactory] to globally change the factory, and hence effect, used by components
361  * within the hierarchy.
362  *
363  * See [rememberOverscrollEffect] to remember an [OverscrollEffect] from the current factory
364  * provided to [LocalOverscrollFactory].
365  */
366 interface OverscrollFactory {
367     /** Returns a new [OverscrollEffect] instance. */
368     fun createOverscrollEffect(): OverscrollEffect
369 
370     /**
371      * Require hashCode() to be implemented. Using a data class is sufficient. Singletons and
372      * instances with no properties may implement this function by returning an arbitrary constant.
373      */
374     override fun hashCode(): Int
375 
376     /**
377      * Require equals() to be implemented. Using a data class is sufficient. Singletons may
378      * implement this function with referential equality (`this === other`). Instances with no
379      * properties may implement this function by checking the type of the other object.
380      */
381     override fun equals(other: Any?): Boolean
382 }
383 
384 /**
385  * CompositionLocal that provides an [OverscrollFactory] through the hierarchy. This will be used by
386  * default by scrolling components, so you can provide an [OverscrollFactory] here to override the
387  * overscroll used by components within a hierarchy.
388  *
389  * See [rememberOverscrollEffect] to remember an [OverscrollEffect] from the current provided value.
390  */
391 val LocalOverscrollFactory: ProvidableCompositionLocal<OverscrollFactory?> =
<lambda>null392     compositionLocalWithComputedDefaultOf {
393         defaultOverscrollFactory()
394     }
395 
defaultOverscrollFactorynull396 internal expect fun CompositionLocalAccessorScope.defaultOverscrollFactory(): OverscrollFactory?
397