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.lazy.layout 18 19 import androidx.compose.animation.core.AnimationState 20 import androidx.compose.animation.core.AnimationVector1D 21 import androidx.compose.animation.core.Spring 22 import androidx.compose.animation.core.VectorConverter 23 import androidx.compose.animation.core.animateTo 24 import androidx.compose.animation.core.copy 25 import androidx.compose.animation.core.spring 26 import androidx.compose.runtime.snapshots.Snapshot 27 import androidx.compose.ui.unit.Density 28 import androidx.compose.ui.unit.dp 29 import kotlinx.coroutines.CoroutineScope 30 import kotlinx.coroutines.Job 31 import kotlinx.coroutines.launch 32 33 /** 34 * This class manages the scroll delta between lookahead pass and approach pass. Lookahead pass is 35 * the source of truth for scrolling lazy layouts. However, at times during an animation, the items 36 * in approach may not be as large as they are in lookahead yet (i.e. these items have not reached 37 * their target size). As such, the same scrolling that lookahead accepts may cause back scroll in 38 * approach due to the smaller item size at the end of the list. In this situation, we will be 39 * taking the amount of back scroll from the approach and gradually animate it down to 0 to avoid 40 * any sudden jump in position via [updateScrollDeltaForApproach]. 41 */ 42 internal class LazyLayoutScrollDeltaBetweenPasses { 43 44 internal val scrollDeltaBetweenPasses: Float 45 get() = _scrollDeltaBetweenPasses.value 46 47 internal var job: Job? = null 48 49 internal val isActive: Boolean 50 get() = _scrollDeltaBetweenPasses.value != 0f 51 52 private var _scrollDeltaBetweenPasses: AnimationState<Float, AnimationVector1D> = 53 AnimationState(Float.VectorConverter, 0f, 0f) 54 55 // Updates the scroll delta between lookahead & post-lookahead pass updateScrollDeltaForApproachnull56 internal fun updateScrollDeltaForApproach( 57 delta: Float, 58 density: Density, 59 coroutineScope: CoroutineScope 60 ) { 61 if (delta <= with(density) { DeltaThresholdForScrollAnimation.toPx() }) { 62 // If the delta is within the threshold, scroll by the delta amount instead of animating 63 return 64 } 65 66 // Scroll delta is updated during lookahead, we don't need to trigger lookahead when 67 // the delta changes. 68 Snapshot.withoutReadObservation { 69 val currentDelta = _scrollDeltaBetweenPasses.value 70 71 job?.cancel() 72 if (_scrollDeltaBetweenPasses.isRunning) { 73 _scrollDeltaBetweenPasses = _scrollDeltaBetweenPasses.copy(currentDelta - delta) 74 } else { 75 _scrollDeltaBetweenPasses = AnimationState(Float.VectorConverter, -delta) 76 } 77 job = 78 coroutineScope.launch { 79 _scrollDeltaBetweenPasses.animateTo( 80 0f, 81 spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 0.5f), 82 true 83 ) 84 } 85 } 86 } 87 stopnull88 internal fun stop() { 89 job?.cancel() 90 _scrollDeltaBetweenPasses = AnimationState(Float.VectorConverter, 0f) 91 } 92 } 93 94 private val DeltaThresholdForScrollAnimation = 1.dp 95