Address PR feedback

Change-Id: I50b49d3e216535e384b64849f915b37f6e2acbbc
pull/947/head
dahunsi 1 year ago
parent 77451779cd
commit beaa85739d

@ -1,95 +1,23 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar
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.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

@ -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");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.LazyStaggeredGridState
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].
@ -35,29 +40,58 @@ import androidx.compose.runtime.Composable
fun LazyListState.scrollbarState(
itemsAvailable: Int,
itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index,
): ScrollbarState =
scrollbarState(
itemsAvailable = itemsAvailable,
visibleItems = { layoutInfo.visibleItemsInfo },
firstVisibleItemIndex = { visibleItems ->
interpolateFirstItemIndex(
visibleItems = visibleItems,
): ScrollbarState = produceState(
initialValue = ScrollbarState.FULL,
key1 = this,
key2 = 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 = { it.size },
offset = { it.offset },
nextItemOnMainAxis = { first -> visibleItems.find { it != first } },
nextItemOnMainAxis = { first -> visibleItemsInfo.find { it != first } },
itemIndex = itemIndex,
)
},
itemPercentVisible = itemPercentVisible@{ itemInfo ->
),
b = itemsAvailable.toFloat(),
)
if (firstIndex.isNaN()) return@snapshotFlow null
val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo ->
itemVisibilityPercentage(
itemSize = itemInfo.size,
itemStartOffset = itemInfo.offset,
viewportStartOffset = layoutInfo.viewportStartOffset,
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]
@ -69,41 +103,68 @@ fun LazyListState.scrollbarState(
fun LazyGridState.scrollbarState(
itemsAvailable: Int,
itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index,
): ScrollbarState =
scrollbarState(
itemsAvailable = itemsAvailable,
visibleItems = { layoutInfo.visibleItemsInfo },
firstVisibleItemIndex = { visibleItems ->
interpolateFirstItemIndex(
visibleItems = visibleItems,
itemSize = {
layoutInfo.orientation.valueOf(it.size)
},
): ScrollbarState = produceState(
initialValue = ScrollbarState.FULL,
key1 = this,
key2 = 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 -> visibleItems.find {
Orientation.Vertical -> visibleItemsInfo.find {
it != first && it.row != first.row
}
Orientation.Horizontal -> visibleItems.find {
Orientation.Horizontal -> visibleItemsInfo.find {
it != first && it.column != first.column
}
}
},
itemIndex = itemIndex,
)
},
itemPercentVisible = itemPercentVisible@{ itemInfo ->
),
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,
)
},
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]
@ -116,28 +177,62 @@ fun LazyGridState.scrollbarState(
fun LazyStaggeredGridState.scrollbarState(
itemsAvailable: Int,
itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index,
): ScrollbarState =
scrollbarState(
itemsAvailable = itemsAvailable,
visibleItems = { layoutInfo.visibleItemsInfo },
firstVisibleItemIndex = { visibleItems ->
interpolateFirstItemIndex(
visibleItems = visibleItems,
): ScrollbarState = produceState(
initialValue = ScrollbarState.FULL,
key1 = this,
key2 = 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 ->
visibleItems.find { it != first && it.lane == first.lane }
visibleItemsInfo.find { it != first && it.lane == first.lane }
},
itemIndex = itemIndex,
)
},
itemPercentVisible = itemPercentVisible@{ itemInfo ->
),
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,
)
},
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