1 /*
2 * Copyright 2021 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.compose.foundation.gestures.Orientation
20 import androidx.compose.foundation.gestures.snapping.offsetOnMainAxis
21 import androidx.compose.ui.layout.MeasureResult
22 import androidx.compose.ui.unit.Constraints
23 import androidx.compose.ui.unit.Density
24 import androidx.compose.ui.unit.IntSize
25 import androidx.compose.ui.util.fastForEach
26 import kotlinx.coroutines.CoroutineScope
27
28 /** The result of the measure pass for lazy grid layout. */
29 internal class LazyGridMeasureResult(
30 // properties defining the scroll position:
31 /** The new first visible line of items. */
32 val firstVisibleLine: LazyGridMeasuredLine?,
33 /** The new value for [LazyGridState.firstVisibleItemScrollOffset]. */
34 val firstVisibleLineScrollOffset: Int,
35 /** True if there is some space available to continue scrolling in the forward direction. */
36 val canScrollForward: Boolean,
37 /** The amount of scroll consumed during the measure pass. */
38 val consumedScroll: Float,
39 /** MeasureResult defining the layout. */
40 private val measureResult: MeasureResult,
41 /** The amount of scroll-back that happened due to reaching the end of the list. */
42 val scrollBackAmount: Float,
43 /** True when extra remeasure is required. */
44 val remeasureNeeded: Boolean,
45 /** Scope for animations. */
46 val coroutineScope: CoroutineScope,
47 /** Density of the last measure. */
48 val density: Density,
49 /** Amount of slots we have in each line. */
50 val slotsPerLine: Int,
51 /** Finds items on a line and their measurement constraints. Used for prefetching. */
52 val prefetchInfoRetriever: (line: Int) -> List<Pair<Int, Constraints>>,
53 // properties representing the info needed for LazyListLayoutInfo:
54 /** see [LazyGridLayoutInfo.visibleItemsInfo] */
55 override val visibleItemsInfo: List<LazyGridMeasuredItem>,
56 /** see [LazyGridLayoutInfo.viewportStartOffset] */
57 override val viewportStartOffset: Int,
58 /** see [LazyGridLayoutInfo.viewportEndOffset] */
59 override val viewportEndOffset: Int,
60 /** see [LazyGridLayoutInfo.totalItemsCount] */
61 override val totalItemsCount: Int,
62 /** see [LazyGridLayoutInfo.reverseLayout] */
63 override val reverseLayout: Boolean,
64 /** see [LazyGridLayoutInfo.orientation] */
65 override val orientation: Orientation,
66 /** see [LazyGridLayoutInfo.afterContentPadding] */
67 override val afterContentPadding: Int,
68 /** see [LazyGridLayoutInfo.mainAxisItemSpacing] */
69 override val mainAxisItemSpacing: Int
<lambda>null70 ) : LazyGridLayoutInfo, MeasureResult by measureResult {
71
72 val canScrollBackward
73 get() = (firstVisibleLine?.index ?: 0) != 0 || firstVisibleLineScrollOffset != 0
74
75 override val viewportSize: IntSize
76 get() = IntSize(width, height)
77
78 override val beforeContentPadding: Int
79 get() = -viewportStartOffset
80
81 override val maxSpan: Int
82 get() = slotsPerLine
83
84 /**
85 * Creates a new layout info with applying a scroll [delta] for this layout info. In some cases
86 * we can apply small scroll deltas by just changing the offsets for each [visibleItemsInfo].
87 * But we can only do so if after applying the delta we would not need to compose a new item or
88 * dispose an item which is currently visible. In this case this function will not apply the
89 * [delta] and return null.
90 *
91 * @return new layout info if we can safely apply a passed scroll [delta] to this layout info.
92 * If If new layout info is returned, only the placement phase is needed to apply new offsets.
93 * If null is returned, it means we have to rerun the full measure phase to apply the [delta].
94 */
95 fun copyWithScrollDeltaWithoutRemeasure(
96 delta: Int,
97 updateAnimations: Boolean
98 ): LazyGridMeasureResult? {
99 if (
100 remeasureNeeded ||
101 visibleItemsInfo.isEmpty() ||
102 firstVisibleLine == null ||
103 // applying this delta will change firstVisibleLineScrollOffset
104 (firstVisibleLineScrollOffset - delta) !in
105 0 until firstVisibleLine.mainAxisSizeWithSpacings
106 ) {
107 return null
108 }
109 val first = visibleItemsInfo.first()
110 val last = visibleItemsInfo.last()
111 if (first.nonScrollableItem || last.nonScrollableItem) {
112 // non scrollable items require special handling.
113 return null
114 }
115 val canApply =
116 if (delta < 0) {
117 // scrolling forward
118 val deltaToFirstItemChange =
119 first.offsetOnMainAxis(orientation) + first.mainAxisSizeWithSpacings -
120 viewportStartOffset
121 val deltaToLastItemChange =
122 last.offsetOnMainAxis(orientation) + last.mainAxisSizeWithSpacings -
123 viewportEndOffset
124 minOf(deltaToFirstItemChange, deltaToLastItemChange) > -delta
125 } else {
126 // scrolling backward
127 val deltaToFirstItemChange =
128 viewportStartOffset - first.offsetOnMainAxis(orientation)
129 val deltaToLastItemChange = viewportEndOffset - last.offsetOnMainAxis(orientation)
130 minOf(deltaToFirstItemChange, deltaToLastItemChange) > delta
131 }
132 return if (canApply) {
133 visibleItemsInfo.fastForEach { it.applyScrollDelta(delta, updateAnimations) }
134 LazyGridMeasureResult(
135 firstVisibleLine = firstVisibleLine,
136 firstVisibleLineScrollOffset = firstVisibleLineScrollOffset - delta,
137 canScrollForward =
138 canScrollForward ||
139 delta > 0, // we scrolled backward, so now we can scroll forward
140 consumedScroll = delta.toFloat(),
141 scrollBackAmount = scrollBackAmount,
142 measureResult = measureResult,
143 remeasureNeeded = remeasureNeeded,
144 coroutineScope = coroutineScope,
145 density = density,
146 slotsPerLine = slotsPerLine,
147 prefetchInfoRetriever = prefetchInfoRetriever,
148 visibleItemsInfo = visibleItemsInfo,
149 viewportStartOffset = viewportStartOffset,
150 viewportEndOffset = viewportEndOffset,
151 totalItemsCount = totalItemsCount,
152 reverseLayout = reverseLayout,
153 orientation = orientation,
154 afterContentPadding = afterContentPadding,
155 mainAxisItemSpacing = mainAxisItemSpacing
156 )
157 } else {
158 null
159 }
160 }
161 }
162