[新增] 为部分由Compose重写的界面增加快速滚动条

This commit is contained in:
HapeLee
2025-12-19 01:50:51 +08:00
parent e8a6c8e40b
commit ca9f4966d6
4 changed files with 569 additions and 8 deletions
@@ -34,6 +34,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.legado.app.data.entities.Bookmark
import io.legado.app.ui.widget.components.FastScrollLazyColumn
import io.legado.app.ui.widget.components.Scroller
import io.legado.app.ui.widget.components.SearchBarSection
import org.koin.androidx.compose.koinViewModel
@@ -142,7 +144,7 @@ fun AllBookmarkScreen(
}
}
) { paddingValues ->
LazyColumn(
FastScrollLazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
@@ -151,7 +153,7 @@ fun AllBookmarkScreen(
val isCollapsed = collapsedGroups.contains(headerKey.toString())
stickyHeader(key = headerKey.toString()) {
stickyHeader(key = "${Scroller.STICKY_HEADER_KEY_PREFIX}${headerKey}") {
BookAuthorHeader(
bookTitle = headerKey.bookName,
bookAuthor = headerKey.bookAuthor,
@@ -46,6 +46,7 @@ import io.legado.app.data.entities.ReplaceRule
import io.legado.app.ui.replace.edit.ReplaceEditActivity
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.AnimatedText
import io.legado.app.ui.widget.components.FastScrollLazyColumn
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.SelectionBottomBar
import kotlinx.coroutines.coroutineScope
@@ -325,14 +326,12 @@ fun ReplaceRuleScreen(
modifier = Modifier
.fillMaxSize()
) {
LazyColumn(
FastScrollLazyColumn(
state = listState,
modifier = Modifier
.padding(padding)
.fillMaxSize(),
contentPadding = PaddingValues(
start = 12.dp,
end = 12.dp,
top = 8.dp,
bottom = 120.dp
),
@@ -342,7 +341,9 @@ fun ReplaceRuleScreen(
val isSelected = selectedRuleIds.contains(ui.id)
ReplaceRuleItem(
modifier = Modifier.animateItem(),
modifier = Modifier
.padding(horizontal = 12.dp)
.animateItem(),
name = ui.name,
isEnabled = ui.isEnabled,
isSelected = isSelected,
@@ -631,8 +632,8 @@ fun ReplaceRuleItem(
)
) {
ListItem(
modifier = Modifier.animateContentSize(),
modifier = Modifier
.animateContentSize(),
headlineContent = {
AnimatedContent(targetState = name, label = "RuleNameAnimation") { targetName ->
Text(
@@ -0,0 +1,82 @@
package io.legado.app.ui.widget.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
/**
* LazyColumn with scrollbar (non-controllable).
* This code is copied from the repository https://github.com/komikku-app/komikku
*/
@Composable
fun ScrollbarLazyColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit,
) {
val direction = LocalLayoutDirection.current
val density = LocalDensity.current
val positionOffset = remember(contentPadding) {
with(density) { contentPadding.calculateEndPadding(direction).toPx() }
}
LazyColumn(
modifier = modifier,
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
userScrollEnabled = userScrollEnabled,
content = content,
)
}
/**
* LazyColumn with fast scroller.
*/
@Composable
fun FastScrollLazyColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit,
) {
VerticalFastScroller(
listState = state,
modifier = modifier,
topContentPadding = contentPadding.calculateTopPadding(),
endContentPadding = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
) {
LazyColumn(
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
userScrollEnabled = userScrollEnabled,
content = content,
)
}
}
@@ -0,0 +1,476 @@
package io.legado.app.ui.widget.components
import android.view.ViewConfiguration
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.systemGestureExclusion
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastLastOrNull
import androidx.compose.ui.util.fastMaxBy
import io.legado.app.ui.widget.components.Scroller.STICKY_HEADER_KEY_PREFIX
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* Draws vertical fast scroller to a lazy list
*
* Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list.
* This code is copied from the repository https://github.com/komikku-app/komikku
*/
@Composable
fun VerticalFastScroller(
listState: LazyListState,
modifier: Modifier = Modifier,
thumbAllowed: () -> Boolean = { true },
thumbColor: Color = MaterialTheme.colorScheme.primary,
topContentPadding: Dp = Dp.Hairline,
bottomContentPadding: Dp = Dp.Hairline,
endContentPadding: Dp = Dp.Hairline,
content: @Composable () -> Unit,
) {
SubcomposeLayout(modifier = modifier) { constraints ->
val contentPlaceable = subcompose("content", content).map { it.measure(constraints) }
val contentHeight = contentPlaceable.fastMaxBy { it.height }?.height ?: 0
val contentWidth = contentPlaceable.fastMaxBy { it.width }?.width ?: 0
val scrollerConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val scrollerPlaceable = subcompose("scroller") {
val layoutInfo = listState.layoutInfo
if (layoutInfo.visibleItemsInfo.isEmpty() || layoutInfo.totalItemsCount == 0) return@subcompose
val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() }
var thumbOffsetY by remember(thumbTopPadding) { mutableFloatStateOf(thumbTopPadding) }
val dragInteractionSource = remember { MutableInteractionSource() }
val isThumbDragged by dragInteractionSource.collectIsDraggedAsState()
val scrolled = remember {
MutableSharedFlow<Unit>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
}
// listState.isScrollInProgress occasionally flickers
val scrollStateTracker = remember { MutableData(listState.isScrollInProgress) }
val stableScrollInProgress = scrollStateTracker.value || listState.isScrollInProgress
scrollStateTracker.value = listState.isScrollInProgress
val anyScrollInProgress = stableScrollInProgress || isThumbDragged
val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() }
val heightPx = contentHeight.toFloat() -
thumbTopPadding -
thumbBottomPadding -
listState.layoutInfo.afterContentPadding
val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() }
val trackHeightPx = heightPx - thumbHeightPx
val scrollHeightPx = contentHeight.toFloat() -
listState.layoutInfo.beforeContentPadding -
listState.layoutInfo.afterContentPadding -
thumbBottomPadding
val visibleItems = layoutInfo.visibleItemsInfo
val topItem = visibleItems.fastFirstOrNull {
it.bottom >= 0 &&
(it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true
} ?: visibleItems.first()
val bottomItem = visibleItems.fastLastOrNull {
it.top <= scrollHeightPx &&
(it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true
} ?: visibleItems.last()
val topHiddenProportion = -1f * topItem.top / topItem.size.coerceAtLeast(1)
val bottomHiddenProportion = (bottomItem.bottom - scrollHeightPx) / bottomItem.size.coerceAtLeast(1)
val previousSections = topHiddenProportion + topItem.index
val remainingSections = bottomHiddenProportion + (layoutInfo.totalItemsCount - (bottomItem.index + 1))
val scrollableSections = previousSections + remainingSections
val layoutChangeTracker = remember { MutableData(scrollableSections) }
val layoutChanged = !anyScrollInProgress && abs(layoutChangeTracker.value - scrollableSections) > 0.1
layoutChangeTracker.value = scrollableSections
val estimateConfidence = remember { MutableData(remainingSections) }
if (layoutChanged) estimateConfidence.value = remainingSections
val maxRemainingSections = remember(estimateConfidence.value) { scrollableSections }
estimateConfidence.value = max(estimateConfidence.value, remainingSections)
if (maxRemainingSections < 0.5) return@subcompose
// When thumb dragged
LaunchedEffect(thumbOffsetY) {
if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect
val thumbProportion = (thumbOffsetY - thumbTopPadding) / trackHeightPx
if (thumbProportion <= 0.001f) {
estimateConfidence.value = -1f
listState.scrollToItem(index = 0, scrollOffset = 0)
scrolled.tryEmit(Unit)
return@LaunchedEffect
}
val scrollRemainingSections = (1f - thumbProportion) * maxRemainingSections
val currentSection = layoutInfo.totalItemsCount - scrollRemainingSections
val scrollSectionIndex = currentSection.toInt().coerceAtMost(layoutInfo.totalItemsCount)
val expectedScrollItem = visibleItems.find { it.index == scrollSectionIndex } ?: visibleItems.first()
val scrollRelativeOffset = expectedScrollItem.size * (currentSection - scrollSectionIndex)
val scrollSectionOffset = (scrollRelativeOffset - scrollHeightPx).roundToInt()
val scrollItemIndex = scrollSectionIndex.coerceIn(0, layoutInfo.totalItemsCount - 1)
val scrollItemOffset = scrollSectionOffset + (scrollSectionIndex - scrollItemIndex) * bottomItem.size
listState.scrollToItem(index = scrollItemIndex, scrollOffset = scrollItemOffset)
scrolled.tryEmit(Unit)
}
// When list scrolled
if (layoutInfo.totalItemsCount != 0 && !isThumbDragged) {
val proportion = 1f - remainingSections / maxRemainingSections
thumbOffsetY = trackHeightPx * proportion + thumbTopPadding
if (stableScrollInProgress) scrolled.tryEmit(Unit)
}
// Thumb alpha
val alpha = remember { Animatable(0f) }
val isThumbVisible = alpha.value > 0f
LaunchedEffect(scrolled, alpha) {
scrolled
.sample(100)
.collectLatest {
if (thumbAllowed()) {
alpha.snapTo(1f)
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
} else {
alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec)
}
}
}
Box(
modifier = Modifier
.offset { IntOffset(0, thumbOffsetY.roundToInt()) }
.then(
// Recompose opts
if (isThumbVisible && !listState.isScrollInProgress) {
Modifier.draggable(
interactionSource = dragInteractionSource,
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
val newOffsetY = thumbOffsetY + delta
thumbOffsetY = newOffsetY.coerceIn(
thumbTopPadding,
thumbTopPadding + trackHeightPx,
)
},
)
} else {
Modifier
},
)
.then(
// Exclude thumb from gesture area only when needed
if (isThumbVisible && !isThumbDragged && !listState.isScrollInProgress) {
Modifier.systemGestureExclusion()
} else {
Modifier
},
)
.height(ThumbLength)
.padding(horizontal = 8.dp)
.padding(end = endContentPadding)
.width(ThumbThickness)
.alpha(alpha.value)
.background(color = thumbColor, shape = ThumbShape),
)
}.map { it.measure(scrollerConstraints) }
val scrollerWidth = scrollerPlaceable.fastMaxBy { it.width }?.width ?: 0
layout(contentWidth, contentHeight) {
contentPlaceable.fastForEach {
it.place(0, 0)
}
scrollerPlaceable.fastForEach {
it.placeRelative(contentWidth - scrollerWidth, 0)
}
}
}
}
@Composable
private fun rememberColumnWidthSums(
columns: GridCells,
horizontalArrangement: Arrangement.Horizontal,
contentPadding: PaddingValues,
) = remember<Density.(Constraints) -> List<Int>>(
columns,
horizontalArrangement,
contentPadding,
) {
{ constraints ->
require(constraints.maxWidth != Constraints.Infinity) {
"LazyVerticalGrid's width should be bound by parent"
}
val horizontalPadding = contentPadding.calculateStartPadding(LayoutDirection.Ltr) +
contentPadding.calculateEndPadding(LayoutDirection.Ltr)
val gridWidth = constraints.maxWidth - horizontalPadding.roundToPx()
with(columns) {
calculateCrossAxisCellSizes(
gridWidth,
horizontalArrangement.spacing.roundToPx(),
).toMutableList().apply {
for (i in 1..<size) {
this[i] += this[i - 1]
}
}
}
}
}
/*
VerticalGridFastScroller was written with a regularity assumption, so it is slightly inaccurate for layouts with
varying row sizes.
*/
// TODO: Ideally rewrite VerticalGridFastScroller to use similar logic as VerticalFastScroller
@Composable
fun VerticalGridFastScroller(
state: LazyGridState,
columns: GridCells,
arrangement: Arrangement.Horizontal,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
thumbAllowed: () -> Boolean = { true },
thumbColor: Color = MaterialTheme.colorScheme.primary,
topContentPadding: Dp = Dp.Hairline,
bottomContentPadding: Dp = Dp.Hairline,
endContentPadding: Dp = Dp.Hairline,
content: @Composable () -> Unit,
) {
val slotSizesSums = rememberColumnWidthSums(
columns = columns,
horizontalArrangement = arrangement,
contentPadding = contentPadding,
)
SubcomposeLayout(modifier = modifier) { constraints ->
val contentPlaceable = subcompose("content", content).map { it.measure(constraints) }
val contentHeight = contentPlaceable.fastMaxBy { it.height }?.height ?: 0
val contentWidth = contentPlaceable.fastMaxBy { it.width }?.width ?: 0
val scrollerConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val scrollerPlaceable = subcompose("scroller") {
val layoutInfo = state.layoutInfo
val showScroller = remember(columns, layoutInfo.totalItemsCount) {
layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount
}
if (!showScroller) return@subcompose
val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() }
var thumbOffsetY by remember(thumbTopPadding) { mutableFloatStateOf(thumbTopPadding) }
val dragInteractionSource = remember { MutableInteractionSource() }
val isThumbDragged by dragInteractionSource.collectIsDraggedAsState()
val scrolled = remember {
MutableSharedFlow<Unit>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
}
val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() }
val heightPx = contentHeight.toFloat() -
thumbTopPadding -
thumbBottomPadding -
state.layoutInfo.afterContentPadding
val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() }
val trackHeightPx = heightPx - thumbHeightPx
val columnCount = remember(columns) { slotSizesSums(constraints).size.coerceAtLeast(1) }
val scrollRange = remember(columns) { computeGridScrollRange(state = state, columnCount = columnCount) }
// When thumb dragged
LaunchedEffect(thumbOffsetY) {
if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect
val visibleItems = state.layoutInfo.visibleItemsInfo
val startChild = visibleItems.first()
val endChild = visibleItems.last()
val laidOutArea = (endChild.offset.y + endChild.size.height) - startChild.offset.y
val laidOutRows = 1 + abs(endChild.index - startChild.index) / columnCount
val avgSizePerRow = laidOutArea.toFloat() / laidOutRows
val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx
val scrollAmt = scrollRatio * (scrollRange.toFloat() - heightPx).coerceAtLeast(1f)
val rowNumber = (scrollAmt / avgSizePerRow).toInt()
val rowOffset = scrollAmt - rowNumber * avgSizePerRow
state.scrollToItem(index = columnCount * rowNumber, scrollOffset = rowOffset.roundToInt())
scrolled.tryEmit(Unit)
}
// When list scrolled
LaunchedEffect(state.firstVisibleItemScrollOffset) {
if (state.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect
val scrollOffset = computeGridScrollOffset(state = state, columnCount = columnCount)
/*
LazyGridItemInfo doesn't always give the accurate height of the object, so we clamp the proportion
at 1 to ensure that there are no issues due to this -- ideally we would correctly compute the value
*/
val extraScrollRange = (scrollRange.toFloat() - heightPx).coerceAtLeast(1f)
val proportion = (scrollOffset.toFloat() / extraScrollRange).coerceAtMost(1f)
thumbOffsetY = trackHeightPx * proportion + thumbTopPadding
scrolled.tryEmit(Unit)
}
// Thumb alpha
val alpha = remember { Animatable(0f) }
val isThumbVisible = alpha.value > 0f
LaunchedEffect(scrolled, alpha) {
scrolled
.sample(100)
.collectLatest {
if (thumbAllowed()) {
alpha.snapTo(1f)
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
} else {
alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec)
}
}
}
Box(
modifier = Modifier
.offset { IntOffset(0, thumbOffsetY.roundToInt()) }
.then(
// Recompose opts
if (isThumbVisible && !state.isScrollInProgress) {
Modifier.draggable(
interactionSource = dragInteractionSource,
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
val newOffsetY = thumbOffsetY + delta
thumbOffsetY = newOffsetY.coerceIn(
thumbTopPadding,
thumbTopPadding + trackHeightPx,
)
},
)
} else {
Modifier
},
)
.then(
// Exclude thumb from gesture area only when needed
if (isThumbVisible && !isThumbDragged && !state.isScrollInProgress) {
Modifier.systemGestureExclusion()
} else {
Modifier
},
)
.height(ThumbLength)
.padding(end = endContentPadding)
.width(ThumbThickness)
.alpha(alpha.value)
.background(color = thumbColor, shape = ThumbShape),
)
}.map { it.measure(scrollerConstraints) }
val scrollerWidth = scrollerPlaceable.fastMaxBy { it.width }?.width ?: 0
layout(contentWidth, contentHeight) {
contentPlaceable.fastForEach {
it.place(0, 0)
}
scrollerPlaceable.fastForEach {
it.placeRelative(contentWidth - scrollerWidth, 0)
}
}
}
}
// TODO: not sure why abs corrections are in the following functions; these can probably be removed
private fun computeGridScrollOffset(state: LazyGridState, columnCount: Int): Int {
if (state.layoutInfo.totalItemsCount == 0) return 0
val visibleItems = state.layoutInfo.visibleItemsInfo
val startChild = visibleItems.first()
val endChild = visibleItems.last()
val laidOutArea = (endChild.offset.y + endChild.size.height) - startChild.offset.y
val laidOutRows = 1 + abs(endChild.index - startChild.index) / columnCount
val avgSizePerRow = laidOutArea.toFloat() / laidOutRows
val rowsBefore = min(startChild.index, endChild.index).coerceAtLeast(0) / columnCount
return (rowsBefore * avgSizePerRow - startChild.offset.y).roundToInt()
}
private fun computeGridScrollRange(state: LazyGridState, columnCount: Int): Int {
if (state.layoutInfo.totalItemsCount == 0) return 0
val visibleItems = state.layoutInfo.visibleItemsInfo
val startChild = visibleItems.first()
val endChild = visibleItems.last()
val laidOutArea = (endChild.offset.y + endChild.size.height) - startChild.offset.y
val laidOutRows = 1 + abs(endChild.index - startChild.index) / columnCount
val avgSizePerRow = laidOutArea.toFloat() / laidOutRows
val totalRows = 1 + (state.layoutInfo.totalItemsCount - 1) / columnCount
val endSpacing = avgSizePerRow - endChild.size.height
return (endSpacing + (laidOutArea.toFloat() / laidOutRows) * totalRows).roundToInt()
}
private class MutableData<T>(var value: T)
object Scroller {
const val STICKY_HEADER_KEY_PREFIX = "sticky:"
}
private val ThumbLength = 48.dp
private val ThumbThickness = 12.dp
private val ThumbShape = RoundedCornerShape(ThumbThickness / 2)
private val FadeOutAnimationSpec = tween<Float>(
durationMillis = ViewConfiguration.getScrollBarFadeDuration(),
delayMillis = 2000,
)
private val ImmediateFadeOutAnimationSpec = tween<Float>(
durationMillis = ViewConfiguration.getScrollBarFadeDuration(),
)
private val LazyListItemInfo.top: Int
get() = offset
private val LazyListItemInfo.bottom: Int
get() = offset + size