1 /*
2  * Copyright 2024 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.animation.core.AnimationSpec
20 import androidx.compose.animation.core.spring
21 import androidx.compose.runtime.ProvidableCompositionLocal
22 import androidx.compose.runtime.Stable
23 import androidx.compose.ui.Modifier
24 import kotlin.math.abs
25 
26 /**
27  * A composition local to customize the focus scrolling behavior used by some scrollable containers.
28  * [LocalBringIntoViewSpec] has a platform defined default behavior.
29  */
30 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
31 expect val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec>
32 
33 /**
34  * The configuration of how a scrollable reacts to bring into view requests.
35  *
36  * Check the following sample for a use case usage of this API:
37  *
38  * @sample androidx.compose.foundation.samples.FocusScrollingInLazyRowSample
39  */
40 @Stable
41 interface BringIntoViewSpec {
42 
43     /**
44      * An Animation Spec to be used as the animation to run to fulfill the BringIntoView requests.
45      */
46     @Deprecated("Animation spec customization is no longer supported.")
47     @get:Deprecated("Animation spec customization is no longer supported.")
48     val scrollAnimationSpec: AnimationSpec<Float>
49         get() = DefaultScrollAnimationSpec
50 
51     /**
52      * Calculate the offset needed to bring one of the scrollable container's child into view. This
53      * will be called for every frame of the scrolling animation. This means that, as the animation
54      * progresses, the offset will naturally change to fulfill the scroll request.
55      *
56      * All distances below are represented in pixels.
57      *
58      * @param offset from the side closest to the start of the container.
59      * @param size is the child size.
60      * @param containerSize Is the main axis size of the scrollable container.
61      * @return The necessary amount to scroll to satisfy the bring into view request. Returning zero
62      *   from here means that the request was satisfied and the scrolling animation should stop.
63      */
calculateScrollDistancenull64     fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float =
65         defaultCalculateScrollDistance(offset, size, containerSize)
66 
67     companion object {
68 
69         /**
70          * The default animation spec used by [Modifier.scrollable] to run Bring Into View requests.
71          */
72         internal val DefaultScrollAnimationSpec: AnimationSpec<Float> = spring()
73 
74         internal val DefaultBringIntoViewSpec = object : BringIntoViewSpec {}
75 
76         internal fun defaultCalculateScrollDistance(
77             offset: Float,
78             size: Float,
79             containerSize: Float
80         ): Float {
81             val trailingEdge = offset + size
82             @Suppress("UnnecessaryVariable") val leadingEdge = offset
83             return when {
84 
85                 // If the item is already visible, no need to scroll.
86                 leadingEdge >= 0 && trailingEdge <= containerSize -> 0f
87 
88                 // If the item is visible but larger than the parent, we don't scroll.
89                 leadingEdge < 0 && trailingEdge > containerSize -> 0f
90 
91                 // Find the minimum scroll needed to make one of the edges coincide with the
92                 // parent's
93                 // edge.
94                 abs(leadingEdge) < abs(trailingEdge - containerSize) -> leadingEdge
95                 else -> trailingEdge - containerSize
96             }
97         }
98     }
99 }
100