Improving the reusability and maintainability in ScrollbarExt.kt file

Signed-off-by: Rodrigo Dias Ferreira <rodrigo.dias.rdf@gmail.com>
pull/1550/head
Rodrigo Dias Ferreira 1 year ago
parent 50b13ecb21
commit 13d4e26758

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

Loading…
Cancel
Save