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