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