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