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