Last active
June 29, 2026 20:58
-
-
Save ahmedre/89c62832f491c2725fff2598eed291a2 to your computer and use it in GitHub Desktop.
Slider Examples for Blog Post
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package net.helw.playground.seekbar | |
| import androidx.compose.animation.AnimatedVisibility | |
| import androidx.compose.animation.fadeIn | |
| import androidx.compose.animation.fadeOut | |
| import androidx.compose.foundation.gestures.awaitEachGesture | |
| import androidx.compose.foundation.gestures.awaitFirstDown | |
| import androidx.compose.foundation.interaction.MutableInteractionSource | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.foundation.layout.offset | |
| import androidx.compose.material3.Button | |
| import androidx.compose.material3.ExperimentalMaterial3Api | |
| import androidx.compose.material3.Slider | |
| import androidx.compose.material3.SliderDefaults | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.mutableIntStateOf | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.ui.Alignment | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.input.pointer.pointerInput | |
| import androidx.compose.ui.layout.onSizeChanged | |
| import androidx.compose.ui.platform.LocalDensity | |
| import androidx.compose.ui.unit.IntOffset | |
| import androidx.compose.ui.unit.dp | |
| import kotlin.math.roundToInt | |
| @Composable | |
| fun BoxBottomBar( | |
| currentPage: Int, | |
| onSetCurrentPage: (Int) -> Unit, | |
| modifier: Modifier = Modifier | |
| ) { | |
| val isInteracting = remember { mutableStateOf(false) } | |
| // later, this should be the actual button width | |
| val buttonWidthDp = 72.dp | |
| val sliderWidthPx = remember { mutableIntStateOf(0) } | |
| val thumbWidthPx = with(LocalDensity.current) { 4.dp.toPx() } | |
| val fraction = ((currentPage - 1f) / (604f - 1f)).coerceIn(0f, 1f) | |
| val usableSliderPx = sliderWidthPx.intValue - thumbWidthPx | |
| val buttonWidthPx = with(LocalDensity.current) { buttonWidthDp.toPx() } | |
| val edge = (buttonWidthPx / usableSliderPx).coerceIn(0f, 0.5f) | |
| val thumbOffsetPx = when { | |
| fraction < edge -> buttonWidthPx | |
| fraction > 1f - edge -> -buttonWidthPx | |
| else -> 0f | |
| } | |
| Box(modifier = modifier.fillMaxWidth()) { | |
| val interactionSource = remember { MutableInteractionSource() } | |
| @OptIn(ExperimentalMaterial3Api::class) | |
| Slider( | |
| interactionSource = interactionSource, | |
| value = currentPage.toFloat(), | |
| onValueChange = { onSetCurrentPage(it.roundToInt()) }, | |
| valueRange = 1f.rangeTo(604f), | |
| onValueChangeFinished = { isInteracting.value = false }, | |
| modifier = Modifier | |
| .pointerInput(Unit) { | |
| awaitEachGesture { | |
| awaitFirstDown(requireUnconsumed = false) | |
| isInteracting.value = true | |
| } | |
| } | |
| .onSizeChanged { sliderWidthPx.intValue = it.width }, | |
| thumb = { | |
| SliderDefaults.Thumb( | |
| interactionSource = interactionSource, | |
| colors = SliderDefaults.colors(), | |
| enabled = true, | |
| modifier = Modifier.offset { | |
| if (!isInteracting.value) { | |
| IntOffset( | |
| thumbOffsetPx.roundToInt(), | |
| 0 | |
| ) | |
| } else { | |
| IntOffset(0, 0) | |
| } | |
| } | |
| ) | |
| }, | |
| ) | |
| AnimatedVisibility( | |
| visible = !isInteracting.value, | |
| modifier = Modifier.align(Alignment.CenterStart), | |
| exit = fadeOut(), | |
| enter = fadeIn() | |
| ) { | |
| Button(onClick = {}) { | |
| Text("B1") | |
| } | |
| } | |
| AnimatedVisibility( | |
| visible = !isInteracting.value, | |
| modifier = Modifier.align(Alignment.CenterEnd), | |
| exit = fadeOut(), | |
| enter = fadeIn() | |
| ) { | |
| Button(onClick = {}) { | |
| Text("B2") | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package net.helw.playground.seekbar | |
| import androidx.compose.animation.AnimatedVisibility | |
| import androidx.compose.animation.fadeIn | |
| import androidx.compose.animation.fadeOut | |
| import androidx.compose.foundation.gestures.awaitEachGesture | |
| import androidx.compose.foundation.gestures.awaitFirstDown | |
| import androidx.compose.foundation.interaction.MutableInteractionSource | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.foundation.layout.offset | |
| import androidx.compose.material3.Button | |
| import androidx.compose.material3.ExperimentalMaterial3Api | |
| import androidx.compose.material3.Slider | |
| import androidx.compose.material3.SliderDefaults | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.ui.Alignment | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.input.pointer.pointerInput | |
| import androidx.compose.ui.platform.LocalDensity | |
| import androidx.compose.ui.unit.IntOffset | |
| import androidx.compose.ui.unit.dp | |
| import androidx.compose.ui.util.lerp | |
| import kotlin.math.roundToInt | |
| @Composable | |
| fun BoxBottomBar( | |
| currentPage: Int, | |
| onSetCurrentPage: (Int) -> Unit, | |
| modifier: Modifier = Modifier | |
| ) { | |
| val isInteracting = remember { mutableStateOf(false) } | |
| // later, this should be the actual button width | |
| val buttonWidthDp = 72.dp | |
| val fraction = ((currentPage - 1f) / (604f - 1f)).coerceIn(0f, 1f) | |
| val buttonWidthPx = with(LocalDensity.current) { buttonWidthDp.toPx() } | |
| val thumbOffsetPx = lerp(buttonWidthPx, -buttonWidthPx, fraction) | |
| Box(modifier = modifier.fillMaxWidth()) { | |
| val interactionSource = remember { MutableInteractionSource() } | |
| @OptIn(ExperimentalMaterial3Api::class) | |
| Slider( | |
| interactionSource = interactionSource, | |
| value = currentPage.toFloat(), | |
| onValueChange = { onSetCurrentPage(it.roundToInt()) }, | |
| valueRange = 1f.rangeTo(604f), | |
| onValueChangeFinished = { isInteracting.value = false }, | |
| thumb = { | |
| SliderDefaults.Thumb( | |
| interactionSource = interactionSource, | |
| colors = SliderDefaults.colors(), | |
| enabled = true, | |
| modifier = Modifier.offset { | |
| if (!isInteracting.value) { | |
| IntOffset( | |
| thumbOffsetPx.roundToInt(), | |
| 0 | |
| ) | |
| } else { | |
| IntOffset(0, 0) | |
| } | |
| } | |
| ) | |
| }, | |
| modifier = Modifier.pointerInput(Unit) { | |
| awaitEachGesture { | |
| awaitFirstDown(requireUnconsumed = false) | |
| isInteracting.value = true | |
| } | |
| } | |
| ) | |
| AnimatedVisibility( | |
| visible = !isInteracting.value, | |
| modifier = Modifier.align(Alignment.CenterStart), | |
| exit = fadeOut(), | |
| enter = fadeIn() | |
| ) { | |
| Button(onClick = {}) { | |
| Text("B1") | |
| } | |
| } | |
| AnimatedVisibility( | |
| visible = !isInteracting.value, | |
| modifier = Modifier.align(Alignment.CenterEnd), | |
| exit = fadeOut(), | |
| enter = fadeIn() | |
| ) { | |
| Button(onClick = {}) { | |
| Text("B2") | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package net.helw.playground.seekbar | |
| import androidx.compose.animation.AnimatedVisibility | |
| import androidx.compose.animation.fadeIn | |
| import androidx.compose.animation.fadeOut | |
| import androidx.compose.foundation.gestures.awaitEachGesture | |
| import androidx.compose.foundation.gestures.awaitFirstDown | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.material3.Button | |
| import androidx.compose.material3.Slider | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.ui.Alignment | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.input.pointer.pointerInput | |
| import kotlin.math.roundToInt | |
| @Composable | |
| fun BoxBottomBar( | |
| currentPage: Int, | |
| onSetCurrentPage: (Int) -> Unit, | |
| modifier: Modifier = Modifier | |
| ) { | |
| val isInteracting = remember { mutableStateOf(false) } | |
| Box(modifier = modifier.fillMaxWidth()) { | |
| Slider( | |
| value = currentPage.toFloat(), | |
| onValueChange = { onSetCurrentPage(it.roundToInt()) }, | |
| valueRange = 1f.rangeTo(604f), | |
| onValueChangeFinished = { isInteracting.value = false }, | |
| modifier = Modifier.pointerInput(Unit) { | |
| awaitEachGesture { | |
| awaitFirstDown(requireUnconsumed = false) | |
| isInteracting.value = true | |
| } | |
| } | |
| ) | |
| AnimatedVisibility( | |
| visible = !isInteracting.value, | |
| modifier = Modifier.align(Alignment.CenterStart), | |
| exit = fadeOut(), | |
| enter = fadeIn() | |
| ) { | |
| Button(onClick = {}) { | |
| Text("B1") | |
| } | |
| } | |
| AnimatedVisibility( | |
| visible = !isInteracting.value, | |
| modifier = Modifier.align(Alignment.CenterEnd), | |
| exit = fadeOut(), | |
| enter = fadeIn() | |
| ) { | |
| Button(onClick = {}) { | |
| Text("B2") | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package net.helw.playground.seekbar | |
| import androidx.compose.foundation.Canvas | |
| import androidx.compose.foundation.gestures.awaitEachGesture | |
| import androidx.compose.foundation.gestures.awaitFirstDown | |
| import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation | |
| import androidx.compose.foundation.gestures.drag | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.foundation.layout.height | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.mutableFloatStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.runtime.rememberUpdatedState | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.geometry.CornerRadius | |
| import androidx.compose.ui.geometry.Offset | |
| import androidx.compose.ui.geometry.Size | |
| import androidx.compose.ui.graphics.Color | |
| import androidx.compose.ui.graphics.StrokeCap | |
| import androidx.compose.ui.input.pointer.pointerInput | |
| import androidx.compose.ui.layout.onSizeChanged | |
| import androidx.compose.ui.platform.LocalDensity | |
| import androidx.compose.ui.unit.Dp | |
| import androidx.compose.ui.unit.dp | |
| import androidx.compose.ui.util.lerp | |
| import kotlin.math.roundToInt | |
| @Composable | |
| fun ManualSlider( | |
| currentPage: Int, | |
| isInteracting: Boolean, | |
| horizontalInset: Dp, | |
| onSetInteracting: (Boolean) -> Unit, | |
| onSetCurrentPage: (Int) -> Unit, | |
| modifier: Modifier = Modifier | |
| ) { | |
| val thumbWidth = 40.dp | |
| val onSetInteracting by rememberUpdatedState(onSetInteracting) | |
| val onSetCurrentPage by rememberUpdatedState(onSetCurrentPage) | |
| val widthPx = remember { mutableFloatStateOf(0f) } | |
| val buttonInsetPx = with(LocalDensity.current) { horizontalInset.toPx() } | |
| val latestCurrentPage by rememberUpdatedState(currentPage) | |
| val thumbHalfPx = with(LocalDensity.current) { 20.dp.toPx() } | |
| fun pageFromX(x: Float): Int { | |
| val start = 0f + thumbHalfPx | |
| val end = widthPx.floatValue - thumbHalfPx | |
| val range = (end - start).takeIf { it > 0f } ?: return latestCurrentPage | |
| val fraction = ((x - start) / range).coerceIn(0f, 1f) | |
| return (1f + fraction * (604f - 1f)) | |
| .roundToInt() | |
| .coerceIn(1, 604) | |
| } | |
| Box( | |
| modifier | |
| .fillMaxWidth() | |
| .onSizeChanged { widthPx.floatValue = it.width.toFloat() } | |
| .pointerInput(Unit) { | |
| awaitEachGesture { | |
| val down = awaitFirstDown(requireUnconsumed = false) | |
| onSetInteracting(true) | |
| try { | |
| val dragStart = awaitTouchSlopOrCancellation(down.id) { change, _ -> | |
| change.consume() | |
| onSetCurrentPage(pageFromX(change.position.x)) | |
| } | |
| if (dragStart != null) { | |
| drag(dragStart.id) { change -> | |
| change.consume() | |
| onSetCurrentPage(pageFromX(change.position.x)) | |
| } | |
| } | |
| } finally { | |
| onSetInteracting(false) | |
| } | |
| } | |
| } | |
| ) { | |
| Canvas(Modifier.fillMaxWidth().height(56.dp)) { | |
| val thumbWidth = thumbWidth.toPx() | |
| val thumbHeight = 20.dp.toPx() | |
| val trackHeight = 20.dp.toPx() | |
| val thumbHalfWidth = thumbWidth / 2f | |
| val visualStart = (if (isInteracting) 0f else buttonInsetPx) + thumbHalfWidth | |
| val visualEnd = (if (isInteracting) size.width else size.width - buttonInsetPx) - thumbHalfWidth | |
| val fraction = ((currentPage - 1f) / (604f - 1f)).coerceIn(0f, 1f) | |
| val thumbX = lerp(visualStart, visualEnd, fraction) | |
| val centerY = size.height / 2f | |
| drawLine( | |
| color = Color.LightGray, | |
| start = Offset(visualStart, centerY), | |
| end = Offset(visualEnd, centerY), | |
| strokeWidth = trackHeight, | |
| cap = StrokeCap.Round, | |
| ) | |
| drawLine( | |
| color = Color.Blue, | |
| start = Offset(visualStart, centerY), | |
| end = Offset(thumbX, centerY), | |
| strokeWidth = trackHeight, | |
| cap = StrokeCap.Round, | |
| ) | |
| drawRoundRect( | |
| color = Color.Gray, | |
| topLeft = Offset( | |
| x = thumbX - thumbWidth / 2f, | |
| y = centerY - thumbHeight / 2f, | |
| ), | |
| size = Size(thumbWidth, thumbHeight), | |
| cornerRadius = CornerRadius(thumbHeight / 2f, thumbHeight / 2f), | |
| ) | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package net.helw.playground.seekbar | |
| import androidx.compose.animation.AnimatedVisibility | |
| import androidx.compose.animation.fadeIn | |
| import androidx.compose.animation.fadeOut | |
| import androidx.compose.foundation.gestures.awaitEachGesture | |
| import androidx.compose.foundation.gestures.awaitFirstDown | |
| import androidx.compose.foundation.layout.Row | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.material3.Button | |
| import androidx.compose.material3.Slider | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.input.pointer.pointerInput | |
| import kotlin.math.roundToInt | |
| @Composable | |
| fun RowBottomBar( | |
| currentPage: Int, | |
| onSetCurrentPage: (Int) -> Unit, | |
| modifier: Modifier = Modifier | |
| ) { | |
| val isInteracting = remember { mutableStateOf(false) } | |
| Row(modifier = modifier.fillMaxWidth()) { | |
| AnimatedVisibility( | |
| visible = !isInteracting.value, | |
| exit = fadeOut(), | |
| enter = fadeIn() | |
| ) { | |
| Button(onClick = {}) { | |
| Text("B1") | |
| } | |
| } | |
| Slider( | |
| value = currentPage.toFloat(), | |
| onValueChange = { onSetCurrentPage(it.roundToInt()) }, | |
| valueRange = 1f.rangeTo(604f), | |
| onValueChangeFinished = { isInteracting.value = false }, | |
| modifier = Modifier.pointerInput(Unit) { | |
| awaitEachGesture { | |
| awaitFirstDown(requireUnconsumed = false) | |
| isInteracting.value = true | |
| } | |
| } | |
| .weight(1f) | |
| ) | |
| AnimatedVisibility( | |
| visible = !isInteracting.value, | |
| exit = fadeOut(), | |
| enter = fadeIn() | |
| ) { | |
| Button(onClick = {}) { | |
| Text("B2") | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment