Skip to content

Commit e9dae32

Browse files
fix: improve scrollbar thumb to sync during manual scroll
- improve drag sensitivity and reaching end for scrollbar component
1 parent 8b6aa65 commit e9dae32

File tree

1 file changed

+37
-20
lines changed

1 file changed

+37
-20
lines changed

app/src/main/kotlin/com/metrolist/music/ui/component/DraggableScrollBarOverlay.kt

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
99
import androidx.compose.foundation.layout.width
1010
import androidx.compose.foundation.lazy.LazyListState
1111
import androidx.compose.material3.MaterialTheme
12+
import androidx.compose.material3.LocalContentColor
1213
import androidx.compose.runtime.Composable
1314
import androidx.compose.runtime.LaunchedEffect
1415
import androidx.compose.runtime.derivedStateOf
@@ -35,21 +36,26 @@ import kotlin.math.max
3536
fun DraggableScrollbar(
3637
scrollState: LazyListState,
3738
modifier: Modifier = Modifier,
38-
thumbColor: Color = Color.Gray.copy(alpha = 0.7f),
39-
thumbColorActive: Color = MaterialTheme.colorScheme.primary,
39+
thumbColor: Color = LocalContentColor.current.copy(alpha = 0.8f),
40+
thumbColorActive: Color = MaterialTheme.colorScheme.secondary,
4041
thumbHeight: Dp = 72.dp,
4142
thumbWidth: Dp = 8.dp,
4243
thumbCornerRadius: Dp = 4.dp,
43-
trackWidth: Dp = 32.dp,
44+
trackWidth: Dp = 24.dp,
4445
minItemCountForScroll: Int = 15,
45-
minScrollRangeForDrag: Int = 4,
46+
minScrollRangeForDrag: Int = 1,
4647
headerItems: Int = 0 // <== Pass your header count here
4748
) {
4849
val density = LocalDensity.current
4950
val coroutineScope = rememberCoroutineScope()
5051
var isDragging by remember { mutableStateOf(false) }
5152
val animatedThumbY = remember { Animatable(0f) }
5253

54+
// Observe whether the list is being scrolled by the user to snap without lag
55+
val isUserScrolling by remember(scrollState) {
56+
derivedStateOf { scrollState.isScrollInProgress }
57+
}
58+
5359
val isScrollable by remember {
5460
derivedStateOf {
5561
val layoutInfo = scrollState.layoutInfo
@@ -88,20 +94,30 @@ fun DraggableScrollbar(
8894

8995
if (maxScrollIndex > minScrollRangeForDrag) {
9096
val touchProgress = (change.position.y / size.height).coerceIn(0f, 1f)
91-
val targetFractionalIndex = touchProgress * maxScrollIndex
92-
val targetIndex = headerItems + targetFractionalIndex.toInt()
93-
val targetFraction = targetFractionalIndex - targetFractionalIndex.toInt()
94-
95-
val avgItemHeightPx = visibleItems.first().size
96-
val targetOffset = (targetFraction * avgItemHeightPx).toInt()
97-
val clampedIndex =
98-
targetIndex.coerceIn(headerItems, layoutInfo.totalItemsCount - 1)
99-
100-
if (clampedIndex != lastTargetIndex || targetOffset != lastTargetOffset) {
101-
lastTargetIndex = clampedIndex
102-
lastTargetOffset = targetOffset
97+
// If the user drags to the very bottom, force-jump to the final item
98+
if (touchProgress >= 0.999f) {
99+
lastTargetIndex = layoutInfo.totalItemsCount - 1
100+
lastTargetOffset = 0
103101
coroutineScope.launch {
104-
scrollState.scrollToItem(clampedIndex)
102+
scrollState.scrollToItem(lastTargetIndex, 0)
103+
}
104+
} else {
105+
val targetFractionalIndex = touchProgress * maxScrollIndex
106+
val targetIndex = headerItems + targetFractionalIndex.toInt()
107+
val targetFraction = targetFractionalIndex - targetFractionalIndex.toInt()
108+
109+
val avgItemHeightPx = visibleItems.first().size
110+
val targetOffset = (targetFraction * avgItemHeightPx).toInt()
111+
val clampedIndex =
112+
targetIndex.coerceIn(headerItems, layoutInfo.totalItemsCount - 1)
113+
114+
if (clampedIndex != lastTargetIndex || targetOffset != lastTargetOffset) {
115+
lastTargetIndex = clampedIndex
116+
lastTargetOffset = targetOffset
117+
coroutineScope.launch {
118+
// Use offset so dragging can reach exact end positions
119+
scrollState.scrollToItem(clampedIndex, targetOffset)
120+
}
105121
}
106122
}
107123
}
@@ -136,8 +152,9 @@ fun DraggableScrollbar(
136152
}
137153
}
138154

139-
LaunchedEffect(targetThumbY, isDragging) {
140-
if (isDragging) {
155+
LaunchedEffect(targetThumbY, isDragging, isUserScrolling) {
156+
if (isDragging || isUserScrolling) {
157+
// Snap while the user is interacting (dragging the thumb or manually scrolling)
141158
animatedThumbY.snapTo(targetThumbY)
142159
} else {
143160
animatedThumbY.animateTo(
@@ -165,4 +182,4 @@ fun DraggableScrollbar(
165182
)
166183
}
167184
}
168-
}
185+
}

0 commit comments

Comments
 (0)