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