1 /*
<lambda>null2  * Copyright 2022 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.staggeredgrid
18 
19 import androidx.compose.foundation.gestures.Orientation
20 import androidx.compose.foundation.lazy.layout.MutableIntervalList
21 import androidx.compose.ui.layout.AlignmentLine
22 import androidx.compose.ui.layout.MeasureResult
23 import androidx.compose.ui.unit.Density
24 import androidx.compose.ui.unit.IntOffset
25 import androidx.compose.ui.unit.IntSize
26 import androidx.compose.ui.util.fastForEach
27 import androidx.compose.ui.util.fastSumBy
28 import kotlin.coroutines.EmptyCoroutineContext
29 import kotlinx.coroutines.CoroutineScope
30 
31 /**
32  * Information about layout state of individual item in lazy staggered grid.
33  *
34  * @see [LazyStaggeredGridLayoutInfo]
35  */
36 sealed interface LazyStaggeredGridItemInfo {
37     /** Relative offset from the start of the staggered grid. */
38     val offset: IntOffset
39 
40     /** Index of the item. */
41     val index: Int
42 
43     /**
44      * Column (for vertical staggered grids) or row (for horizontal staggered grids) that the item
45      * is in.
46      */
47     val lane: Int
48 
49     /** Key of the item passed in [LazyStaggeredGridScope.items] */
50     val key: Any
51 
52     /**
53      * Item size in pixels. If item contains multiple layouts, the size is calculated as a sum of
54      * their sizes.
55      */
56     val size: IntSize
57 
58     /** The content type of the item which was passed to the item() or items() function. */
59     val contentType: Any?
60 }
61 
62 /**
63  * Information about layout state of lazy staggered grids. Can be retrieved from
64  * [LazyStaggeredGridState.layoutInfo].
65  */
66 // todo(b/182882362): expose more information about layout state
67 sealed interface LazyStaggeredGridLayoutInfo {
68     /** Orientation of the staggered grid. */
69     val orientation: Orientation
70 
71     /** The list of [LazyStaggeredGridItemInfo] per each visible item ordered by index. */
72     val visibleItemsInfo: List<LazyStaggeredGridItemInfo>
73 
74     /** The total count of items passed to staggered grid. */
75     val totalItemsCount: Int
76 
77     /** Layout viewport (content + content padding) size in pixels. */
78     val viewportSize: IntSize
79 
80     /**
81      * The start offset of the layout's viewport in pixels. You can think of it as a minimum offset
82      * which would be visible. Can be negative if non-zero [beforeContentPadding] was applied as the
83      * content displayed in the content padding area is still visible.
84      *
85      * You can use it to understand what items from [visibleItemsInfo] are fully visible.
86      */
87     val viewportStartOffset: Int
88 
89     /**
90      * The end offset of the layout's viewport in pixels. You can think of it as a maximum offset
91      * which would be visible. It is the size of the lazy grid layout minus [beforeContentPadding].
92      *
93      * You can use it to understand what items from [visibleItemsInfo] are fully visible.
94      */
95     val viewportEndOffset: Int
96 
97     /** Content padding in pixels applied before the items in scroll direction. */
98     val beforeContentPadding: Int
99 
100     /** Content padding in pixels applied after the items in scroll direction. */
101     val afterContentPadding: Int
102 
103     /** The spacing between items in scroll direction. */
104     val mainAxisItemSpacing: Int
105 }
106 
findVisibleItemnull107 internal fun LazyStaggeredGridLayoutInfo.findVisibleItem(
108     itemIndex: Int
109 ): LazyStaggeredGridItemInfo? {
110     if (visibleItemsInfo.isEmpty()) {
111         return null
112     }
113 
114     if (itemIndex !in visibleItemsInfo.first().index..visibleItemsInfo.last().index) {
115         return null
116     }
117 
118     val index = visibleItemsInfo.binarySearch { it.index - itemIndex }
119     return visibleItemsInfo.getOrNull(index)
120 }
121 
122 internal class LazyStaggeredGridMeasureResult(
123     val firstVisibleItemIndices: IntArray,
124     val firstVisibleItemScrollOffsets: IntArray,
125     val consumedScroll: Float,
126     val measureResult: MeasureResult,
127     /** The amount of scroll-back that happened due to reaching the end of the list. */
128     val scrollBackAmount: Float,
129     val canScrollForward: Boolean,
130     val isVertical: Boolean,
131     /** True when extra remeasure is required. */
132     val remeasureNeeded: Boolean,
133     val slots: LazyStaggeredGridSlots,
134     val spanProvider: LazyStaggeredGridSpanProvider,
135     val density: Density,
136     override val totalItemsCount: Int,
137     override val visibleItemsInfo: List<LazyStaggeredGridMeasuredItem>,
138     override val viewportSize: IntSize,
139     override val viewportStartOffset: Int,
140     override val viewportEndOffset: Int,
141     override val beforeContentPadding: Int,
142     override val afterContentPadding: Int,
143     override val mainAxisItemSpacing: Int,
144     val coroutineScope: CoroutineScope
<lambda>null145 ) : LazyStaggeredGridLayoutInfo, MeasureResult by measureResult {
146 
147     val canScrollBackward
148         // only scroll backward if the first item is not on screen or fully visible
149         get() = !(firstVisibleItemIndices[0] == 0 && firstVisibleItemScrollOffsets[0] <= 0)
150 
151     override val orientation: Orientation =
152         if (isVertical) Orientation.Vertical else Orientation.Horizontal
153 
154     /**
155      * Creates a new layout info with applying a scroll [delta] for this layout info. In some cases
156      * we can apply small scroll deltas by just changing the offsets for each [visibleItemsInfo].
157      * But we can only do so if after applying the delta we would not need to compose a new item or
158      * dispose an item which is currently visible. In this case this function will not apply the
159      * [delta] and return null.
160      *
161      * @return new layout info if we can safely apply a passed scroll [delta] to this layout info.
162      *   If If new layout info is returned, only the placement phase is needed to apply new offsets.
163      *   If null is returned, it means we have to rerun the full measure phase to apply the [delta].
164      */
165     fun copyWithScrollDeltaWithoutRemeasure(
166         delta: Int,
167         updateAnimations: Boolean
168     ): LazyStaggeredGridMeasureResult? {
169         if (
170             remeasureNeeded ||
171                 visibleItemsInfo.isEmpty() ||
172                 firstVisibleItemIndices.isEmpty() ||
173                 firstVisibleItemScrollOffsets.isEmpty()
174         ) {
175             return null
176         }
177         val mainAxisMax = viewportEndOffset - afterContentPadding
178         visibleItemsInfo.fastForEach {
179             // non scrollable items require special handling.
180             if (
181                 it.nonScrollableItem ||
182                     // applying delta will make this item to cross the 0th pixel, this means
183                     // that firstVisibleItemIndices will change. we require a remeasure for it.
184                     it.mainAxisOffset <= 0 != it.mainAxisOffset + delta <= 0
185             ) {
186                 return null
187             }
188             if (it.mainAxisOffset <= viewportStartOffset) {
189                 // we compare with viewportStartOffset in order to know when the item will became
190                 // not visible anymore, and with 0 to know when the firstVisibleItemIndices will
191                 // change. when we have a beforeContentPadding those values will not be the same.
192                 val canApply =
193                     if (delta < 0) { // scrolling forward
194                         it.mainAxisOffset + it.mainAxisSizeWithSpacings - viewportStartOffset >
195                             -delta
196                     } else { // scrolling backward
197                         viewportStartOffset - it.mainAxisOffset > delta
198                     }
199                 if (!canApply) return null
200             }
201             // item is partially visible at the bottom.
202             if (it.mainAxisOffset + it.mainAxisSizeWithSpacings >= mainAxisMax) {
203                 val canApply =
204                     if (delta < 0) { // scrolling forward
205                         it.mainAxisOffset + it.mainAxisSizeWithSpacings - viewportEndOffset > -delta
206                     } else { // scrolling backward
207                         viewportEndOffset - it.mainAxisOffset > delta
208                     }
209                 if (!canApply) return null
210             }
211         }
212         visibleItemsInfo.fastForEach { it.applyScrollDelta(delta, updateAnimations) }
213         return LazyStaggeredGridMeasureResult(
214             firstVisibleItemIndices = firstVisibleItemIndices,
215             firstVisibleItemScrollOffsets =
216                 IntArray(firstVisibleItemScrollOffsets.size) { index ->
217                     firstVisibleItemScrollOffsets[index] - delta
218                 },
219             consumedScroll = delta.toFloat(),
220             scrollBackAmount = scrollBackAmount,
221             measureResult = measureResult,
222             canScrollForward =
223                 canScrollForward || delta > 0, // we scrolled backward, so now we can scroll forward
224             isVertical = isVertical,
225             remeasureNeeded = remeasureNeeded,
226             slots = slots,
227             spanProvider = spanProvider,
228             density = density,
229             totalItemsCount = totalItemsCount,
230             visibleItemsInfo = visibleItemsInfo,
231             viewportSize = viewportSize,
232             viewportStartOffset = viewportStartOffset,
233             viewportEndOffset = viewportEndOffset,
234             beforeContentPadding = beforeContentPadding,
235             afterContentPadding = afterContentPadding,
236             mainAxisItemSpacing = mainAxisItemSpacing,
237             coroutineScope = coroutineScope,
238         )
239     }
240 }
241 
242 private val EmptyArray = IntArray(0)
243 
244 internal val EmptyLazyStaggeredGridLayoutInfo =
245     LazyStaggeredGridMeasureResult(
246         firstVisibleItemIndices = EmptyArray,
247         firstVisibleItemScrollOffsets = EmptyArray,
248         consumedScroll = 0f,
249         measureResult =
250             object : MeasureResult {
251                 override val width: Int = 0
252                 override val height: Int = 0
253                 @Suppress("PrimitiveInCollection")
254                 override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
255 
placeChildrennull256                 override fun placeChildren() {}
257             },
258         canScrollForward = false,
259         isVertical = false,
260         visibleItemsInfo = emptyList(),
261         totalItemsCount = 0,
262         remeasureNeeded = false,
263         viewportSize = IntSize.Zero,
264         viewportStartOffset = 0,
265         viewportEndOffset = 0,
266         beforeContentPadding = 0,
267         afterContentPadding = 0,
268         mainAxisItemSpacing = 0,
269         slots = LazyStaggeredGridSlots(EmptyArray, EmptyArray),
270         spanProvider = LazyStaggeredGridSpanProvider(MutableIntervalList()),
271         density = Density(1f),
272         scrollBackAmount = 0f,
273         coroutineScope = CoroutineScope(EmptyCoroutineContext)
274     )
275 
visibleItemsAverageSizenull276 internal fun LazyStaggeredGridLayoutInfo.visibleItemsAverageSize(): Int {
277     val visibleItems = visibleItemsInfo
278     if (visibleItems.isEmpty()) return 0
279     val itemSizeSum =
280         visibleItems.fastSumBy {
281             if (orientation == Orientation.Vertical) {
282                 it.size.height
283             } else {
284                 it.size.width
285             }
286         }
287     return itemSizeSum / visibleItems.size + mainAxisItemSpacing
288 }
289 
290 internal val LazyStaggeredGridLayoutInfo.singleAxisViewportSize: Int
291     get() =
292         if (orientation == Orientation.Vertical) {
293             viewportSize.height
294         } else {
295             viewportSize.width
296         }
297