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