@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
99import androidx.compose.foundation.layout.width
1010import androidx.compose.foundation.lazy.LazyListState
1111import androidx.compose.material3.MaterialTheme
12+ import androidx.compose.material3.LocalContentColor
1213import androidx.compose.runtime.Composable
1314import androidx.compose.runtime.LaunchedEffect
1415import androidx.compose.runtime.derivedStateOf
@@ -35,21 +36,26 @@ import kotlin.math.max
3536fun 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