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