Skip to content

Commit

Permalink
Correctly support Arrangement.spacedBy in ScrollbarAdapters for lazy …
Browse files Browse the repository at this point in the history
…lists/grids (#380)
  • Loading branch information
m-sasha authored Feb 2, 2023
1 parent 2611702 commit 41c1b8f
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.mainAxisItemSpacing
import androidx.compose.foundation.lazy.mainAxisItemSpacing
import androidx.compose.foundation.text.TextFieldScrollState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
Expand Down Expand Up @@ -147,25 +149,35 @@ internal abstract class LazyLineContentAdapter: ScrollbarAdapter{
*/
protected abstract fun averageVisibleLineSize(): Double

private val averageLineSize by derivedStateOf {
/**
* The spacing between lines.
*/
protected abstract val lineSpacing: Int

private val averageVisibleLineSize by derivedStateOf {
if (totalLineCount() == 0)
0.0
else
averageVisibleLineSize()
}

private val averageVisibleLineSizeWithSpacing get() = averageVisibleLineSize + lineSpacing

override val scrollOffset: Double
get() {
val firstVisibleLine = firstVisibleLine()
return if (firstVisibleLine == null)
0.0
else
firstVisibleLine.index * averageLineSize - firstVisibleLine.offset
firstVisibleLine.index * averageVisibleLineSizeWithSpacing - firstVisibleLine.offset
}

override val contentSize: Double
get() {
return averageLineSize * totalLineCount() + contentPadding()
val totalLineCount = totalLineCount()
return averageVisibleLineSize * totalLineCount +
lineSpacing * (totalLineCount - 1).coerceAtLeast(0) +
contentPadding()
}

override suspend fun scrollTo(scrollOffset: Double) {
Expand All @@ -186,12 +198,12 @@ internal abstract class LazyLineContentAdapter: ScrollbarAdapter{
private suspend fun snapTo(scrollOffset: Double) {
val scrollOffsetCoerced = scrollOffset.coerceIn(0.0, maxScrollOffset)

val index = (scrollOffsetCoerced / averageLineSize)
val index = (scrollOffsetCoerced / averageVisibleLineSizeWithSpacing)
.toInt()
.coerceAtLeast(0)
.coerceAtMost(totalLineCount() - 1)

val offset = (scrollOffsetCoerced - index * averageLineSize)
val offset = (scrollOffsetCoerced - index * averageVisibleLineSizeWithSpacing)
.toInt()
.coerceAtLeast(0)

Expand Down Expand Up @@ -241,11 +253,14 @@ internal class LazyListScrollbarAdapter(

val first = first()
val last = last()
(last.offset + last.size - first.offset).toDouble() / size
(last.offset + last.size - first.offset - (size-1)*lineSpacing).toDouble() / size
}

override val lineSpacing get() = scrollState.layoutInfo.mainAxisItemSpacing

}


internal class LazyGridScrollbarAdapter(
private val scrollState: LazyGridState
): LazyLineContentAdapter() {
Expand Down Expand Up @@ -274,6 +289,10 @@ internal class LazyGridScrollbarAdapter(
if (isVertical) y else x
}

private fun lineOfIndex(index: Int) = index / scrollState.slotsPerLine

private fun indexOfFirstInLine(line: Int) = line * scrollState.slotsPerLine

override fun firstVisibleLine(): VisibleLine? {
return scrollState.layoutInfo.visibleItemsInfo
.firstOrNull { it.line() != unknownLine } // Skip exiting items
Expand All @@ -285,10 +304,6 @@ internal class LazyGridScrollbarAdapter(
}
}

private fun lineOfIndex(index: Int) = index / scrollState.slotsPerLine

private fun indexOfFirstInLine(line: Int) = line * scrollState.slotsPerLine

override fun totalLineCount(): Int{
val itemCount = scrollState.layoutInfo.totalItemsCount
return if (itemCount == 0)
Expand All @@ -312,28 +327,33 @@ internal class LazyGridScrollbarAdapter(
scrollState.scrollBy(value)
}

override fun averageVisibleLineSize(): Double {
override fun averageVisibleLineSize(): Double{
val visibleItemsInfo = scrollState.layoutInfo.visibleItemsInfo
val indexOfFirstKnownLineItem = visibleItemsInfo.indexOfFirst { it.line() != unknownLine }
if (indexOfFirstKnownLineItem == -1)
return 0.0
val reallyVisibleItemsInfo = // Non-exiting visible items
visibleItemsInfo.subList(indexOfFirstKnownLineItem, visibleItemsInfo.size)

val realVisibleItemsInfo = visibleItemsInfo
.subList(indexOfFirstKnownLineItem, visibleItemsInfo.size)
val lastLine = realVisibleItemsInfo.last().line()
val lastLineSize = realVisibleItemsInfo
// Compute the size of the last line
val lastLine = reallyVisibleItemsInfo.last().line()
val lastLineSize = reallyVisibleItemsInfo
.asReversed()
.asSequence()
.takeWhile { it.line() == lastLine }
.maxOf { it.mainAxisSize() }

val first = realVisibleItemsInfo.first()
val last = realVisibleItemsInfo.last()
val first = reallyVisibleItemsInfo.first()
val last = reallyVisibleItemsInfo.last()
val lineCount = last.line() - first.line() + 1
val lineSizeSum = last.mainAxisOffset() + lastLineSize - first.mainAxisOffset()
return lineSizeSum.toDouble() / lineCount
val lineSpacingSum = (lineCount - 1) * lineSpacing
return (
last.mainAxisOffset() + lastLineSize - first.mainAxisOffset() - lineSpacingSum
).toDouble() / lineCount
}

override val lineSpacing get() = scrollState.layoutInfo.mainAxisItemSpacing

}

@OptIn(ExperimentalFoundationApi::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollConfig
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
Expand Down Expand Up @@ -1037,6 +1038,67 @@ class ScrollbarTest {
}
}

private suspend fun testLazyContentWithLineSpacing(firstBoxTag: String, lastBoxTag: String){
// Test the size of the scrollbar thumb by trying to drag by one pixel below where it
// should end
rule.onNodeWithTag("scrollbar").performMouseInput {
instantDrag(start = Offset(0f, 50f), end = Offset(0f, 200f))
}
rule.awaitIdle()
rule.onNodeWithTag(firstBoxTag).assertTopPositionInRootIsEqualTo(0.dp)

// Test the size of the scrollbar thumb by trying to drag by its bottommost pixel
// This also tests the proportionality of the scrolling
rule.onNodeWithTag("scrollbar").performMouseInput {
instantDrag(start = Offset(0f, 49f), end = Offset(0f, 54f))
}
rule.awaitIdle()
rule.onNodeWithTag(firstBoxTag).assertTopPositionInRootIsEqualTo((-10).dp)

// Scroll to the bottom and check the last item position
rule.onNodeWithTag("scrollbar").performMouseInput {
instantDrag(start = Offset(0f, 54f), end = Offset(0f, 99f))
}
rule.onNodeWithTag(lastBoxTag).assertTopPositionInRootIsEqualTo(80.dp)
}

@Test
fun `lazy list with line spacing`(){
runBlocking(Dispatchers.Main) {
rule.setContent {
LazyListTestBox(
size = 100.dp,
childSize = 20.dp,
childCount = 5,
scrollbarWidth = 10.dp,
verticalArrangement = Arrangement.spacedBy(25.dp)
)
}
rule.awaitIdle()

testLazyContentWithLineSpacing("box0", "box4")
}
}

@Test
fun `lazy grid with line spacing`(){
runBlocking(Dispatchers.Main) {
rule.setContent {
LazyGridTestBox(
columns = GridCells.Fixed(4),
size = DpSize(100.dp, 100.dp),
childSize = DpSize(20.dp, 20.dp),
childCount = 18,
scrollbarWidth = 10.dp,
verticalArrangement = Arrangement.spacedBy(25.dp),
)
}
rule.awaitIdle()

testLazyContentWithLineSpacing("box0", "box17")
}
}

private suspend fun tryUntilSucceeded(block: suspend () -> Unit) {
while (true) {
try {
Expand Down Expand Up @@ -1125,6 +1187,7 @@ class ScrollbarTest {
scrollbarWidth: Dp,
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
content: LazyListScope.() -> Unit
) = withTestEnvironment {
Box(Modifier.size(size)) {
Expand All @@ -1133,6 +1196,7 @@ class ScrollbarTest {
state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
content = content
)

Expand All @@ -1156,12 +1220,14 @@ class ScrollbarTest {
scrollbarWidth: Dp,
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
) = LazyListTestBox(
state = state,
size = size,
scrollbarWidth = scrollbarWidth,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement
) {
items(childCount) {
Box(Modifier.size(childSize).testTag("box$it"))
Expand All @@ -1176,6 +1242,7 @@ class ScrollbarTest {
scrollbarWidth: Dp,
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
content: LazyGridScope.() -> Unit
) = withTestEnvironment {
Box(Modifier.size(size)) {
Expand All @@ -1185,6 +1252,7 @@ class ScrollbarTest {
columns = columns,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
content = content
)

Expand All @@ -1209,13 +1277,15 @@ class ScrollbarTest {
scrollbarWidth: Dp,
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
) = LazyGridTestBox(
state = state,
columns = columns,
size = size,
scrollbarWidth = scrollbarWidth,
contentPadding = contentPadding,
reverseLayout = reverseLayout
reverseLayout = reverseLayout,
verticalArrangement
){
items(childCount) {
Box(Modifier.size(childSize).testTag("box$it"))
Expand Down

0 comments on commit 41c1b8f

Please sign in to comment.