1 /* 2 * Copyright 2025 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.gestures.snapping.singleAxisViewportSize 21 import androidx.compose.foundation.lazy.layout.CacheWindowLogic 22 import androidx.compose.foundation.lazy.layout.CacheWindowScope 23 import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow 24 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle 25 import androidx.compose.foundation.lazy.layout.NestedPrefetchScope 26 import androidx.compose.ui.unit.Density 27 import kotlin.math.absoluteValue 28 29 /** 30 * This is a transition class based on [androidx.compose.foundation.lazy.LazyListPrefetchStrategy] 31 * where we will perform a window based prefetching for items in the direction of the scroll 32 * movement (ahead). 33 */ 34 @OptIn(ExperimentalFoundationApi::class) 35 internal class LazyListCacheWindowStrategy(cacheWindow: LazyLayoutCacheWindow) : 36 CacheWindowLogic(cacheWindow), LazyListPrefetchStrategy { 37 private val cacheWindowScope = LazyListCacheWindowScope() 38 onScrollnull39 override fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo) { 40 applyWindowScope(layoutInfo) { onScroll(delta) } 41 } 42 onVisibleItemsUpdatednull43 override fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo) { 44 applyWindowScope(layoutInfo) { onVisibleItemsUpdated() } 45 } 46 onNestedPrefetchnull47 override fun NestedPrefetchScope.onNestedPrefetch(firstVisibleItemIndex: Int) { 48 val resolvedNestedPrefetchItemCount = 49 if (nestedPrefetchItemCount == -1) { 50 DefaultNestedPrefetchCount 51 } else { 52 nestedPrefetchItemCount 53 } 54 repeat(resolvedNestedPrefetchItemCount) { 55 schedulePrecomposition(firstVisibleItemIndex + it) 56 } 57 } 58 59 /** Adapts the LazyListPrefetchScope and LazyListLayoutInfo to a single scope. */ applyWindowScopenull60 private inline fun LazyListPrefetchScope.applyWindowScope( 61 layoutInfo: LazyListLayoutInfo, 62 block: CacheWindowScope.() -> Unit 63 ) { 64 cacheWindowScope.layoutInfo = layoutInfo 65 cacheWindowScope.prefetchScope = this 66 block(cacheWindowScope) 67 } 68 } 69 70 @OptIn(ExperimentalFoundationApi::class) 71 internal class LazyListCacheWindowScope() : CacheWindowScope { 72 lateinit var layoutInfo: LazyListLayoutInfo 73 lateinit var prefetchScope: LazyListPrefetchScope 74 75 override val totalItemsCount: Int 76 get() = layoutInfo.totalItemsCount 77 78 override val hasVisibleItems: Boolean 79 get() = layoutInfo.visibleItemsInfo.isNotEmpty() 80 81 override val mainAxisExtraSpaceStart: Int 82 get() { 83 val firstVisibleItem = layoutInfo.visibleItemsInfo.first() 84 // how much of the first item is peeking out of view at the start of the layout. 85 val firstItemOverflowOffset = 86 (firstVisibleItem.offset + layoutInfo.beforeContentPadding).coerceAtMost(0) 87 // extra space is always positive in this context 88 return firstItemOverflowOffset.absoluteValue 89 } 90 91 override val mainAxisExtraSpaceEnd: Int 92 get() { 93 val lastVisibleItem = layoutInfo.visibleItemsInfo.last() 94 // how much of the last item is peeking out of view at the end of the layout 95 val lastItemOverflowOffset = 96 lastVisibleItem.offset + lastVisibleItem.size + layoutInfo.mainAxisItemSpacing 97 98 // extra space is always positive in this context 99 return (lastItemOverflowOffset - layoutInfo.viewportEndOffset).absoluteValue 100 } 101 102 override val firstVisibleLineIndex: Int 103 get() = layoutInfo.visibleItemsInfo.first().index 104 105 override val lastVisibleLineIndex: Int 106 get() = layoutInfo.visibleItemsInfo.last().index 107 108 override val mainAxisViewportSize: Int 109 get() = layoutInfo.singleAxisViewportSize 110 111 override val density: Density? 112 get() = (layoutInfo as? LazyListMeasureResult)?.density 113 schedulePrefetchnull114 override fun schedulePrefetch( 115 lineIndex: Int, 116 onItemPrefetched: (Int, Int) -> Unit 117 ): List<PrefetchHandle> { 118 return listOf( 119 prefetchScope.schedulePrefetch( 120 lineIndex, 121 { onItemPrefetched.invoke(index, mainAxisSize) } 122 ) 123 ) 124 } 125 126 override val visibleLineCount: Int 127 get() = layoutInfo.visibleItemsInfo.size 128 getVisibleItemSizenull129 override fun getVisibleItemSize(indexInVisibleLines: Int): Int = 130 layoutInfo.visibleItemsInfo[indexInVisibleLines].size 131 132 override fun getVisibleItemLine(indexInVisibleLines: Int): Int = 133 layoutInfo.visibleItemsInfo[indexInVisibleLines].index 134 } 135 136 // we use 2 here because nested list have usually > 1 visible elements, so 2 is the minimum 137 // logical value we could use. 138 private const val DefaultNestedPrefetchCount = 2 139