blob: 9c1781dbd44ad9a2da0f1b9fee9f28a926f7fcbf [file] [log] [blame]
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.foundation.lazy.staggeredgrid
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
/**
* Information about layout state of individual item in lazy staggered grid.
* @see [LazyStaggeredGridLayoutInfo]
*/
sealed interface LazyStaggeredGridItemInfo {
/**
* Relative offset from the start of the staggered grid.
*/
val offset: IntOffset
/**
* Index of the item.
*/
val index: Int
/**
* Column (for vertical staggered grids) or row (for horizontal staggered grids) that the item
* is in.
*/
val lane: Int
/**
* Key of the item passed in [LazyStaggeredGridScope.items]
*/
val key: Any
/**
* Item size in pixels. If item contains multiple layouts, the size is calculated as a sum of
* their sizes.
*/
val size: IntSize
/**
* The content type of the item which was passed to the item() or items() function.
*/
val contentType: Any?
}
/**
* Information about layout state of lazy staggered grids.
* Can be retrieved from [LazyStaggeredGridState.layoutInfo].
*/
// todo(b/182882362): expose more information about layout state
sealed interface LazyStaggeredGridLayoutInfo {
/**
* Orientation of the staggered grid.
*/
val orientation: Orientation
/**
* The list of [LazyStaggeredGridItemInfo] per each visible item ordered by index.
*/
val visibleItemsInfo: List<LazyStaggeredGridItemInfo>
/**
* The total count of items passed to staggered grid.
*/
val totalItemsCount: Int
/**
* Layout viewport (content + content padding) size in pixels.
*/
val viewportSize: IntSize
/**
* The start offset of the layout's viewport in pixels. You can think of it as a minimum offset
* which would be visible. Can be negative if non-zero [beforeContentPadding]
* was applied as the content displayed in the content padding area is still visible.
*
* You can use it to understand what items from [visibleItemsInfo] are fully visible.
*/
val viewportStartOffset: Int
/**
* The end offset of the layout's viewport in pixels. You can think of it as a maximum offset
* which would be visible. It is the size of the lazy grid layout minus [beforeContentPadding].
*
* You can use it to understand what items from [visibleItemsInfo] are fully visible.
*/
val viewportEndOffset: Int
/**
* Content padding in pixels applied before the items in scroll direction.
*/
val beforeContentPadding: Int
/**
* Content padding in pixels applied after the items in scroll direction.
*/
val afterContentPadding: Int
/**
* The spacing between items in scroll direction.
*/
val mainAxisItemSpacing: Int
}
internal fun LazyStaggeredGridLayoutInfo.findVisibleItem(
itemIndex: Int
): LazyStaggeredGridItemInfo? {
if (visibleItemsInfo.isEmpty()) {
return null
}
if (itemIndex !in visibleItemsInfo.first().index..visibleItemsInfo.last().index) {
return null
}
val index = visibleItemsInfo.binarySearch { it.index - itemIndex }
return visibleItemsInfo.getOrNull(index)
}
internal class LazyStaggeredGridMeasureResult(
val firstVisibleItemIndices: IntArray,
val firstVisibleItemScrollOffsets: IntArray,
var consumedScroll: Float,
val measureResult: MeasureResult,
var canScrollForward: Boolean,
val isVertical: Boolean,
/** True when extra remeasure is required. */
val remeasureNeeded: Boolean,
override val totalItemsCount: Int,
override val visibleItemsInfo: List<LazyStaggeredGridMeasuredItem>,
override val viewportSize: IntSize,
override val viewportStartOffset: Int,
override val viewportEndOffset: Int,
override val beforeContentPadding: Int,
override val afterContentPadding: Int,
override val mainAxisItemSpacing: Int
) : LazyStaggeredGridLayoutInfo, MeasureResult by measureResult {
val canScrollBackward
// only scroll backward if the first item is not on screen or fully visible
get() = !(firstVisibleItemIndices[0] == 0 && firstVisibleItemScrollOffsets[0] <= 0)
override val orientation: Orientation =
if (isVertical) Orientation.Vertical else Orientation.Horizontal
/**
* Tries to apply a scroll [delta] for this layout info. In some cases we can apply small
* scroll deltas by just changing the offsets for each [visibleItemsInfo].
* But we can only do so if after applying the delta we would not need to compose a new item
* or dispose an item which is currently visible. In this case this function will not apply
* the [delta] and return false.
*
* @return true if we can safely apply a passed scroll [delta] to this layout info.
* If true is returned, only the placement phase is needed to apply new offsets.
* If false is returned, it means we have to rerun the full measure phase to apply the [delta].
*/
fun tryToApplyScrollWithoutRemeasure(delta: Int): Boolean {
if (remeasureNeeded || visibleItemsInfo.isEmpty() || firstVisibleItemIndices.isEmpty() ||
firstVisibleItemScrollOffsets.isEmpty()
) {
return false
}
visibleItemsInfo.fastForEach {
// non scrollable items require special handling.
if (it.nonScrollableItem ||
// applying delta will make this item to cross the 0th pixel, this means
// that firstVisibleItemIndices will change. we require a remeasure for it.
it.mainAxisOffset <= 0 != it.mainAxisOffset + delta <= 0
) {
return false
}
if (it.mainAxisOffset <= viewportStartOffset) {
// we compare with viewportStartOffset in order to know when the item will became
// not visible anymore, and with 0 to know when the firstVisibleItemIndices will
// change. when we have a beforeContentPadding those values will not be the same.
val canApply = if (delta < 0) { // scrolling forward
it.mainAxisOffset + it.sizeWithSpacings - viewportStartOffset > -delta
} else { // scrolling backward
viewportStartOffset - it.mainAxisOffset > delta
}
if (!canApply) return false
}
// item is partially visible at the bottom.
if (it.mainAxisOffset + it.sizeWithSpacings >= viewportEndOffset) {
val canApply = if (delta < 0) { // scrolling forward
it.mainAxisOffset + it.sizeWithSpacings - viewportEndOffset > -delta
} else { // scrolling backward
viewportEndOffset - it.mainAxisOffset > delta
}
if (!canApply) return false
}
}
repeat(firstVisibleItemScrollOffsets.size) { index ->
firstVisibleItemScrollOffsets[index] -= delta
}
visibleItemsInfo.fastForEach {
it.applyScrollDelta(delta)
}
consumedScroll = delta.toFloat()
if (!canScrollForward && delta > 0) {
// we scrolled backward, so now we can scroll forward
canScrollForward = true
}
return true
}
}
internal val EmptyLazyStaggeredGridLayoutInfo = LazyStaggeredGridMeasureResult(
firstVisibleItemIndices = IntArray(0),
firstVisibleItemScrollOffsets = IntArray(0),
consumedScroll = 0f,
measureResult = object : MeasureResult {
override val width: Int = 0
override val height: Int = 0
@Suppress("PrimitiveInCollection")
override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
override fun placeChildren() {}
},
canScrollForward = false,
isVertical = false,
visibleItemsInfo = emptyList(),
totalItemsCount = 0,
remeasureNeeded = false,
viewportSize = IntSize.Zero,
viewportStartOffset = 0,
viewportEndOffset = 0,
beforeContentPadding = 0,
afterContentPadding = 0,
mainAxisItemSpacing = 0
)