Merge pull request #1063 from android/ben/scrollbars_perf

Move Scrollbar state reads out of composition
pull/1066/head
Ben Trengrove 2 years ago committed by GitHub
commit eb54ec71e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -16,10 +16,10 @@
package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar
import android.annotation.SuppressLint
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.Orientation.Horizontal import androidx.compose.foundation.gestures.Orientation.Horizontal
import androidx.compose.foundation.gestures.Orientation.Vertical import androidx.compose.foundation.gestures.Orientation.Vertical
@ -38,12 +38,22 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorProducer
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.invalidateDraw
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Active import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Active
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Dormant import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Dormant
@ -130,12 +140,7 @@ private fun ScrollableState.DraggableScrollbarThumb(
Horizontal -> height(12.dp).fillMaxWidth() Horizontal -> height(12.dp).fillMaxWidth()
} }
} }
.background( .scrollThumb(this, interactionSource),
color = scrollbarThumbColor(
interactionSource = interactionSource,
),
shape = RoundedCornerShape(16.dp),
),
) )
} }
@ -155,31 +160,72 @@ private fun ScrollableState.DecorativeScrollbarThumb(
Horizontal -> height(2.dp).fillMaxWidth() Horizontal -> height(2.dp).fillMaxWidth()
} }
} }
.background( .scrollThumb(this, interactionSource),
color = scrollbarThumbColor(
interactionSource = interactionSource,
),
shape = RoundedCornerShape(16.dp),
),
) )
} }
// TODO: This lint is removed in 1.6 as the recommendation has changed
// remove when project is upgraded
@SuppressLint("ComposableModifierFactory")
@Composable
private fun Modifier.scrollThumb(
scrollableState: ScrollableState,
interactionSource: InteractionSource,
): Modifier {
val colorState = scrollbarThumbColor(scrollableState, interactionSource)
return this then ScrollThumbElement { colorState.value }
}
private data class ScrollThumbElement(val colorProducer: ColorProducer) :
ModifierNodeElement<ScrollThumbNode>() {
override fun create(): ScrollThumbNode = ScrollThumbNode(colorProducer)
override fun update(node: ScrollThumbNode) {
node.colorProducer = colorProducer
node.invalidateDraw()
}
}
private class ScrollThumbNode(var colorProducer: ColorProducer) : DrawModifierNode, Modifier.Node() {
private val shape = RoundedCornerShape(16.dp)
// naive cache outline calculation if size is the same
private var lastSize: Size? = null
private var lastLayoutDirection: LayoutDirection? = null
private var lastOutline: Outline? = null
override fun ContentDrawScope.draw() {
val color = colorProducer()
val outline =
if (size == lastSize && layoutDirection == lastLayoutDirection) {
lastOutline!!
} else {
shape.createOutline(size, layoutDirection, this)
}
if (color != Color.Unspecified) drawOutline(outline, color = color)
lastOutline = outline
lastSize = size
lastLayoutDirection = layoutDirection
}
}
/** /**
* The color of the scrollbar thumb as a function of its interaction state. * The color of the scrollbar thumb as a function of its interaction state.
* @param interactionSource source of interactions in the scrolling container * @param interactionSource source of interactions in the scrolling container
*/ */
@Composable @Composable
private fun ScrollableState.scrollbarThumbColor( private fun scrollbarThumbColor(
scrollableState: ScrollableState,
interactionSource: InteractionSource, interactionSource: InteractionSource,
): Color { ): State<Color> {
var state by remember { mutableStateOf(Dormant) } var state by remember { mutableStateOf(Dormant) }
val pressed by interactionSource.collectIsPressedAsState() val pressed by interactionSource.collectIsPressedAsState()
val hovered by interactionSource.collectIsHoveredAsState() val hovered by interactionSource.collectIsHoveredAsState()
val dragged by interactionSource.collectIsDraggedAsState() val dragged by interactionSource.collectIsDraggedAsState()
val active = (canScrollForward || canScrollForward) && val active = (scrollableState.canScrollForward || scrollableState.canScrollForward) &&
(pressed || hovered || dragged || isScrollInProgress) (pressed || hovered || dragged || scrollableState.isScrollInProgress)
val color by animateColorAsState( val color = animateColorAsState(
targetValue = when (state) { targetValue = when (state) {
Active -> MaterialTheme.colorScheme.onSurface.copy(0.5f) Active -> MaterialTheme.colorScheme.onSurface.copy(0.5f)
Inactive -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f) Inactive -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f)

@ -16,8 +16,9 @@
package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.Orientation.Horizontal
import androidx.compose.foundation.gestures.Orientation.Vertical
import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.gestures.detectVerticalDragGestures
@ -28,31 +29,28 @@ import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.util.packFloats import androidx.compose.ui.util.packFloats
import androidx.compose.ui.util.unpackFloat1 import androidx.compose.ui.util.unpackFloat1
import androidx.compose.ui.util.unpackFloat2 import androidx.compose.ui.util.unpackFloat2
@ -61,6 +59,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt
/** /**
* The delay between scrolls when a user long presses on the scrollbar track to initiate a scroll * The delay between scrolls when a user long presses on the scrollbar track to initiate a scroll
@ -74,21 +73,53 @@ private const val SCROLLBAR_PRESS_DELAY_MS = 10L
*/ */
private const val SCROLLBAR_PRESS_DELTA_PCT = 0.02f private const val SCROLLBAR_PRESS_DELTA_PCT = 0.02f
class ScrollbarState {
private var packedValue by mutableLongStateOf(0L)
internal fun onScroll(stateValue: ScrollbarStateValue) {
packedValue = stateValue.packedValue
}
/**
* Returns the thumb size of the scrollbar as a percentage of the total track size
*/
val thumbSizePercent
get() = unpackFloat1(packedValue)
/**
* Returns the distance the thumb has traveled as a percentage of total track size
*/
val thumbMovedPercent
get() = unpackFloat2(packedValue)
}
/**
* Returns the size of the scrollbar track in pixels
*/
private val ScrollbarTrack.size
get() = unpackFloat2(packedValue) - unpackFloat1(packedValue)
/**
* Returns the position of the scrollbar thumb on the track as a percentage
*/
private fun ScrollbarTrack.thumbPosition(
dimension: Float,
): Float = max(
a = min(
a = dimension / size,
b = 1f,
),
b = 0f,
)
/** /**
* Class definition for the core properties of a scroll bar * Class definition for the core properties of a scroll bar
*/ */
@Immutable @Immutable
@JvmInline @JvmInline
value class ScrollbarState internal constructor( value class ScrollbarStateValue internal constructor(
internal val packedValue: Long, internal val packedValue: Long,
) {
companion object {
val FULL = ScrollbarState(
thumbSizePercent = 1f,
thumbMovedPercent = 0f,
) )
}
}
/** /**
* Class definition for the core properties of a scroll bar track * Class definition for the core properties of a scroll bar track
@ -105,54 +136,23 @@ private value class ScrollbarTrack(
} }
/** /**
* Creates a [ScrollbarState] with the listed properties * Creates a [ScrollbarStateValue] with the listed properties
* @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size. * @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size.
* Refers to either the thumb width (for horizontal scrollbars) * Refers to either the thumb width (for horizontal scrollbars)
* or height (for vertical scrollbars). * or height (for vertical scrollbars).
* @param thumbMovedPercent the distance the thumb has traveled as a percentage of total * @param thumbMovedPercent the distance the thumb has traveled as a percentage of total
* track size. * track size.
*/ */
fun ScrollbarState( fun scrollbarStateValue(
thumbSizePercent: Float, thumbSizePercent: Float,
thumbMovedPercent: Float, thumbMovedPercent: Float,
) = ScrollbarState( ) = ScrollbarStateValue(
packFloats( packFloats(
val1 = thumbSizePercent, val1 = thumbSizePercent,
val2 = thumbMovedPercent, val2 = thumbMovedPercent,
), ),
) )
/**
* Returns the thumb size of the scrollbar as a percentage of the total track size
*/
val ScrollbarState.thumbSizePercent
get() = unpackFloat1(packedValue)
/**
* Returns the distance the thumb has traveled as a percentage of total track size
*/
val ScrollbarState.thumbMovedPercent
get() = unpackFloat2(packedValue)
/**
* Returns the size of the scrollbar track in pixels
*/
private val ScrollbarTrack.size
get() = unpackFloat2(packedValue) - unpackFloat1(packedValue)
/**
* Returns the position of the scrollbar thumb on the track as a percentage
*/
private fun ScrollbarTrack.thumbPosition(
dimension: Float,
): Float = max(
a = min(
a = dimension / size,
b = 1f,
),
b = 0f,
)
/** /**
* Returns the value of [offset] along the axis specified by [this] * Returns the value of [offset] along the axis specified by [this]
*/ */
@ -197,8 +197,6 @@ fun Scrollbar(
thumb: @Composable () -> Unit, thumb: @Composable () -> Unit,
onThumbMoved: ((Float) -> Unit)? = null, onThumbMoved: ((Float) -> Unit)? = null,
) { ) {
val localDensity = LocalDensity.current
// Using Offset.Unspecified and Float.NaN instead of null // Using Offset.Unspecified and Float.NaN instead of null
// to prevent unnecessary boxing of primitives // to prevent unnecessary boxing of primitives
var pressedOffset by remember { mutableStateOf(Offset.Unspecified) } var pressedOffset by remember { mutableStateOf(Offset.Unspecified) }
@ -210,23 +208,6 @@ fun Scrollbar(
var track by remember { mutableStateOf(ScrollbarTrack(packedValue = 0)) } var track by remember { mutableStateOf(ScrollbarTrack(packedValue = 0)) }
val thumbTravelPercent = when {
interactionThumbTravelPercent.isNaN() -> state.thumbMovedPercent
else -> interactionThumbTravelPercent
}
val thumbSizePx = max(
a = state.thumbSizePercent * track.size,
b = with(localDensity) { minThumbSize.toPx() },
)
val thumbSizeDp by animateDpAsState(
targetValue = with(localDensity) { thumbSizePx.toDp() },
label = "scrollbar thumb size",
)
val thumbMovedPx = min(
a = track.size * thumbTravelPercent,
b = track.size - thumbSizePx,
)
// scrollbar track container // scrollbar track container
Box( Box(
modifier = modifier modifier = modifier
@ -320,50 +301,73 @@ fun Scrollbar(
} }
}, },
) { ) {
val scrollbarThumbMovedDp = max(
a = with(localDensity) { thumbMovedPx.toDp() },
b = 0.dp,
)
// scrollbar thumb container // scrollbar thumb container
Box( Layout(content = { thumb() }) { measurables, constraints ->
modifier = Modifier val measurable = measurables.first()
.align(Alignment.TopStart)
.run { val thumbSizePx = max(
when (orientation) { a = state.thumbSizePercent * track.size,
Orientation.Horizontal -> width(thumbSizeDp) b = minThumbSize.toPx(),
Orientation.Vertical -> height(thumbSizeDp) )
val thumbTravelPercent = when {
interactionThumbTravelPercent.isNaN() -> state.thumbMovedPercent
else -> interactionThumbTravelPercent
} }
val thumbMovedPx = min(
a = track.size * thumbTravelPercent,
b = track.size - thumbSizePx,
)
val scrollbarThumbMovedPx = max(
a = thumbMovedPx.roundToInt(),
b = 0,
)
val y = when (orientation) {
Horizontal -> 0
Vertical -> scrollbarThumbMovedPx
} }
.offset( val x = when (orientation) {
y = when (orientation) { Horizontal -> scrollbarThumbMovedPx
Orientation.Horizontal -> 0.dp Vertical -> 0
Orientation.Vertical -> scrollbarThumbMovedDp }
},
x = when (orientation) { val updatedConstraints = when (orientation) {
Orientation.Horizontal -> scrollbarThumbMovedDp Horizontal -> {
Orientation.Vertical -> 0.dp constraints.copy(
}, minWidth = thumbSizePx.roundToInt(),
), maxWidth = thumbSizePx.roundToInt(),
) { )
thumb() }
Vertical -> {
constraints.copy(
minHeight = thumbSizePx.roundToInt(),
maxHeight = thumbSizePx.roundToInt(),
)
} }
} }
if (onThumbMoved == null) return val placeable = measurable.measure(updatedConstraints)
layout(placeable.width, placeable.height) {
placeable.place(x, y)
}
}
}
// State that will be read inside the effects that follow if (onThumbMoved == null) return
// but will not cause re-triggering of them
val updatedState by rememberUpdatedState(state)
// Process presses // Process presses
LaunchedEffect(pressedOffset) { LaunchedEffect(Unit) {
snapshotFlow { pressedOffset }.collect { pressedOffset ->
// Press ended, reset interactionThumbTravelPercent // Press ended, reset interactionThumbTravelPercent
if (pressedOffset == Offset.Unspecified) { if (pressedOffset == Offset.Unspecified) {
interactionThumbTravelPercent = Float.NaN interactionThumbTravelPercent = Float.NaN
return@LaunchedEffect return@collect
} }
var currentThumbMovedPercent = updatedState.thumbMovedPercent var currentThumbMovedPercent = state.thumbMovedPercent
val destinationThumbMovedPercent = track.thumbPosition( val destinationThumbMovedPercent = track.thumbPosition(
dimension = orientation.valueOf(pressedOffset), dimension = orientation.valueOf(pressedOffset),
) )
@ -387,12 +391,14 @@ fun Scrollbar(
delay(SCROLLBAR_PRESS_DELAY_MS) delay(SCROLLBAR_PRESS_DELAY_MS)
} }
} }
}
// Process drags // Process drags
LaunchedEffect(draggedOffset) { LaunchedEffect(Unit) {
snapshotFlow { draggedOffset }.collect { draggedOffset ->
if (draggedOffset == Offset.Unspecified) { if (draggedOffset == Offset.Unspecified) {
interactionThumbTravelPercent = Float.NaN interactionThumbTravelPercent = Float.NaN
return@LaunchedEffect return@collect
} }
val currentTravel = track.thumbPosition( val currentTravel = track.thumbPosition(
dimension = orientation.valueOf(draggedOffset), dimension = orientation.valueOf(draggedOffset),
@ -401,3 +407,4 @@ fun Scrollbar(
interactionThumbTravelPercent = currentTravel interactionThumbTravelPercent = currentTravel
} }
} }
}

@ -24,7 +24,8 @@ import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.produceState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
@ -40,11 +41,9 @@ import kotlin.math.min
fun LazyListState.scrollbarState( fun LazyListState.scrollbarState(
itemsAvailable: Int, itemsAvailable: Int,
itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index, itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index,
): ScrollbarState = produceState( ): ScrollbarState {
initialValue = ScrollbarState.FULL, val state = remember { ScrollbarState() }
key1 = this, LaunchedEffect(this, itemsAvailable) {
key2 = itemsAvailable,
) {
snapshotFlow { snapshotFlow {
if (itemsAvailable == 0) return@snapshotFlow null if (itemsAvailable == 0) return@snapshotFlow null
@ -80,7 +79,7 @@ fun LazyListState.scrollbarState(
a = itemsVisible / itemsAvailable, a = itemsVisible / itemsAvailable,
b = 1f, b = 1f,
) )
ScrollbarState( scrollbarStateValue(
thumbSizePercent = thumbSizePercent, thumbSizePercent = thumbSizePercent,
thumbMovedPercent = when { thumbMovedPercent = when {
layoutInfo.reverseLayout -> 1f - thumbTravelPercent layoutInfo.reverseLayout -> 1f - thumbTravelPercent
@ -90,8 +89,10 @@ fun LazyListState.scrollbarState(
} }
.filterNotNull() .filterNotNull()
.distinctUntilChanged() .distinctUntilChanged()
.collect { value = it } .collect { state.onScroll(it) }
}.value }
return state
}
/** /**
* Calculates a [ScrollbarState] driven by the changes in a [LazyGridState] * Calculates a [ScrollbarState] driven by the changes in a [LazyGridState]
@ -103,11 +104,9 @@ fun LazyListState.scrollbarState(
fun LazyGridState.scrollbarState( fun LazyGridState.scrollbarState(
itemsAvailable: Int, itemsAvailable: Int,
itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index, itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index,
): ScrollbarState = produceState( ): ScrollbarState {
initialValue = ScrollbarState.FULL, val state = remember { ScrollbarState() }
key1 = this, LaunchedEffect(this, itemsAvailable) {
key2 = itemsAvailable,
) {
snapshotFlow { snapshotFlow {
if (itemsAvailable == 0) return@snapshotFlow null if (itemsAvailable == 0) return@snapshotFlow null
@ -153,7 +152,7 @@ fun LazyGridState.scrollbarState(
a = itemsVisible / itemsAvailable, a = itemsVisible / itemsAvailable,
b = 1f, b = 1f,
) )
ScrollbarState( scrollbarStateValue(
thumbSizePercent = thumbSizePercent, thumbSizePercent = thumbSizePercent,
thumbMovedPercent = when { thumbMovedPercent = when {
layoutInfo.reverseLayout -> 1f - thumbTravelPercent layoutInfo.reverseLayout -> 1f - thumbTravelPercent
@ -163,8 +162,10 @@ fun LazyGridState.scrollbarState(
} }
.filterNotNull() .filterNotNull()
.distinctUntilChanged() .distinctUntilChanged()
.collect { value = it } .collect { state.onScroll(it) }
}.value }
return state
}
/** /**
* Remembers a [ScrollbarState] driven by the changes in a [LazyStaggeredGridState] * Remembers a [ScrollbarState] driven by the changes in a [LazyStaggeredGridState]
@ -177,11 +178,9 @@ fun LazyGridState.scrollbarState(
fun LazyStaggeredGridState.scrollbarState( fun LazyStaggeredGridState.scrollbarState(
itemsAvailable: Int, itemsAvailable: Int,
itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index, itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index,
): ScrollbarState = produceState( ): ScrollbarState {
initialValue = ScrollbarState.FULL, val state = remember { ScrollbarState() }
key1 = this, LaunchedEffect(this, itemsAvailable) {
key2 = itemsAvailable,
) {
snapshotFlow { snapshotFlow {
if (itemsAvailable == 0) return@snapshotFlow null if (itemsAvailable == 0) return@snapshotFlow null
@ -219,15 +218,17 @@ fun LazyStaggeredGridState.scrollbarState(
a = itemsVisible / itemsAvailable, a = itemsVisible / itemsAvailable,
b = 1f, b = 1f,
) )
ScrollbarState( scrollbarStateValue(
thumbSizePercent = thumbSizePercent, thumbSizePercent = thumbSizePercent,
thumbMovedPercent = thumbTravelPercent, thumbMovedPercent = thumbTravelPercent,
) )
} }
.filterNotNull() .filterNotNull()
.distinctUntilChanged() .distinctUntilChanged()
.collect { value = it } .collect { state.onScroll(it) }
}.value }
return state
}
private inline fun <T> List<T>.floatSumOf(selector: (T) -> Float): Float { private inline fun <T> List<T>.floatSumOf(selector: (T) -> Float): Float {
var sum = 0f var sum = 0f

Loading…
Cancel
Save