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.grid 18 19 import androidx.compose.foundation.ExperimentalFoundationApi 20 import androidx.compose.foundation.gestures.snapping.offsetOnMainAxis 21 import androidx.compose.foundation.gestures.snapping.sizeOnMainAxis 22 import androidx.compose.foundation.lazy.layout.CacheWindowLogic 23 import androidx.compose.foundation.lazy.layout.CacheWindowScope 24 import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow 25 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle 26 import androidx.compose.foundation.lazy.layout.NestedPrefetchScope 27 import androidx.compose.ui.unit.Density 28 import androidx.compose.ui.util.fastFilter 29 import androidx.compose.ui.util.fastForEach 30 import kotlin.math.absoluteValue 31 32 @ExperimentalFoundationApi 33 internal class LazyGridCacheWindowPrefetchStrategy(cacheWindow: LazyLayoutCacheWindow) : 34 CacheWindowLogic(cacheWindow), LazyGridPrefetchStrategy { 35 private val cacheWindowScope = LazyGridCacheWindowScope() 36 onScrollnull37 override fun LazyGridPrefetchScope.onScroll(delta: Float, layoutInfo: LazyGridLayoutInfo) { 38 applyWindowScope(layoutInfo) { onScroll(delta) } 39 } 40 onVisibleItemsUpdatednull41 override fun LazyGridPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyGridLayoutInfo) { 42 applyWindowScope(layoutInfo) { onVisibleItemsUpdated() } 43 } 44 onNestedPrefetchnull45 override fun NestedPrefetchScope.onNestedPrefetch(firstVisibleItemIndex: Int) { 46 repeat(nestedPrefetchItemCount) { schedulePrecomposition(firstVisibleItemIndex + it) } 47 } 48 49 /** Adapts the LazyGridPrefetchScope and LazyGridLayoutInfo to a single scope. */ applyWindowScopenull50 private inline fun LazyGridPrefetchScope.applyWindowScope( 51 layoutInfo: LazyGridLayoutInfo, 52 block: CacheWindowScope.() -> Unit 53 ) { 54 cacheWindowScope.layoutInfo = layoutInfo 55 cacheWindowScope.prefetchScope = this 56 block(cacheWindowScope) 57 } 58 } 59 60 @ExperimentalFoundationApi 61 private class LazyGridCacheWindowScope() : CacheWindowScope { 62 lateinit var layoutInfo: LazyGridLayoutInfo 63 lateinit var prefetchScope: LazyGridPrefetchScope 64 65 override val totalItemsCount: Int 66 get() = layoutInfo.totalItemsCount 67 68 override val hasVisibleItems: Boolean 69 get() = layoutInfo.visibleItemsInfo.isNotEmpty() 70 71 override val mainAxisExtraSpaceStart: Int 72 get() { 73 val firstVisibleItem = layoutInfo.visibleItemsInfo.first() 74 // how much of the first item is peeking out of view at the start of the layout. 75 val firstItemOverflowOffset = 76 (firstVisibleItem.offsetOnMainAxis(layoutInfo.orientation) + 77 layoutInfo.beforeContentPadding) 78 .coerceAtMost(0) 79 // extra space is always positive in this context 80 return firstItemOverflowOffset.absoluteValue 81 } 82 83 override val mainAxisExtraSpaceEnd: Int 84 get() { 85 val lastVisibleItem = layoutInfo.visibleItemsInfo.last() 86 // how much of the last item is peeking out of view at the end of the layout 87 val lastItemOverflowOffset = 88 lastVisibleItem.offsetOnMainAxis(layoutInfo.orientation) + 89 lastVisibleItem.sizeOnMainAxis(orientation = layoutInfo.orientation) + 90 layoutInfo.mainAxisItemSpacing 91 92 // extra space is always positive in this context 93 return (lastItemOverflowOffset - layoutInfo.viewportEndOffset).absoluteValue 94 } 95 96 override val firstVisibleLineIndex: Int 97 get() { 98 return layoutInfo.visibleItemsInfo.first().lineIndex 99 } 100 101 override val lastVisibleLineIndex: Int 102 get() = layoutInfo.visibleItemsInfo.last().lineIndex 103 104 override val mainAxisViewportSize: Int 105 get() = layoutInfo.singleAxisViewportSize 106 107 override val density: Density? 108 get() = (layoutInfo as? LazyGridMeasureResult)?.density 109 schedulePrefetchnull110 override fun schedulePrefetch( 111 lineIndex: Int, 112 onItemPrefetched: (Int, Int) -> Unit 113 ): List<PrefetchHandle> { 114 return prefetchScope.scheduleLinePrefetch(lineIndex) { 115 var tallestElement = Int.MIN_VALUE 116 repeat(lineItemCount) { tallestElement = maxOf(getMainAxisSize(it)) } 117 if (tallestElement != Int.MIN_VALUE) { 118 onItemPrefetched(lineIndex, tallestElement) 119 } 120 } 121 } 122 123 override val visibleLineCount: Int 124 get() = lastVisibleLineIndex - firstVisibleLineIndex + 1 125 getVisibleItemSizenull126 override fun getVisibleItemSize(indexInVisibleLines: Int): Int { 127 val laneIndex = indexInVisibleLines + firstVisibleLineIndex 128 var tallestItemSize = 0 129 layoutInfo.visibleItemsInfo 130 .fastFilter { it.lineIndex == laneIndex } 131 .fastForEach { 132 tallestItemSize = 133 maxOf(it.sizeOnMainAxis(orientation = layoutInfo.orientation), tallestItemSize) 134 } 135 136 return tallestItemSize 137 } 138 getVisibleItemLinenull139 override fun getVisibleItemLine(indexInVisibleLines: Int): Int = 140 firstVisibleLineIndex + indexInVisibleLines 141 142 val LazyGridItemInfo.lineIndex: Int 143 get() = lineIndex(layoutInfo.orientation) 144 } 145