1 /*
<lambda>null2  * Copyright 2022 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.annotation.IntRange as AndroidXIntRange
20 import androidx.compose.foundation.ExperimentalFoundationApi
21 import androidx.compose.foundation.MutatePriority
22 import androidx.compose.foundation.gestures.Orientation
23 import androidx.compose.foundation.gestures.ScrollScope
24 import androidx.compose.foundation.gestures.ScrollableState
25 import androidx.compose.foundation.gestures.stopScroll
26 import androidx.compose.foundation.interaction.InteractionSource
27 import androidx.compose.foundation.interaction.MutableInteractionSource
28 import androidx.compose.foundation.internal.checkPrecondition
29 import androidx.compose.foundation.lazy.grid.LazyGridState.Companion.Saver
30 import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
31 import androidx.compose.foundation.lazy.layout.CacheWindowLogic
32 import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
33 import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
34 import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator
35 import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
36 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
37 import androidx.compose.foundation.lazy.layout.LazyLayoutScrollDeltaBetweenPasses
38 import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
39 import androidx.compose.foundation.lazy.layout.animateScrollToItem
40 import androidx.compose.runtime.Composable
41 import androidx.compose.runtime.Stable
42 import androidx.compose.runtime.annotation.FrequentlyChangingValue
43 import androidx.compose.runtime.getValue
44 import androidx.compose.runtime.mutableStateOf
45 import androidx.compose.runtime.neverEqualPolicy
46 import androidx.compose.runtime.remember
47 import androidx.compose.runtime.saveable.Saver
48 import androidx.compose.runtime.saveable.listSaver
49 import androidx.compose.runtime.saveable.rememberSaveable
50 import androidx.compose.runtime.setValue
51 import androidx.compose.runtime.snapshots.Snapshot
52 import androidx.compose.ui.layout.AlignmentLine
53 import androidx.compose.ui.layout.MeasureResult
54 import androidx.compose.ui.layout.Remeasurement
55 import androidx.compose.ui.layout.RemeasurementModifier
56 import androidx.compose.ui.unit.Density
57 import androidx.compose.ui.util.fastForEach
58 import kotlin.coroutines.EmptyCoroutineContext
59 import kotlin.math.abs
60 import kotlin.math.roundToInt
61 import kotlinx.coroutines.CoroutineScope
62 import kotlinx.coroutines.launch
63 
64 /**
65  * Creates a [LazyGridState] that is remembered across compositions.
66  *
67  * Changes to the provided initial values will **not** result in the state being recreated or
68  * changed in any way if it has already been created.
69  *
70  * @param initialFirstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
71  * @param initialFirstVisibleItemScrollOffset the initial value for
72  *   [LazyGridState.firstVisibleItemScrollOffset]
73  */
74 @Composable
75 fun rememberLazyGridState(
76     initialFirstVisibleItemIndex: Int = 0,
77     initialFirstVisibleItemScrollOffset: Int = 0
78 ): LazyGridState {
79     return rememberSaveable(saver = LazyGridState.Saver) {
80         LazyGridState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset)
81     }
82 }
83 
84 /**
85  * Creates a [LazyGridState] that is remembered across compositions.
86  *
87  * Changes to the provided initial values will **not** result in the state being recreated or
88  * changed in any way if it has already been created.
89  *
90  * @param initialFirstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
91  * @param initialFirstVisibleItemScrollOffset the initial value for
92  *   [LazyGridState.firstVisibleItemScrollOffset]
93  * @param prefetchStrategy the [LazyGridPrefetchStrategy] to use for prefetching content in this
94  *   grid
95  */
96 @ExperimentalFoundationApi
97 @Composable
rememberLazyGridStatenull98 fun rememberLazyGridState(
99     initialFirstVisibleItemIndex: Int = 0,
100     initialFirstVisibleItemScrollOffset: Int = 0,
101     prefetchStrategy: LazyGridPrefetchStrategy = remember { LazyGridPrefetchStrategy() },
102 ): LazyGridState {
<lambda>null103     return rememberSaveable(prefetchStrategy, saver = LazyGridState.saver(prefetchStrategy)) {
104         LazyGridState(
105             initialFirstVisibleItemIndex,
106             initialFirstVisibleItemScrollOffset,
107             prefetchStrategy,
108         )
109     }
110 }
111 
112 /**
113  * Creates a [LazyGridState] that is remembered across compositions.
114  *
115  * Changes to the provided initial values will **not** result in the state being recreated or
116  * changed in any way if it has already been created.
117  *
118  * @param cacheWindow specifies the size of the ahead and behind window to be used as per
119  *   [LazyLayoutCacheWindow].
120  * @param initialFirstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
121  * @param initialFirstVisibleItemScrollOffset the initial value for
122  *   [LazyGridState.firstVisibleItemScrollOffset]
123  */
124 @ExperimentalFoundationApi
125 @Composable
rememberLazyGridStatenull126 fun rememberLazyGridState(
127     cacheWindow: LazyLayoutCacheWindow,
128     initialFirstVisibleItemIndex: Int = 0,
129     initialFirstVisibleItemScrollOffset: Int = 0
130 ): LazyGridState {
131     return rememberSaveable(cacheWindow, saver = LazyGridState.saver(cacheWindow)) {
132         LazyGridState(
133             cacheWindow,
134             initialFirstVisibleItemIndex,
135             initialFirstVisibleItemScrollOffset,
136         )
137     }
138 }
139 
140 /**
141  * A state object that can be hoisted to control and observe scrolling.
142  *
143  * In most cases, this will be created via [rememberLazyGridState].
144  *
145  * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
146  * @param firstVisibleItemScrollOffset the initial value for
147  *   [LazyGridState.firstVisibleItemScrollOffset]
148  * @param prefetchStrategy the [LazyGridPrefetchStrategy] to use for prefetching content in this
149  *   grid
150  */
151 @OptIn(ExperimentalFoundationApi::class)
152 @Stable
153 class LazyGridState
154 @ExperimentalFoundationApi
155 constructor(
156     firstVisibleItemIndex: Int = 0,
157     firstVisibleItemScrollOffset: Int = 0,
158     internal val prefetchStrategy: LazyGridPrefetchStrategy = LazyGridPrefetchStrategy(),
159 ) : ScrollableState {
160 
161     /**
162      * @param cacheWindow specifies the size of the ahead and behind window to be used as per
163      *   [LazyLayoutCacheWindow].
164      * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
165      * @param firstVisibleItemScrollOffset the initial value for
166      *   [LazyGridState.firstVisibleItemScrollOffset]
167      */
168     @ExperimentalFoundationApi
169     constructor(
170         cacheWindow: LazyLayoutCacheWindow,
171         firstVisibleItemIndex: Int = 0,
172         firstVisibleItemScrollOffset: Int = 0
173     ) : this(
174         firstVisibleItemIndex,
175         firstVisibleItemScrollOffset,
176         LazyGridCacheWindowPrefetchStrategy(cacheWindow)
177     )
178 
179     /**
180      * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
181      * @param firstVisibleItemScrollOffset the initial value for
182      *   [LazyGridState.firstVisibleItemScrollOffset]
183      */
184     constructor(
185         firstVisibleItemIndex: Int = 0,
186         firstVisibleItemScrollOffset: Int = 0
187     ) : this(firstVisibleItemIndex, firstVisibleItemScrollOffset, LazyGridPrefetchStrategy())
188 
189     internal var hasLookaheadOccurred: Boolean = false
190         private set
191 
192     internal var approachLayoutInfo: LazyGridMeasureResult? = null
193         private set
194 
195     // always execute requests in high priority
196     private var executeRequestsInHighPriorityMode = false
197 
198     /** The holder class for the current scroll position. */
199     private val scrollPosition =
200         LazyGridScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
201 
202     /**
203      * The index of the first item that is visible within the scrollable viewport area, this means,
204      * not including items in the content padding region. For the first visible item that includes
205      * items in the content padding please use [LazyGridLayoutInfo.visibleItemsInfo].
206      *
207      * Note that this property is observable and if you use it in the composable function it will be
208      * recomposed on every change causing potential performance issues.
209      *
210      * If you want to run some side effects like sending an analytics event or updating a state
211      * based on this value consider using "snapshotFlow":
212      *
213      * @sample androidx.compose.foundation.samples.UsingGridScrollPositionForSideEffectSample
214      *
215      * If you need to use it in the composition then consider wrapping the calculation into a
216      * derived state in order to only have recompositions when the derived value changes:
217      *
218      * @sample androidx.compose.foundation.samples.UsingGridScrollPositionInCompositionSample
219      */
220     val firstVisibleItemIndex: Int
221         @FrequentlyChangingValue get() = scrollPosition.index
222 
223     /**
224      * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the amount
225      * that the item is offset backwards
226      */
227     val firstVisibleItemScrollOffset: Int
228         @FrequentlyChangingValue get() = scrollPosition.scrollOffset
229 
230     /** Backing state for [layoutInfo] */
231     private val layoutInfoState = mutableStateOf(EmptyLazyGridLayoutInfo, neverEqualPolicy())
232 
233     /**
234      * The object of [LazyGridLayoutInfo] calculated during the last layout pass. For example, you
235      * can use it to calculate what items are currently visible.
236      *
237      * Note that this property is observable and is updated after every scroll or remeasure. If you
238      * use it in the composable function it will be recomposed on every change causing potential
239      * performance issues including infinity recomposition loop. Therefore, avoid using it in the
240      * composition.
241      *
242      * If you want to run some side effects like sending an analytics event or updating a state
243      * based on this value consider using "snapshotFlow":
244      *
245      * @sample androidx.compose.foundation.samples.UsingGridLayoutInfoForSideEffectSample
246      */
247     val layoutInfo: LazyGridLayoutInfo
248         @FrequentlyChangingValue get() = layoutInfoState.value
249 
250     /**
251      * [InteractionSource] that will be used to dispatch drag events when this grid is being
252      * dragged. If you want to know whether the fling (or animated scroll) is in progress, use
253      * [isScrollInProgress].
254      */
255     val interactionSource: InteractionSource
256         get() = internalInteractionSource
257 
258     internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()
259 
260     /**
261      * The amount of scroll to be consumed in the next layout pass. Scrolling forward is negative
262      * - that is, it is the amount that the items are offset in y
263      */
264     internal var scrollToBeConsumed = 0f
265         private set
266 
267     internal val slotsPerLine: Int
268         get() = layoutInfoState.value.slotsPerLine
269 
270     internal val density: Density
271         get() = layoutInfoState.value.density
272 
273     /**
274      * The ScrollableController instance. We keep it as we need to call stopAnimation on it once we
275      * reached the end of the grid.
276      */
<lambda>null277     private val scrollableState = ScrollableState { -onScroll(-it) }
278 
279     /** Only used for testing to confirm that we're not making too many measure passes */
280     /*@VisibleForTesting*/
281     internal var numMeasurePasses: Int = 0
282         private set
283 
284     /** Only used for testing to disable prefetching when needed to test the main logic. */
285     /*@VisibleForTesting*/
286     internal var prefetchingEnabled: Boolean = true
287 
288     /**
289      * The [Remeasurement] object associated with our layout. It allows us to remeasure
290      * synchronously during scroll.
291      */
292     internal var remeasurement: Remeasurement? = null
293         private set
294 
295     /** The modifier which provides [remeasurement]. */
296     internal val remeasurementModifier =
297         object : RemeasurementModifier {
onRemeasurementAvailablenull298             override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
299                 this@LazyGridState.remeasurement = remeasurement
300             }
301         }
302 
303     /**
304      * Provides a modifier which allows to delay some interactions (e.g. scroll) until layout is
305      * ready.
306      */
307     internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
308 
309     internal val itemAnimator = LazyLayoutItemAnimator<LazyGridMeasuredItem>()
310 
311     internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
312 
313     internal val prefetchState =
<lambda>null314         LazyLayoutPrefetchState(prefetchStrategy.prefetchScheduler) {
315             with(prefetchStrategy) {
316                 onNestedPrefetch(Snapshot.withoutReadObservation { firstVisibleItemIndex })
317             }
318         }
319 
320     private val prefetchScope: LazyGridPrefetchScope =
321         object : LazyGridPrefetchScope {
scheduleLinePrefetchnull322             override fun scheduleLinePrefetch(
323                 lineIndex: Int
324             ): List<LazyLayoutPrefetchState.PrefetchHandle> {
325                 return scheduleLinePrefetch(lineIndex, null)
326             }
327 
328             @Suppress("PrimitiveInCollection")
scheduleLinePrefetchnull329             override fun scheduleLinePrefetch(
330                 lineIndex: Int,
331                 onPrefetchFinished: (LazyGridPrefetchResultScope.() -> Unit)?
332             ): List<LazyLayoutPrefetchState.PrefetchHandle> {
333                 // Without read observation since this can be triggered from scroll - this will then
334                 // cause us to recompose when the measure result changes. We don't care since the
335                 // prefetch is best effort.
336                 val prefetchHandles = mutableListOf<LazyLayoutPrefetchState.PrefetchHandle>()
337                 val itemSizes: MutableList<Int>? =
338                     if (onPrefetchFinished == null) null else mutableListOf()
339 
340                 Snapshot.withoutReadObservation {
341                     val layoutInfo =
342                         if (hasLookaheadOccurred) {
343                             approachLayoutInfo
344                         } else {
345                             layoutInfoState.value
346                         }
347 
348                     layoutInfo?.let { measureResult ->
349                         var completedCount = 1
350                         val itemsInLineInfo = measureResult.prefetchInfoRetriever(lineIndex)
351                         itemsInLineInfo.fastForEach { lineInfo ->
352                             prefetchHandles.add(
353                                 prefetchState.schedulePrecompositionAndPremeasure(
354                                     lineInfo.first,
355                                     lineInfo.second,
356                                     executeRequestsInHighPriorityMode
357                                 ) {
358                                     var itemMainAxisItemSize = 0
359                                     repeat(placeablesCount) {
360                                         itemMainAxisItemSize +=
361                                             if (measureResult.orientation == Orientation.Vertical) {
362                                                 getSize(it).height
363                                             } else {
364                                                 getSize(it).width
365                                             }
366                                     }
367 
368                                     itemSizes?.add(itemMainAxisItemSize)
369                                     // all items in this line were prefetched, report the size
370                                     if (completedCount == itemsInLineInfo.size) {
371                                         if (onPrefetchFinished != null && itemSizes != null) {
372                                             onPrefetchFinished.invoke(
373                                                 LazyGridPrefetchResultScopeImpl(
374                                                     lineIndex,
375                                                     itemSizes
376                                                 )
377                                             )
378                                         }
379                                     } else {
380                                         completedCount++
381                                     }
382                                 }
383                             )
384                         }
385                     }
386                 }
387                 return prefetchHandles
388             }
389         }
390 
391     /** Stores currently pinned items which are always composed. */
392     internal val pinnedItems = LazyLayoutPinnedItemList()
393 
394     internal val nearestRange: IntRange by scrollPosition.nearestRangeState
395 
396     internal val placementScopeInvalidator = ObservableScopeInvalidator()
397 
398     /**
399      * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
400      * pixels.
401      *
402      * @param index the index to which to scroll. Must be non-negative.
403      * @param scrollOffset the offset that the item should end up after the scroll. Note that
404      *   positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
405      *   scroll the item further upward (taking it partly offscreen).
406      */
scrollToItemnull407     suspend fun scrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) {
408         scroll { snapToItemIndexInternal(index, scrollOffset, forceRemeasure = true) }
409     }
410 
411     internal val measurementScopeInvalidator = ObservableScopeInvalidator()
412 
413     /**
414      * Requests the item at [index] to be at the start of the viewport during the next remeasure,
415      * offset by [scrollOffset], and schedules a remeasure.
416      *
417      * The scroll position will be updated to the requested position rather than maintain the index
418      * based on the first visible item key (when a data set change will also be applied during the
419      * next remeasure), but *only* for the next remeasure.
420      *
421      * Any scroll in progress will be cancelled.
422      *
423      * @param index the index to which to scroll. Must be non-negative.
424      * @param scrollOffset the offset that the item should end up after the scroll. Note that
425      *   positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
426      *   scroll the item further upward (taking it partly offscreen).
427      */
requestScrollToItemnull428     fun requestScrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) {
429         // Cancel any scroll in progress.
430         if (isScrollInProgress) {
431             layoutInfoState.value.coroutineScope.launch { stopScroll() }
432         }
433 
434         snapToItemIndexInternal(index, scrollOffset, forceRemeasure = false)
435     }
436 
snapToItemIndexInternalnull437     internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int, forceRemeasure: Boolean) {
438         val positionChanged =
439             scrollPosition.index != index || scrollPosition.scrollOffset != scrollOffset
440         // sometimes this method is called not to scroll, but to stay on the same index when
441         // the data changes, as by default we maintain the scroll position by key, not index.
442         // when this happens we don't need to reset the animations as from the user perspective
443         // we didn't scroll anywhere and if there is an offset change for an item, this change
444         // should be animated.
445         // however, when the request is to really scroll to a different position, we have to
446         // reset previously known item positions as we don't want offset changes to be animated.
447         // this offset should be considered as a scroll, not the placement change.
448         if (positionChanged) {
449             itemAnimator.reset()
450             // we changed positions, cancel existing requests and wait for the next scroll to
451             // refill the window
452             (prefetchStrategy as? CacheWindowLogic)?.resetStrategy()
453         }
454         scrollPosition.requestPositionAndForgetLastKnownKey(index, scrollOffset)
455         if (forceRemeasure) {
456             remeasurement?.forceRemeasure()
457         } else {
458             measurementScopeInvalidator.invalidateScope()
459         }
460     }
461 
462     /**
463      * Call this function to take control of scrolling and gain the ability to send scroll events
464      * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
465      * performed within a [scroll] block (even if they don't call any other methods on this object)
466      * in order to guarantee that mutual exclusion is enforced.
467      *
468      * If [scroll] is called from elsewhere, this will be canceled.
469      */
scrollnull470     override suspend fun scroll(
471         scrollPriority: MutatePriority,
472         block: suspend ScrollScope.() -> Unit
473     ) {
474         awaitLayoutModifier.waitForFirstLayout()
475         scrollableState.scroll(scrollPriority, block)
476     }
477 
dispatchRawDeltanull478     override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta)
479 
480     override val isScrollInProgress: Boolean
481         get() = scrollableState.isScrollInProgress
482 
483     override var canScrollForward: Boolean by mutableStateOf(false)
484         private set
485 
486     override var canScrollBackward: Boolean by mutableStateOf(false)
487         private set
488 
489     @get:Suppress("GetterSetterNames")
490     override val lastScrolledForward: Boolean
491         get() = scrollableState.lastScrolledForward
492 
493     @get:Suppress("GetterSetterNames")
494     override val lastScrolledBackward: Boolean
495         get() = scrollableState.lastScrolledBackward
496 
497     // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
498     //  fine-grained control over scrolling
499     /*@VisibleForTesting*/
500     internal fun onScroll(distance: Float): Float {
501         if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
502             return 0f
503         }
504         checkPrecondition(abs(scrollToBeConsumed) <= 0.5f) {
505             "entered drag with non-zero pending scroll"
506         }
507         scrollToBeConsumed += distance
508 
509         // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
510         // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
511         // we have less than 0.5 pixels
512         if (abs(scrollToBeConsumed) > 0.5f) {
513             val preScrollToBeConsumed = scrollToBeConsumed
514             val intDelta = scrollToBeConsumed.roundToInt()
515             var scrolledLayoutInfo =
516                 layoutInfoState.value.copyWithScrollDeltaWithoutRemeasure(
517                     delta = intDelta,
518                     updateAnimations = !hasLookaheadOccurred
519                 )
520             if (scrolledLayoutInfo != null && this.approachLayoutInfo != null) {
521                 // if we were able to scroll the lookahead layout info without remeasure, lets
522                 // try to do the same for post lookahead layout info (sometimes they diverge).
523                 val scrolledApproachLayoutInfo =
524                     approachLayoutInfo?.copyWithScrollDeltaWithoutRemeasure(
525                         delta = intDelta,
526                         updateAnimations = true
527                     )
528                 if (scrolledApproachLayoutInfo != null) {
529                     // we can apply scroll delta for both phases without remeasure
530                     approachLayoutInfo = scrolledApproachLayoutInfo
531                 } else {
532                     // we can't apply scroll delta for post lookahead, so we have to remeasure
533                     scrolledLayoutInfo = null
534                 }
535             }
536             if (scrolledLayoutInfo != null) {
537                 applyMeasureResult(
538                     result = scrolledLayoutInfo,
539                     isLookingAhead = hasLookaheadOccurred,
540                     visibleItemsStayedTheSame = true
541                 )
542                 // we don't need to remeasure, so we only trigger re-placement:
543                 placementScopeInvalidator.invalidateScope()
544 
545                 notifyPrefetchOnScroll(
546                     preScrollToBeConsumed - scrollToBeConsumed,
547                     scrolledLayoutInfo
548                 )
549             } else {
550                 remeasurement?.forceRemeasure()
551                 notifyPrefetchOnScroll(preScrollToBeConsumed - scrollToBeConsumed, this.layoutInfo)
552             }
553         }
554 
555         // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
556         if (abs(scrollToBeConsumed) <= 0.5f) {
557             // We consumed all of it - we'll hold onto the fractional scroll for later, so report
558             // that we consumed the whole thing
559             return distance
560         } else {
561             val scrollConsumed = distance - scrollToBeConsumed
562             // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
563             // nested scrolling)
564             scrollToBeConsumed = 0f // We're not consuming the rest, give it back
565             return scrollConsumed
566         }
567     }
568 
notifyPrefetchOnScrollnull569     private fun notifyPrefetchOnScroll(delta: Float, layoutInfo: LazyGridLayoutInfo) {
570         if (prefetchingEnabled) {
571             with(prefetchStrategy) { prefetchScope.onScroll(delta, layoutInfo) }
572         }
573     }
574 
575     private val numOfItemsToTeleport: Int
576         get() = 100 * slotsPerLine
577 
578     /**
579      * Animate (smooth scroll) to the given item.
580      *
581      * @param index the index to which to scroll. Must be non-negative.
582      * @param scrollOffset the offset that the item should end up after the scroll. Note that
583      *   positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
584      *   scroll the item further upward (taking it partly offscreen).
585      */
animateScrollToItemnull586     suspend fun animateScrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) {
587         scroll {
588             LazyLayoutScrollScope(this@LazyGridState, this)
589                 .animateScrollToItem(index, scrollOffset, numOfItemsToTeleport, density)
590         }
591     }
592 
593     /** Updates the state with the new calculated scroll position and consumed scroll. */
applyMeasureResultnull594     internal fun applyMeasureResult(
595         result: LazyGridMeasureResult,
596         isLookingAhead: Boolean,
597         visibleItemsStayedTheSame: Boolean = false
598     ) {
599         // update the prefetch state with the number of nested prefetch items this layout
600         // should use.
601         prefetchState.idealNestedPrefetchCount = result.visibleItemsInfo.size
602 
603         if (!isLookingAhead && hasLookaheadOccurred) {
604             // If there was already a lookahead pass, record this result as Approach result
605             approachLayoutInfo = result
606         } else {
607             if (isLookingAhead) {
608                 hasLookaheadOccurred = true
609             }
610             scrollToBeConsumed -= result.consumedScroll
611             layoutInfoState.value = result
612 
613             canScrollBackward = result.canScrollBackward
614             canScrollForward = result.canScrollForward
615 
616             if (visibleItemsStayedTheSame) {
617                 scrollPosition.updateScrollOffset(result.firstVisibleLineScrollOffset)
618             } else {
619                 scrollPosition.updateFromMeasureResult(result)
620                 if (prefetchingEnabled) {
621                     with(prefetchStrategy) { prefetchScope.onVisibleItemsUpdated(result) }
622                 }
623             }
624 
625             if (isLookingAhead) {
626                 _lazyLayoutScrollDeltaBetweenPasses.updateScrollDeltaForApproach(
627                     result.scrollBackAmount,
628                     result.density,
629                     result.coroutineScope
630                 )
631             }
632             numMeasurePasses++
633         }
634     }
635 
636     private val _lazyLayoutScrollDeltaBetweenPasses = LazyLayoutScrollDeltaBetweenPasses()
637 
638     internal val scrollDeltaBetweenPasses
639         get() = _lazyLayoutScrollDeltaBetweenPasses.scrollDeltaBetweenPasses
640 
641     /**
642      * When the user provided custom keys for the items we can try to detect when there were items
643      * added or removed before our current first visible item and keep this item as the first
644      * visible one even given that its index has been changed.
645      */
updateScrollPositionIfTheFirstItemWasMovednull646     internal fun updateScrollPositionIfTheFirstItemWasMoved(
647         itemProvider: LazyGridItemProvider,
648         firstItemIndex: Int
649     ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex)
650 
651     companion object {
652         /** The default [Saver] implementation for [LazyGridState]. */
653         val Saver: Saver<LazyGridState, *> =
654             listSaver(
655                 save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
656                 restore = {
657                     LazyGridState(
658                         firstVisibleItemIndex = it[0],
659                         firstVisibleItemScrollOffset = it[1]
660                     )
661                 }
662             )
663 
664         /**
665          * A [Saver] implementation for [LazyGridState] that handles setting a custom
666          * [LazyGridPrefetchStrategy].
667          */
668         @ExperimentalFoundationApi
669         internal fun saver(prefetchStrategy: LazyGridPrefetchStrategy): Saver<LazyGridState, *> =
670             listSaver(
671                 save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
672                 restore = {
673                     LazyGridState(
674                         firstVisibleItemIndex = it[0],
675                         firstVisibleItemScrollOffset = it[1],
676                         prefetchStrategy
677                     )
678                 }
679             )
680 
681         /**
682          * A [Saver] implementation for [LazyGridState] that handles setting a custom
683          * [LazyLayoutCacheWindow].
684          */
685         @ExperimentalFoundationApi
686         internal fun saver(cacheWindow: LazyLayoutCacheWindow): Saver<LazyGridState, *> =
687             listSaver(
688                 save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
689                 restore = {
690                     LazyGridState(
691                         cacheWindow = cacheWindow,
692                         firstVisibleItemIndex = it[0],
693                         firstVisibleItemScrollOffset = it[1],
694                     )
695                 }
696             )
697     }
698 }
699 
700 private val EmptyLazyGridLayoutInfo =
701     LazyGridMeasureResult(
702         firstVisibleLine = null,
703         firstVisibleLineScrollOffset = 0,
704         canScrollForward = false,
705         consumedScroll = 0f,
706         measureResult =
707             object : MeasureResult {
708                 override val width: Int = 0
709                 override val height: Int = 0
710 
711                 @Suppress("PrimitiveInCollection")
712                 override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
713 
placeChildrennull714                 override fun placeChildren() {}
715             },
716         scrollBackAmount = 0f,
717         visibleItemsInfo = emptyList(),
718         viewportStartOffset = 0,
719         viewportEndOffset = 0,
720         totalItemsCount = 0,
721         reverseLayout = false,
722         orientation = Orientation.Vertical,
723         afterContentPadding = 0,
724         mainAxisItemSpacing = 0,
725         remeasureNeeded = false,
726         density = Density(1f),
727         slotsPerLine = 0,
728         coroutineScope = CoroutineScope(EmptyCoroutineContext),
<lambda>null729         prefetchInfoRetriever = { emptyList() }
730     )
731