Address PR feedback

Change-Id: I50b49d3e216535e384b64849f915b37f6e2acbbc
pull/1837/head
dahunsi 2 years ago
parent 738fbc190d
commit 401b42388a

@ -1,95 +1,23 @@
/* /*
* Copyright 2023 The Android Open Source Project * Copyright 2023 The Android Open Source Project
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* https://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
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.ScrollableState import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min
/**
* Calculates the [ScrollbarState] for lazy layouts.
* @param itemsAvailable the total amount of items available to scroll in the layout.
* @param visibleItems a list of items currently visible in the layout.
* @param firstVisibleItemIndex a function for interpolating the first visible index in the lazy layout
* as scrolling progresses for smooth and linear scrollbar thumb progression.
* [itemsAvailable].
* @param reverseLayout if the items in the backing lazy layout are laid out in reverse order.
* */
@Composable
internal inline fun <LazyState : ScrollableState, LazyStateItem> LazyState.scrollbarState(
itemsAvailable: Int,
crossinline visibleItems: LazyState.() -> List<LazyStateItem>,
crossinline firstVisibleItemIndex: LazyState.(List<LazyStateItem>) -> Float,
crossinline itemPercentVisible: LazyState.(LazyStateItem) -> Float,
crossinline reverseLayout: LazyState.() -> Boolean,
): ScrollbarState {
var state by remember { mutableStateOf(ScrollbarState.FULL) }
LaunchedEffect(
key1 = this,
key2 = itemsAvailable,
) {
snapshotFlow {
if (itemsAvailable == 0) return@snapshotFlow null
val visibleItemsInfo = visibleItems(this@scrollbarState)
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null
val firstIndex = min(
a = firstVisibleItemIndex(visibleItemsInfo),
b = itemsAvailable.toFloat(),
)
if (firstIndex.isNaN()) return@snapshotFlow null
val itemsVisible = visibleItemsInfo.sumOf {
itemPercentVisible(it).toDouble()
}.toFloat()
val thumbTravelPercent = min(
a = firstIndex / itemsAvailable,
b = 1f,
)
val thumbSizePercent = min(
a = itemsVisible / itemsAvailable,
b = 1f,
)
ScrollbarState(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = when {
reverseLayout() -> 1f - thumbTravelPercent
else -> thumbTravelPercent
},
)
}
.filterNotNull()
.distinctUntilChanged()
.collect { state = it }
}
return state
}
/** /**
* Linearly interpolates the index for the first item in [visibleItems] for smooth scrollbar * Linearly interpolates the index for the first item in [visibleItems] for smooth scrollbar

@ -1,17 +1,17 @@
/* /*
* Copyright 2021 The Android Open Source Project * Copyright 2023 The Android Open Source Project
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* https://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar
@ -24,6 +24,11 @@ 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.snapshotFlow
import kotlinx.coroutines.flow.distinctUntilChanged
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 [LazyListState].
@ -35,29 +40,58 @@ import androidx.compose.runtime.Composable
fun LazyListState.scrollbarState( fun LazyListState.scrollbarState(
itemsAvailable: Int, itemsAvailable: Int,
itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index, itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index,
): ScrollbarState = ): ScrollbarState = produceState(
scrollbarState( initialValue = ScrollbarState.FULL,
itemsAvailable = itemsAvailable, key1 = this,
visibleItems = { layoutInfo.visibleItemsInfo }, key2 = itemsAvailable,
firstVisibleItemIndex = { visibleItems -> ) {
interpolateFirstItemIndex( snapshotFlow {
visibleItems = visibleItems, if (itemsAvailable == 0) return@snapshotFlow null
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null
val firstIndex = min(
a = interpolateFirstItemIndex(
visibleItems = visibleItemsInfo,
itemSize = { it.size }, itemSize = { it.size },
offset = { it.offset }, offset = { it.offset },
nextItemOnMainAxis = { first -> visibleItems.find { it != first } }, nextItemOnMainAxis = { first -> visibleItemsInfo.find { it != first } },
itemIndex = itemIndex, itemIndex = itemIndex,
) ),
}, b = itemsAvailable.toFloat(),
itemPercentVisible = itemPercentVisible@{ itemInfo -> )
if (firstIndex.isNaN()) return@snapshotFlow null
val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo ->
itemVisibilityPercentage( itemVisibilityPercentage(
itemSize = itemInfo.size, itemSize = itemInfo.size,
itemStartOffset = itemInfo.offset, itemStartOffset = itemInfo.offset,
viewportStartOffset = layoutInfo.viewportStartOffset, viewportStartOffset = layoutInfo.viewportStartOffset,
viewportEndOffset = layoutInfo.viewportEndOffset, viewportEndOffset = layoutInfo.viewportEndOffset,
) )
}, }
reverseLayout = { layoutInfo.reverseLayout },
) val thumbTravelPercent = min(
a = firstIndex / itemsAvailable,
b = 1f,
)
val thumbSizePercent = min(
a = itemsVisible / itemsAvailable,
b = 1f,
)
ScrollbarState(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = when {
layoutInfo.reverseLayout -> 1f - thumbTravelPercent
else -> thumbTravelPercent
},
)
}
.filterNotNull()
.distinctUntilChanged()
.collect { value = it }
}.value
/** /**
* Calculates a [ScrollbarState] driven by the changes in a [LazyGridState] * Calculates a [ScrollbarState] driven by the changes in a [LazyGridState]
@ -69,41 +103,68 @@ fun LazyListState.scrollbarState(
fun LazyGridState.scrollbarState( fun LazyGridState.scrollbarState(
itemsAvailable: Int, itemsAvailable: Int,
itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index, itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index,
): ScrollbarState = ): ScrollbarState = produceState(
scrollbarState( initialValue = ScrollbarState.FULL,
itemsAvailable = itemsAvailable, key1 = this,
visibleItems = { layoutInfo.visibleItemsInfo }, key2 = itemsAvailable,
firstVisibleItemIndex = { visibleItems -> ) {
interpolateFirstItemIndex( snapshotFlow {
visibleItems = visibleItems, if (itemsAvailable == 0) return@snapshotFlow null
itemSize = {
layoutInfo.orientation.valueOf(it.size) 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) }, offset = { layoutInfo.orientation.valueOf(it.offset) },
nextItemOnMainAxis = { first -> nextItemOnMainAxis = { first ->
when (layoutInfo.orientation) { when (layoutInfo.orientation) {
Orientation.Vertical -> visibleItems.find { Orientation.Vertical -> visibleItemsInfo.find {
it != first && it.row != first.row it != first && it.row != first.row
} }
Orientation.Horizontal -> visibleItems.find { Orientation.Horizontal -> visibleItemsInfo.find {
it != first && it.column != first.column it != first && it.column != first.column
} }
} }
}, },
itemIndex = itemIndex, itemIndex = itemIndex,
) ),
}, b = itemsAvailable.toFloat(),
itemPercentVisible = itemPercentVisible@{ itemInfo -> )
if (firstIndex.isNaN()) return@snapshotFlow null
val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo ->
itemVisibilityPercentage( itemVisibilityPercentage(
itemSize = layoutInfo.orientation.valueOf(itemInfo.size), itemSize = layoutInfo.orientation.valueOf(itemInfo.size),
itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset), itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset),
viewportStartOffset = layoutInfo.viewportStartOffset, viewportStartOffset = layoutInfo.viewportStartOffset,
viewportEndOffset = layoutInfo.viewportEndOffset, viewportEndOffset = layoutInfo.viewportEndOffset,
) )
}, }
reverseLayout = { layoutInfo.reverseLayout },
) val thumbTravelPercent = min(
a = firstIndex / itemsAvailable,
b = 1f,
)
val thumbSizePercent = min(
a = itemsVisible / itemsAvailable,
b = 1f,
)
ScrollbarState(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = when {
layoutInfo.reverseLayout -> 1f - thumbTravelPercent
else -> thumbTravelPercent
},
)
}
.filterNotNull()
.distinctUntilChanged()
.collect { value = it }
}.value
/** /**
* Remembers a [ScrollbarState] driven by the changes in a [LazyStaggeredGridState] * Remembers a [ScrollbarState] driven by the changes in a [LazyStaggeredGridState]
@ -116,28 +177,62 @@ fun LazyGridState.scrollbarState(
fun LazyStaggeredGridState.scrollbarState( fun LazyStaggeredGridState.scrollbarState(
itemsAvailable: Int, itemsAvailable: Int,
itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index, itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index,
): ScrollbarState = ): ScrollbarState = produceState(
scrollbarState( initialValue = ScrollbarState.FULL,
itemsAvailable = itemsAvailable, key1 = this,
visibleItems = { layoutInfo.visibleItemsInfo }, key2 = itemsAvailable,
firstVisibleItemIndex = { visibleItems -> ) {
interpolateFirstItemIndex( snapshotFlow {
visibleItems = visibleItems, 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 ->
visibleItems.find { it != first && it.lane == first.lane } visibleItemsInfo.find { it != first && it.lane == first.lane }
}, },
itemIndex = itemIndex, itemIndex = itemIndex,
) ),
}, b = itemsAvailable.toFloat(),
itemPercentVisible = itemPercentVisible@{ itemInfo -> )
if (firstIndex.isNaN()) return@snapshotFlow null
val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo ->
itemVisibilityPercentage( itemVisibilityPercentage(
itemSize = layoutInfo.orientation.valueOf(itemInfo.size), itemSize = layoutInfo.orientation.valueOf(itemInfo.size),
itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset), itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset),
viewportStartOffset = layoutInfo.viewportStartOffset, viewportStartOffset = layoutInfo.viewportStartOffset,
viewportEndOffset = layoutInfo.viewportEndOffset, viewportEndOffset = layoutInfo.viewportEndOffset,
) )
}, }
reverseLayout = { false },
) val thumbTravelPercent = min(
a = firstIndex / itemsAvailable,
b = 1f,
)
val thumbSizePercent = min(
a = itemsVisible / itemsAvailable,
b = 1f,
)
ScrollbarState(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = thumbTravelPercent,
)
}
.filterNotNull()
.distinctUntilChanged()
.collect { value = it }
}.value
private inline fun <T> List<T>.floatSumOf(selector: (T) -> Float): Float {
var sum = 0f
for (element in this) {
sum += selector(element)
}
return sum
}
Loading…
Cancel
Save