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