diff --git a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt index 3fcc8f2c0..1f37dd4c1 100644 --- a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt +++ b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt @@ -17,6 +17,7 @@ package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.grid.LazyGridItemInfo @@ -32,31 +33,46 @@ import kotlinx.coroutines.flow.filterNotNull import kotlin.math.min /** - * Calculates a [ScrollbarState] driven by the changes in a [LazyListState]. + * Calculates a [ScrollbarState] driven by the changes in a [LazyState]. * - * @param itemsAvailable the total amount of items available to scroll in the lazy list. - * @param itemIndex a lookup function for index of an item in the list relative to [itemsAvailable]. + * @param itemsAvailable the total amount of items available to scroll in the [LazyState]. + * @param itemSize a lookup function for the size of an item in the layout. + * @param offset a lookup function for the offset of an item relative to the start of the view port. + * @param nextItemOnMainAxis a lookup function for the next item on the main axis in the direction + * of the scroll. + * @param itemSize next [LazyStateItem] on the main axis of [LazyState], null if none. + * @param index a lookup function for index of an item in the layout relative to + * @param viewportStartOffset a lookup function for the start offset of the view port + * @param viewportEndOffset a lookup function for the end offset of the view port + * @param reverseLayout a lookup function for the reverseLayout of [LazyState]. */ @Composable -fun LazyListState.scrollbarState( +internal inline fun LazyState.genericScrollbarState( itemsAvailable: Int, - itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index, + crossinline visibleItems: () -> List, + crossinline itemSize: LazyState.(LazyStateItem) -> Int, + crossinline offset: LazyState.(LazyStateItem) -> Int, + crossinline nextItemOnMainAxis: LazyState.(LazyStateItem) -> LazyStateItem?, + crossinline index: (LazyStateItem) -> Int, + crossinline viewportStartOffset: () -> Int, + crossinline viewportEndOffset: () -> Int, + crossinline reverseLayout: () -> Boolean = { false }, ): ScrollbarState { val state = remember { ScrollbarState() } LaunchedEffect(this, itemsAvailable) { snapshotFlow { if (itemsAvailable == 0) return@snapshotFlow null - val visibleItemsInfo = layoutInfo.visibleItemsInfo + val visibleItemsInfo = visibleItems() if (visibleItemsInfo.isEmpty()) return@snapshotFlow null val firstIndex = min( a = interpolateFirstItemIndex( visibleItems = visibleItemsInfo, - itemSize = { it.size }, - offset = { it.offset }, - nextItemOnMainAxis = { first -> visibleItemsInfo.find { it != first } }, - itemIndex = itemIndex, + itemSize = itemSize, + offset = offset, + nextItemOnMainAxis = nextItemOnMainAxis, + itemIndex = index, ), b = itemsAvailable.toFloat(), ) @@ -64,10 +80,10 @@ fun LazyListState.scrollbarState( val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo -> itemVisibilityPercentage( - itemSize = itemInfo.size, - itemStartOffset = itemInfo.offset, - viewportStartOffset = layoutInfo.viewportStartOffset, - viewportEndOffset = layoutInfo.viewportEndOffset, + itemSize = itemSize(itemInfo), + itemStartOffset = offset(itemInfo), + viewportStartOffset = viewportStartOffset(), + viewportEndOffset = viewportEndOffset(), ) } @@ -82,7 +98,7 @@ fun LazyListState.scrollbarState( scrollbarStateValue( thumbSizePercent = thumbSizePercent, thumbMovedPercent = when { - layoutInfo.reverseLayout -> 1f - thumbTravelPercent + reverseLayout() -> 1f - thumbTravelPercent else -> thumbTravelPercent }, ) @@ -94,6 +110,28 @@ fun LazyListState.scrollbarState( return state } +/** + * Calculates a [ScrollbarState] driven by the changes in a [LazyListState]. + * + * @param itemsAvailable the total amount of items available to scroll in the lazy list. + * @param itemIndex a lookup function for index of an item in the list relative to [itemsAvailable]. + */ +@Composable +fun LazyListState.scrollbarState( + itemsAvailable: Int, + itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index, +) = genericScrollbarState( + itemsAvailable = itemsAvailable, + visibleItems = { layoutInfo.visibleItemsInfo }, + itemSize = { it.size }, + offset = { it.offset }, + nextItemOnMainAxis = { first -> layoutInfo.visibleItemsInfo.find { it != first } }, + index = itemIndex, + viewportStartOffset = { layoutInfo.viewportStartOffset }, + viewportEndOffset = { layoutInfo.viewportEndOffset }, + reverseLayout = { layoutInfo.reverseLayout }, +) + /** * Calculates a [ScrollbarState] driven by the changes in a [LazyGridState] * @@ -104,68 +142,27 @@ fun LazyListState.scrollbarState( fun LazyGridState.scrollbarState( itemsAvailable: Int, itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index, -): ScrollbarState { - val state = remember { ScrollbarState() } - LaunchedEffect(this, itemsAvailable) { - snapshotFlow { - if (itemsAvailable == 0) return@snapshotFlow null - - val visibleItemsInfo = layoutInfo.visibleItemsInfo - if (visibleItemsInfo.isEmpty()) return@snapshotFlow null - - val firstIndex = min( - a = interpolateFirstItemIndex( - visibleItems = visibleItemsInfo, - itemSize = { layoutInfo.orientation.valueOf(it.size) }, - offset = { layoutInfo.orientation.valueOf(it.offset) }, - nextItemOnMainAxis = { first -> - when (layoutInfo.orientation) { - Orientation.Vertical -> visibleItemsInfo.find { - it != first && it.row != first.row - } - - Orientation.Horizontal -> visibleItemsInfo.find { - it != first && it.column != first.column - } - } - }, - itemIndex = itemIndex, - ), - b = itemsAvailable.toFloat(), - ) - if (firstIndex.isNaN()) return@snapshotFlow null - - val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo -> - itemVisibilityPercentage( - itemSize = layoutInfo.orientation.valueOf(itemInfo.size), - itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset), - viewportStartOffset = layoutInfo.viewportStartOffset, - viewportEndOffset = layoutInfo.viewportEndOffset, - ) +) = genericScrollbarState( + itemsAvailable = itemsAvailable, + visibleItems = { layoutInfo.visibleItemsInfo }, + itemSize = { layoutInfo.orientation.valueOf(it.size) }, + offset = { layoutInfo.orientation.valueOf(it.offset) }, + nextItemOnMainAxis = { first -> + when (layoutInfo.orientation) { + Orientation.Vertical -> layoutInfo.visibleItemsInfo.find { + it != first && it.row != first.row } - val thumbTravelPercent = min( - a = firstIndex / itemsAvailable, - b = 1f, - ) - val thumbSizePercent = min( - a = itemsVisible / itemsAvailable, - b = 1f, - ) - scrollbarStateValue( - thumbSizePercent = thumbSizePercent, - thumbMovedPercent = when { - layoutInfo.reverseLayout -> 1f - thumbTravelPercent - else -> thumbTravelPercent - }, - ) + Orientation.Horizontal -> layoutInfo.visibleItemsInfo.find { + it != first && it.column != first.column + } } - .filterNotNull() - .distinctUntilChanged() - .collect { state.onScroll(it) } - } - return state -} + }, + index = itemIndex, + viewportStartOffset = { layoutInfo.viewportStartOffset }, + viewportEndOffset = { layoutInfo.viewportEndOffset }, + reverseLayout = { layoutInfo.reverseLayout }, +) /** * Remembers a [ScrollbarState] driven by the changes in a [LazyStaggeredGridState] @@ -178,57 +175,18 @@ fun LazyGridState.scrollbarState( fun LazyStaggeredGridState.scrollbarState( itemsAvailable: Int, itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index, -): ScrollbarState { - val state = remember { ScrollbarState() } - LaunchedEffect(this, itemsAvailable) { - snapshotFlow { - if (itemsAvailable == 0) return@snapshotFlow null - - val visibleItemsInfo = layoutInfo.visibleItemsInfo - if (visibleItemsInfo.isEmpty()) return@snapshotFlow null - - val firstIndex = min( - a = interpolateFirstItemIndex( - visibleItems = visibleItemsInfo, - itemSize = { layoutInfo.orientation.valueOf(it.size) }, - offset = { layoutInfo.orientation.valueOf(it.offset) }, - nextItemOnMainAxis = { first -> - visibleItemsInfo.find { it != first && it.lane == first.lane } - }, - itemIndex = itemIndex, - ), - b = itemsAvailable.toFloat(), - ) - if (firstIndex.isNaN()) return@snapshotFlow null - - val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo -> - itemVisibilityPercentage( - itemSize = layoutInfo.orientation.valueOf(itemInfo.size), - itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset), - viewportStartOffset = layoutInfo.viewportStartOffset, - viewportEndOffset = layoutInfo.viewportEndOffset, - ) - } - - val thumbTravelPercent = min( - a = firstIndex / itemsAvailable, - b = 1f, - ) - val thumbSizePercent = min( - a = itemsVisible / itemsAvailable, - b = 1f, - ) - scrollbarStateValue( - thumbSizePercent = thumbSizePercent, - thumbMovedPercent = thumbTravelPercent, - ) - } - .filterNotNull() - .distinctUntilChanged() - .collect { state.onScroll(it) } - } - return state -} +) = genericScrollbarState( + itemsAvailable = itemsAvailable, + visibleItems = { layoutInfo.visibleItemsInfo }, + itemSize = { layoutInfo.orientation.valueOf(it.size) }, + offset = { layoutInfo.orientation.valueOf(it.offset) }, + nextItemOnMainAxis = { first -> + layoutInfo.visibleItemsInfo.find { it != first && it.lane == first.lane } + }, + index = itemIndex, + viewportStartOffset = { layoutInfo.viewportStartOffset }, + viewportEndOffset = { layoutInfo.viewportEndOffset }, +) private inline fun List.floatSumOf(selector: (T) -> Float): Float = fold(initial = 0f) { accumulator, listItem -> accumulator + selector(listItem) }