1 /*
2  * Copyright (C) 2024 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 com.android.intentresolver.contentpreview.payloadtoggle.ui.composable
18 
19 import androidx.compose.foundation.ExperimentalFoundationApi
20 import androidx.compose.foundation.lazy.LazyListItemInfo
21 import androidx.compose.foundation.lazy.LazyListLayoutInfo
22 import androidx.compose.foundation.lazy.LazyListPrefetchScope
23 import androidx.compose.foundation.lazy.LazyListPrefetchStrategy
24 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
25 import androidx.compose.foundation.lazy.layout.NestedPrefetchScope
26 
27 /** Prefetch strategy to fetch items ahead and behind the current scroll position. */
28 @OptIn(ExperimentalFoundationApi::class)
29 class ShareouselLazyListPrefetchStrategy(
30     private val lookAhead: Int = 4,
31     private val lookBackward: Int = 1
32 ) : LazyListPrefetchStrategy {
33     // Map of index -> prefetch handle
34     private val prefetchHandles: MutableMap<Int, LazyLayoutPrefetchState.PrefetchHandle> =
35         mutableMapOf()
36 
37     private var prefetchRange = IntRange.EMPTY
38 
39     private enum class ScrollDirection {
40         UNKNOWN, // The user hasn't scrolled in either direction yet.
41         FORWARD,
42         BACKWARD,
43     }
44 
45     private var scrollDirection: ScrollDirection = ScrollDirection.UNKNOWN
46 
onScrollnull47     override fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo) {
48         if (layoutInfo.visibleItemsInfo.isNotEmpty()) {
49             scrollDirection = if (delta < 0) ScrollDirection.FORWARD else ScrollDirection.BACKWARD
50             updatePrefetchSet(layoutInfo.visibleItemsInfo)
51         }
52 
53         if (scrollDirection == ScrollDirection.FORWARD) {
54             val lastItem = layoutInfo.visibleItemsInfo.last()
55             val spacing = layoutInfo.mainAxisItemSpacing
56             val distanceToPrefetchItem =
57                 lastItem.offset + lastItem.size + spacing - layoutInfo.viewportEndOffset
58             // if in the next frame we will get the same delta will we reach the item?
59             if (distanceToPrefetchItem < -delta) {
60                 prefetchHandles.get(lastItem.index + 1)?.markAsUrgent()
61             }
62         } else {
63             val firstItem = layoutInfo.visibleItemsInfo.first()
64             val distanceToPrefetchItem = layoutInfo.viewportStartOffset - firstItem.offset
65             // if in the next frame we will get the same delta will we reach the item?
66             if (distanceToPrefetchItem < delta) {
67                 prefetchHandles.get(firstItem.index - 1)?.markAsUrgent()
68             }
69         }
70     }
71 
onVisibleItemsUpdatednull72     override fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo) {
73         if (layoutInfo.visibleItemsInfo.isNotEmpty()) {
74             updatePrefetchSet(layoutInfo.visibleItemsInfo)
75         }
76     }
77 
onNestedPrefetchnull78     override fun NestedPrefetchScope.onNestedPrefetch(firstVisibleItemIndex: Int) {}
79 
getVisibleRangenull80     private fun getVisibleRange(visibleItems: List<LazyListItemInfo>) =
81         if (visibleItems.isEmpty()) IntRange.EMPTY
82         else IntRange(visibleItems.first().index, visibleItems.last().index)
83 
84     /** Update prefetchRange based upon the visible item range and scroll direction. */
85     private fun updatePrefetchRange(visibleRange: IntRange) {
86         prefetchRange =
87             when (scrollDirection) {
88                 // Prefetch in both directions
89                 ScrollDirection.UNKNOWN ->
90                     visibleRange.first - lookAhead / 2..visibleRange.last + lookAhead / 2
91                 ScrollDirection.FORWARD ->
92                     visibleRange.first - lookBackward..visibleRange.last + lookAhead
93                 ScrollDirection.BACKWARD ->
94                     visibleRange.first - lookAhead..visibleRange.last + lookBackward
95             }
96     }
97 
LazyListPrefetchScopenull98     private fun LazyListPrefetchScope.updatePrefetchSet(visibleItems: List<LazyListItemInfo>) {
99         val visibleRange = getVisibleRange(visibleItems)
100         updatePrefetchRange(visibleRange)
101         updatePrefetchOperations(visibleRange)
102     }
103 
LazyListPrefetchScopenull104     private fun LazyListPrefetchScope.updatePrefetchOperations(visibleItemsRange: IntRange) {
105         // Remove any fetches outside of the prefetch range or inside the visible range
106         prefetchHandles
107             .filterKeys { it !in prefetchRange || it in visibleItemsRange }
108             .forEach {
109                 it.value.cancel()
110                 prefetchHandles.remove(it.key)
111             }
112 
113         // Ensure all non-visible items in the range are being prefetched
114         prefetchRange.forEach {
115             if (it !in visibleItemsRange && !prefetchHandles.containsKey(it)) {
116                 prefetchHandles[it] = schedulePrefetch(it)
117             }
118         }
119     }
120 }
121