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