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