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.lazy.grid
18 
19 import androidx.compose.foundation.gestures.Orientation
20 import androidx.compose.foundation.gestures.snapping.offsetOnMainAxis
21 import androidx.compose.ui.layout.MeasureResult
22 import androidx.compose.ui.unit.Constraints
23 import androidx.compose.ui.unit.Density
24 import androidx.compose.ui.unit.IntSize
25 import androidx.compose.ui.util.fastForEach
26 import kotlinx.coroutines.CoroutineScope
27 
28 /** The result of the measure pass for lazy grid layout. */
29 internal class LazyGridMeasureResult(
30     // properties defining the scroll position:
31     /** The new first visible line of items. */
32     val firstVisibleLine: LazyGridMeasuredLine?,
33     /** The new value for [LazyGridState.firstVisibleItemScrollOffset]. */
34     val firstVisibleLineScrollOffset: Int,
35     /** True if there is some space available to continue scrolling in the forward direction. */
36     val canScrollForward: Boolean,
37     /** The amount of scroll consumed during the measure pass. */
38     val consumedScroll: Float,
39     /** MeasureResult defining the layout. */
40     private val measureResult: MeasureResult,
41     /** The amount of scroll-back that happened due to reaching the end of the list. */
42     val scrollBackAmount: Float,
43     /** True when extra remeasure is required. */
44     val remeasureNeeded: Boolean,
45     /** Scope for animations. */
46     val coroutineScope: CoroutineScope,
47     /** Density of the last measure. */
48     val density: Density,
49     /** Amount of slots we have in each line. */
50     val slotsPerLine: Int,
51     /** Finds items on a line and their measurement constraints. Used for prefetching. */
52     val prefetchInfoRetriever: (line: Int) -> List<Pair<Int, Constraints>>,
53     // properties representing the info needed for LazyListLayoutInfo:
54     /** see [LazyGridLayoutInfo.visibleItemsInfo] */
55     override val visibleItemsInfo: List<LazyGridMeasuredItem>,
56     /** see [LazyGridLayoutInfo.viewportStartOffset] */
57     override val viewportStartOffset: Int,
58     /** see [LazyGridLayoutInfo.viewportEndOffset] */
59     override val viewportEndOffset: Int,
60     /** see [LazyGridLayoutInfo.totalItemsCount] */
61     override val totalItemsCount: Int,
62     /** see [LazyGridLayoutInfo.reverseLayout] */
63     override val reverseLayout: Boolean,
64     /** see [LazyGridLayoutInfo.orientation] */
65     override val orientation: Orientation,
66     /** see [LazyGridLayoutInfo.afterContentPadding] */
67     override val afterContentPadding: Int,
68     /** see [LazyGridLayoutInfo.mainAxisItemSpacing] */
69     override val mainAxisItemSpacing: Int
<lambda>null70 ) : LazyGridLayoutInfo, MeasureResult by measureResult {
71 
72     val canScrollBackward
73         get() = (firstVisibleLine?.index ?: 0) != 0 || firstVisibleLineScrollOffset != 0
74 
75     override val viewportSize: IntSize
76         get() = IntSize(width, height)
77 
78     override val beforeContentPadding: Int
79         get() = -viewportStartOffset
80 
81     override val maxSpan: Int
82         get() = slotsPerLine
83 
84     /**
85      * Creates a new layout info with applying a scroll [delta] for this layout info. In some cases
86      * we can apply small scroll deltas by just changing the offsets for each [visibleItemsInfo].
87      * But we can only do so if after applying the delta we would not need to compose a new item or
88      * dispose an item which is currently visible. In this case this function will not apply the
89      * [delta] and return null.
90      *
91      * @return new layout info if we can safely apply a passed scroll [delta] to this layout info.
92      *   If If new layout info is returned, only the placement phase is needed to apply new offsets.
93      *   If null is returned, it means we have to rerun the full measure phase to apply the [delta].
94      */
95     fun copyWithScrollDeltaWithoutRemeasure(
96         delta: Int,
97         updateAnimations: Boolean
98     ): LazyGridMeasureResult? {
99         if (
100             remeasureNeeded ||
101                 visibleItemsInfo.isEmpty() ||
102                 firstVisibleLine == null ||
103                 // applying this delta will change firstVisibleLineScrollOffset
104                 (firstVisibleLineScrollOffset - delta) !in
105                     0 until firstVisibleLine.mainAxisSizeWithSpacings
106         ) {
107             return null
108         }
109         val first = visibleItemsInfo.first()
110         val last = visibleItemsInfo.last()
111         if (first.nonScrollableItem || last.nonScrollableItem) {
112             // non scrollable items require special handling.
113             return null
114         }
115         val canApply =
116             if (delta < 0) {
117                 // scrolling forward
118                 val deltaToFirstItemChange =
119                     first.offsetOnMainAxis(orientation) + first.mainAxisSizeWithSpacings -
120                         viewportStartOffset
121                 val deltaToLastItemChange =
122                     last.offsetOnMainAxis(orientation) + last.mainAxisSizeWithSpacings -
123                         viewportEndOffset
124                 minOf(deltaToFirstItemChange, deltaToLastItemChange) > -delta
125             } else {
126                 // scrolling backward
127                 val deltaToFirstItemChange =
128                     viewportStartOffset - first.offsetOnMainAxis(orientation)
129                 val deltaToLastItemChange = viewportEndOffset - last.offsetOnMainAxis(orientation)
130                 minOf(deltaToFirstItemChange, deltaToLastItemChange) > delta
131             }
132         return if (canApply) {
133             visibleItemsInfo.fastForEach { it.applyScrollDelta(delta, updateAnimations) }
134             LazyGridMeasureResult(
135                 firstVisibleLine = firstVisibleLine,
136                 firstVisibleLineScrollOffset = firstVisibleLineScrollOffset - delta,
137                 canScrollForward =
138                     canScrollForward ||
139                         delta > 0, // we scrolled backward, so now we can scroll forward
140                 consumedScroll = delta.toFloat(),
141                 scrollBackAmount = scrollBackAmount,
142                 measureResult = measureResult,
143                 remeasureNeeded = remeasureNeeded,
144                 coroutineScope = coroutineScope,
145                 density = density,
146                 slotsPerLine = slotsPerLine,
147                 prefetchInfoRetriever = prefetchInfoRetriever,
148                 visibleItemsInfo = visibleItemsInfo,
149                 viewportStartOffset = viewportStartOffset,
150                 viewportEndOffset = viewportEndOffset,
151                 totalItemsCount = totalItemsCount,
152                 reverseLayout = reverseLayout,
153                 orientation = orientation,
154                 afterContentPadding = afterContentPadding,
155                 mainAxisItemSpacing = mainAxisItemSpacing
156             )
157         } else {
158             null
159         }
160     }
161 }
162