[go: nahoru, domu]

blob: 13569c4c4a0831955cd04c7cfa367bf3c1c0e8b5 [file] [log] [blame]
/*
* Copyright 2021 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.tv.foundation.lazy.grid
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastSumBy
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sign
/**
* Measures and calculates the positions for the currently visible items. The result is produced
* as a [TvLazyGridMeasureResult] which contains all the calculations.
*/
internal fun measureLazyGrid(
itemsCount: Int,
measuredLineProvider: LazyMeasuredLineProvider,
measuredItemProvider: LazyMeasuredItemProvider,
mainAxisAvailableSize: Int,
slotsPerLine: Int,
beforeContentPadding: Int,
afterContentPadding: Int,
firstVisibleLineIndex: LineIndex,
firstVisibleLineScrollOffset: Int,
scrollToBeConsumed: Float,
constraints: Constraints,
isVertical: Boolean,
verticalArrangement: Arrangement.Vertical?,
horizontalArrangement: Arrangement.Horizontal?,
reverseLayout: Boolean,
density: Density,
placementAnimator: LazyGridItemPlacementAnimator,
layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
): TvLazyGridMeasureResult {
require(beforeContentPadding >= 0)
require(afterContentPadding >= 0)
if (itemsCount <= 0) {
// empty data set. reset the current scroll and report zero size
return TvLazyGridMeasureResult(
firstVisibleLine = null,
firstVisibleLineScrollOffset = 0,
canScrollForward = false,
consumedScroll = 0f,
measureResult = layout(constraints.minWidth, constraints.minHeight) {},
visibleItemsInfo = emptyList(),
viewportStartOffset = -beforeContentPadding,
viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
totalItemsCount = 0,
reverseLayout = reverseLayout,
orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
afterContentPadding = afterContentPadding
)
} else {
var currentFirstLineIndex = firstVisibleLineIndex
var currentFirstLineScrollOffset = firstVisibleLineScrollOffset
// represents the real amount of scroll we applied as a result of this measure pass.
var scrollDelta = scrollToBeConsumed.roundToInt()
// applying the whole requested scroll offset. we will figure out if we can't consume
// all of it later
currentFirstLineScrollOffset -= scrollDelta
// if the current scroll offset is less than minimally possible
if (currentFirstLineIndex == LineIndex(0) && currentFirstLineScrollOffset < 0) {
scrollDelta += currentFirstLineScrollOffset
currentFirstLineScrollOffset = 0
}
// this will contain all the MeasuredItems representing the visible lines
val visibleLines = mutableListOf<LazyMeasuredLine>()
// include the start padding so we compose items in the padding area. before starting
// scrolling forward we would remove it back
currentFirstLineScrollOffset -= beforeContentPadding
// define min and max offsets (min offset currently includes beforeContentPadding)
val minOffset = -beforeContentPadding
val maxOffset = mainAxisAvailableSize
// we had scrolled backward or we compose items in the start padding area, which means
// items before current firstLineScrollOffset should be visible. compose them and update
// firstLineScrollOffset
while (currentFirstLineScrollOffset < 0 && currentFirstLineIndex > LineIndex(0)) {
val previous = LineIndex(currentFirstLineIndex.value - 1)
val measuredLine = measuredLineProvider.getAndMeasure(previous)
visibleLines.add(0, measuredLine)
currentFirstLineScrollOffset += measuredLine.mainAxisSizeWithSpacings
currentFirstLineIndex = previous
}
// if we were scrolled backward, but there were not enough lines before. this means
// not the whole scroll was consumed
if (currentFirstLineScrollOffset < minOffset) {
scrollDelta += currentFirstLineScrollOffset
currentFirstLineScrollOffset = minOffset
}
// neutralize previously added start padding as we stopped filling the before content padding
currentFirstLineScrollOffset += beforeContentPadding
var index = currentFirstLineIndex
val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
var currentMainAxisOffset = -currentFirstLineScrollOffset
// first we need to skip lines we already composed while composing backward
visibleLines.fastForEach {
index++
currentMainAxisOffset += it.mainAxisSizeWithSpacings
}
// then composing visible lines forward until we fill the whole viewport.
// we want to have at least one line in visibleItems even if in fact all the items are
// offscreen, this can happen if the content padding is larger than the available size.
while (currentMainAxisOffset <= maxMainAxis || visibleLines.isEmpty()) {
val measuredLine = measuredLineProvider.getAndMeasure(index)
if (measuredLine.isEmpty()) {
--index
break
}
currentMainAxisOffset += measuredLine.mainAxisSizeWithSpacings
if (currentMainAxisOffset <= minOffset &&
measuredLine.items.last().index.value != itemsCount - 1) {
// this line is offscreen and will not be placed. advance firstVisibleLineIndex
currentFirstLineIndex = index + 1
currentFirstLineScrollOffset -= measuredLine.mainAxisSizeWithSpacings
} else {
visibleLines.add(measuredLine)
}
index++
}
// we didn't fill the whole viewport with lines starting from firstVisibleLineIndex.
// lets try to scroll back if we have enough lines before firstVisibleLineIndex.
if (currentMainAxisOffset < maxOffset) {
val toScrollBack = maxOffset - currentMainAxisOffset
currentFirstLineScrollOffset -= toScrollBack
currentMainAxisOffset += toScrollBack
while (currentFirstLineScrollOffset < beforeContentPadding &&
currentFirstLineIndex > LineIndex(0)
) {
val previousIndex = LineIndex(currentFirstLineIndex.value - 1)
val measuredLine = measuredLineProvider.getAndMeasure(previousIndex)
visibleLines.add(0, measuredLine)
currentFirstLineScrollOffset += measuredLine.mainAxisSizeWithSpacings
currentFirstLineIndex = previousIndex
}
scrollDelta += toScrollBack
if (currentFirstLineScrollOffset < 0) {
scrollDelta += currentFirstLineScrollOffset
currentMainAxisOffset += currentFirstLineScrollOffset
currentFirstLineScrollOffset = 0
}
}
// report the amount of pixels we consumed. scrollDelta can be smaller than
// scrollToBeConsumed if there were not enough lines to fill the offered space or it
// can be larger if lines were resized, or if, for example, we were previously
// displaying the line 15, but now we have only 10 lines in total in the data set.
val consumedScroll = if (scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
abs(scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
) {
scrollDelta.toFloat()
} else {
scrollToBeConsumed
}
// the initial offset for lines from visibleLines list
val visibleLinesScrollOffset = -currentFirstLineScrollOffset
var firstLine = visibleLines.first()
// even if we compose lines to fill before content padding we should ignore lines fully
// located there for the state's scroll position calculation (first line + first offset)
if (beforeContentPadding > 0) {
for (i in visibleLines.indices) {
val size = visibleLines[i].mainAxisSizeWithSpacings
if (currentFirstLineScrollOffset != 0 && size <= currentFirstLineScrollOffset &&
i != visibleLines.lastIndex) {
currentFirstLineScrollOffset -= size
firstLine = visibleLines[i + 1]
} else {
break
}
}
}
val layoutWidth = if (isVertical) {
constraints.maxWidth
} else {
constraints.constrainWidth(currentMainAxisOffset)
}
val layoutHeight = if (isVertical) {
constraints.constrainHeight(currentMainAxisOffset)
} else {
constraints.maxHeight
}
val positionedItems = calculateItemsOffsets(
lines = visibleLines,
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
finalMainAxisOffset = currentMainAxisOffset,
maxOffset = maxOffset,
firstLineScrollOffset = visibleLinesScrollOffset,
isVertical = isVertical,
verticalArrangement = verticalArrangement,
horizontalArrangement = horizontalArrangement,
reverseLayout = reverseLayout,
density = density
)
placementAnimator.onMeasured(
consumedScroll = consumedScroll.toInt(),
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
slotsPerLine = slotsPerLine,
reverseLayout = reverseLayout,
positionedItems = positionedItems,
measuredItemProvider = measuredItemProvider
)
return TvLazyGridMeasureResult(
firstVisibleLine = firstLine,
firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
canScrollForward = currentMainAxisOffset > maxOffset,
consumedScroll = consumedScroll,
measureResult = layout(layoutWidth, layoutHeight) {
positionedItems.fastForEach { it.place(this) }
},
viewportStartOffset = -beforeContentPadding,
viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
visibleItemsInfo = positionedItems,
totalItemsCount = itemsCount,
reverseLayout = reverseLayout,
orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
afterContentPadding = afterContentPadding
)
}
}
/**
* Calculates [LazyMeasuredLine]s offsets.
*/
private fun calculateItemsOffsets(
lines: List<LazyMeasuredLine>,
layoutWidth: Int,
layoutHeight: Int,
finalMainAxisOffset: Int,
maxOffset: Int,
firstLineScrollOffset: Int,
isVertical: Boolean,
verticalArrangement: Arrangement.Vertical?,
horizontalArrangement: Arrangement.Horizontal?,
reverseLayout: Boolean,
density: Density,
): MutableList<TvLazyGridPositionedItem> {
val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
val hasSpareSpace = finalMainAxisOffset < min(mainAxisLayoutSize, maxOffset)
if (hasSpareSpace) {
check(firstLineScrollOffset == 0)
}
val positionedItems = ArrayList<TvLazyGridPositionedItem>(lines.fastSumBy { it.items.size })
if (hasSpareSpace) {
val linesCount = lines.size
fun Int.reverseAware() =
if (!reverseLayout) this else linesCount - this - 1
val sizes = IntArray(linesCount) { index ->
lines[index.reverseAware()].mainAxisSize
}
val offsets = IntArray(linesCount) { 0 }
if (isVertical) {
with(requireNotNull(verticalArrangement)) {
density.arrange(mainAxisLayoutSize, sizes, offsets)
}
} else {
with(requireNotNull(horizontalArrangement)) {
// Enforces Ltr layout direction as it is mirrored with placeRelative later.
density.arrange(mainAxisLayoutSize, sizes, LayoutDirection.Ltr, offsets)
}
}
val reverseAwareOffsetIndices =
if (reverseLayout) offsets.indices.reversed() else offsets.indices
for (index in reverseAwareOffsetIndices) {
val absoluteOffset = offsets[index]
// when reverseLayout == true, offsets are stored in the reversed order to items
val line = lines[index.reverseAware()]
val relativeOffset = if (reverseLayout) {
// inverse offset to align with scroll direction for positioning
mainAxisLayoutSize - absoluteOffset - line.mainAxisSize
} else {
absoluteOffset
}
positionedItems.addAll(
line.position(relativeOffset, layoutWidth, layoutHeight)
)
}
} else {
var currentMainAxis = firstLineScrollOffset
lines.fastForEach {
positionedItems.addAll(it.position(currentMainAxis, layoutWidth, layoutHeight))
currentMainAxis += it.mainAxisSizeWithSpacings
}
}
return positionedItems
}