1 /*
<lambda>null2  * 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.grid
18 
19 import androidx.compose.foundation.ExperimentalFoundationApi
20 import androidx.compose.foundation.gestures.Orientation
21 import androidx.compose.foundation.gestures.snapping.offsetOnMainAxis
22 import androidx.compose.foundation.gestures.snapping.sizeOnMainAxis
23 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
24 import androidx.compose.foundation.lazy.layout.NestedPrefetchScope
25 import androidx.compose.foundation.lazy.layout.PrefetchScheduler
26 import androidx.compose.foundation.lazy.layout.UnspecifiedNestedPrefetchCount
27 import androidx.compose.runtime.Stable
28 import androidx.compose.runtime.collection.mutableVectorOf
29 
30 /**
31  * Implementations of this interface control which indices of a LazyGrid should be prefetched
32  * (precomposed and premeasured during idle time) as the user interacts with it.
33  *
34  * Implementations should invoke [LazyGridPrefetchScope.scheduleLinePrefetch] to schedule prefetches
35  * from the [onScroll] and [onVisibleItemsUpdated] callbacks. If any of the returned PrefetchHandles
36  * no longer need to be prefetched, use [LazyLayoutPrefetchState.PrefetchHandle.cancel] to cancel
37  * the request.
38  */
39 @ExperimentalFoundationApi
40 interface LazyGridPrefetchStrategy {
41 
42     /**
43      * A [PrefetchScheduler] implementation which will be used to execute prefetch requests for this
44      * strategy implementation. If null, the default [PrefetchScheduler] for the platform will be
45      * used.
46      */
47     val prefetchScheduler: PrefetchScheduler?
48         get() = null
49 
50     /**
51      * onScroll is invoked when the LazyGrid scrolls, whether or not the visible items have changed.
52      * If the visible items have also changed, then this will be invoked in the same frame *after*
53      * [onVisibleItemsUpdated].
54      *
55      * @param delta the change in scroll direction. Delta < 0 indicates scrolling down while delta >
56      *   0 indicates scrolling up.
57      * @param layoutInfo the current [LazyGridLayoutInfo]
58      */
59     fun LazyGridPrefetchScope.onScroll(delta: Float, layoutInfo: LazyGridLayoutInfo)
60 
61     /**
62      * onVisibleItemsUpdated is invoked when the LazyGrid scrolls if the visible items have changed.
63      *
64      * @param layoutInfo the current [LazyGridLayoutInfo]. Info about the updated visible items can
65      *   be found in [LazyGridLayoutInfo.visibleItemsInfo].
66      */
67     fun LazyGridPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyGridLayoutInfo)
68 
69     /**
70      * onNestedPrefetch is invoked when a parent LazyLayout has prefetched content which contains
71      * this LazyGrid. It gives this LazyGrid a chance to request prefetch for some of its own
72      * children before coming onto screen.
73      *
74      * Implementations can use [NestedPrefetchScope.schedulePrefetch] to schedule child prefetches.
75      * For example, this is useful if this LazyGrid is a LazyRow that is a child of a LazyColumn: in
76      * that case, [onNestedPrefetch] can schedule the children it expects to be visible when it
77      * comes onto screen, giving the LazyLayout infra a chance to compose these children ahead of
78      * time and reduce jank.
79      *
80      * Generally speaking, [onNestedPrefetch] should only request prefetch for children that it
81      * expects to actually be visible when this grid is scrolled into view.
82      *
83      * @param firstVisibleItemIndex the index of the first visible item. It should be used to start
84      *   prefetching from the correct index in case the grid has been created at a non-zero offset.
85      */
86     fun NestedPrefetchScope.onNestedPrefetch(firstVisibleItemIndex: Int)
87 }
88 
89 /** Scope for callbacks in [LazyGridPrefetchStrategy] which allows prefetches to be requested. */
90 @ExperimentalFoundationApi
91 interface LazyGridPrefetchScope {
92 
93     /**
94      * Schedules a prefetch for the given line index. Requests are executed in the order they're
95      * requested. If a requested prefetch is no longer necessary (for example, due to changing
96      * scroll direction), the request should be canceled via
97      * [LazyLayoutPrefetchState.PrefetchHandle.cancel].
98      *
99      * See [PrefetchScheduler].
100      *
101      * @param lineIndex index of the row or column to prefetch
102      */
scheduleLinePrefetchnull103     fun scheduleLinePrefetch(lineIndex: Int): List<LazyLayoutPrefetchState.PrefetchHandle>
104 
105     /**
106      * Schedules a prefetch for the given line index. Requests are executed in the order they're
107      * requested. If a requested prefetch is no longer necessary (for example, due to changing
108      * scroll direction), the request should be canceled via
109      * [LazyLayoutPrefetchState.PrefetchHandle.cancel].
110      *
111      * See [PrefetchScheduler].
112      *
113      * @param lineIndex index of the row or column to prefetch
114      * @param onPrefetchFinished A callback that will be invoked when the prefetching of this line
115      *   is completed. This means precomposition and premeasuring. If the request is canceled before
116      *   either phases can complete, or before all items in this line have been prepared, this
117      *   callback won't be invoked. The lineIndex and the main axis size in pixels of the prefetched
118      *   items are available as a parameter of this callback. See [LazyGridPrefetchResultScope] for
119      *   information about the line prefetched.
120      */
121     fun scheduleLinePrefetch(
122         lineIndex: Int,
123         onPrefetchFinished: (LazyGridPrefetchResultScope.() -> Unit)?
124     ): List<LazyLayoutPrefetchState.PrefetchHandle> = scheduleLinePrefetch(lineIndex)
125 }
126 
127 /**
128  * Creates an instance of the default [LazyGridPrefetchStrategy], allowing for customization of the
129  * nested prefetch count.
130  *
131  * @param nestedPrefetchItemCount specifies how many inner items should be prefetched when this
132  *   LazyGrid is nested inside another LazyLayout. For example, if this is the state for a
133  *   horizontal LazyGrid nested in a vertical LazyGrid, you might want to set this to the number of
134  *   items that will be visible when this grid is scrolled into view. If automatic nested prefetch
135  *   is enabled, this value will be used as the initial count and the strategy will adapt the count
136  *   automatically.
137  */
138 @ExperimentalFoundationApi
139 fun LazyGridPrefetchStrategy(nestedPrefetchItemCount: Int = 2): LazyGridPrefetchStrategy =
140     DefaultLazyGridPrefetchStrategy(nestedPrefetchItemCount)
141 
142 /**
143  * The default prefetching strategy for LazyGrids - this will be used automatically if no other
144  * strategy is provided.
145  */
146 @OptIn(ExperimentalFoundationApi::class)
147 @Stable
148 private class DefaultLazyGridPrefetchStrategy(private val initialNestedPrefetchItemCount: Int = 2) :
149     LazyGridPrefetchStrategy {
150 
151     /**
152      * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
153      */
154     private var lineToPrefetch = -1
155 
156     /** The list of handles associated with the items from the [lineToPrefetch] line. */
157     private val currentLinePrefetchHandles =
158         mutableVectorOf<LazyLayoutPrefetchState.PrefetchHandle>()
159 
160     /**
161      * Keeps the scrolling direction during the previous calculation in order to be able to detect
162      * the scrolling direction change.
163      */
164     private var wasScrollingForward = false
165 
166     override fun LazyGridPrefetchScope.onScroll(delta: Float, layoutInfo: LazyGridLayoutInfo) {
167         if (layoutInfo.visibleItemsInfo.isNotEmpty()) {
168             val scrollingForward = delta < 0
169             val lineToPrefetch: Int
170             val closestNextItemToPrefetch: Int
171             if (scrollingForward) {
172                 lineToPrefetch =
173                     1 +
174                         layoutInfo.visibleItemsInfo.last().let {
175                             if (layoutInfo.orientation == Orientation.Vertical) it.row
176                             else it.column
177                         }
178                 closestNextItemToPrefetch = layoutInfo.visibleItemsInfo.last().index + 1
179             } else {
180                 lineToPrefetch =
181                     -1 +
182                         layoutInfo.visibleItemsInfo.first().let {
183                             if (layoutInfo.orientation == Orientation.Vertical) it.row
184                             else it.column
185                         }
186                 closestNextItemToPrefetch = layoutInfo.visibleItemsInfo.first().index - 1
187             }
188             if (closestNextItemToPrefetch in 0 until layoutInfo.totalItemsCount) {
189                 if (
190                     lineToPrefetch != this@DefaultLazyGridPrefetchStrategy.lineToPrefetch &&
191                         lineToPrefetch >= 0
192                 ) {
193                     if (wasScrollingForward != scrollingForward) {
194                         // the scrolling direction has been changed which means the last prefetched
195                         // is not going to be reached anytime soon so it is safer to dispose it.
196                         // if this line is already visible it is safe to call the method anyway
197                         // as it will be no-op
198                         currentLinePrefetchHandles.forEach { it.cancel() }
199                     }
200                     this@DefaultLazyGridPrefetchStrategy.wasScrollingForward = scrollingForward
201                     this@DefaultLazyGridPrefetchStrategy.lineToPrefetch = lineToPrefetch
202                     currentLinePrefetchHandles.clear()
203                     currentLinePrefetchHandles.addAll(scheduleLinePrefetch(lineToPrefetch))
204                 }
205                 if (scrollingForward) {
206                     val lastItem = layoutInfo.visibleItemsInfo.last()
207                     val itemSize = lastItem.sizeOnMainAxis(layoutInfo.orientation)
208                     val itemSpacing = layoutInfo.mainAxisItemSpacing
209                     val distanceToPrefetchItem =
210                         lastItem.offsetOnMainAxis(layoutInfo.orientation) + itemSize + itemSpacing -
211                             layoutInfo.viewportEndOffset
212                     // if in the next frame we will get the same delta will we reach the item?
213                     if (distanceToPrefetchItem < -delta) {
214                         currentLinePrefetchHandles.forEach { it.markAsUrgent() }
215                     }
216                 } else {
217                     val firstItem = layoutInfo.visibleItemsInfo.first()
218                     val distanceToPrefetchItem =
219                         layoutInfo.viewportStartOffset -
220                             firstItem.offsetOnMainAxis(layoutInfo.orientation)
221                     // if in the next frame we will get the same delta will we reach the item?
222                     if (distanceToPrefetchItem < delta) {
223                         currentLinePrefetchHandles.forEach { it.markAsUrgent() }
224                     }
225                 }
226             }
227         }
228     }
229 
230     override fun LazyGridPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyGridLayoutInfo) {
231         if (lineToPrefetch != -1 && layoutInfo.visibleItemsInfo.isNotEmpty()) {
232             val expectedLineToPrefetch =
233                 if (wasScrollingForward) {
234                     layoutInfo.visibleItemsInfo.last().let {
235                         if (layoutInfo.orientation == Orientation.Vertical) it.row else it.column
236                     } + 1
237                 } else {
238                     layoutInfo.visibleItemsInfo.first().let {
239                         if (layoutInfo.orientation == Orientation.Vertical) it.row else it.column
240                     } - 1
241                 }
242             if (lineToPrefetch != expectedLineToPrefetch) {
243                 lineToPrefetch = -1
244                 currentLinePrefetchHandles.forEach { it.cancel() }
245                 currentLinePrefetchHandles.clear()
246             }
247         }
248     }
249 
250     override fun NestedPrefetchScope.onNestedPrefetch(firstVisibleItemIndex: Int) {
251         val resolvedNestedPrefetchItemCount =
252             if (nestedPrefetchItemCount == UnspecifiedNestedPrefetchCount) {
253                 initialNestedPrefetchItemCount
254             } else {
255                 nestedPrefetchItemCount
256             }
257         repeat(resolvedNestedPrefetchItemCount) { i ->
258             schedulePrecomposition(firstVisibleItemIndex + i)
259         }
260     }
261 }
262 
263 /**
264  * A scope for [LazyGridPrefetchScope.scheduleLinePrefetch] callbacks. The scope provides additional
265  * information about a prefetched item.
266  */
267 @ExperimentalFoundationApi
268 interface LazyGridPrefetchResultScope {
269 
270     /** The number of items in this prefetched line. */
271     val lineItemCount: Int
272 
273     /** The index of the prefetched line */
274     val lineIndex: Int
275 
276     /**
277      * Returns the main axis size in pixels of a prefecthed item in this line. [itemIndexInLine] is
278      * the item index from 0 to [lineItemCount] -1.
279      */
getMainAxisSizenull280     fun getMainAxisSize(itemIndexInLine: Int): Int
281 }
282 
283 @OptIn(ExperimentalFoundationApi::class)
284 @Suppress("PrimitiveInCollection")
285 internal class LazyGridPrefetchResultScopeImpl(
286     override val lineIndex: Int,
287     private val mainAxisSizes: List<Int>
288 ) : LazyGridPrefetchResultScope {
289     override val lineItemCount: Int
290         get() = mainAxisSizes.size
291 
292     override fun getMainAxisSize(itemIndexInLine: Int): Int = mainAxisSizes[itemIndexInLine]
293 }
294