Chroma Slider

Chroma Slider

Subscribe for Live Previews, in your browser

$3 / month

Code

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color.Companion.Black
import androidx.compose.ui.graphics.Color.Companion.White
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import theme.Colors.Slate300
import kotlin.math.roundToInt


@Composable
fun ChromaSliderImpl() {
    var x by remember { mutableStateOf(50f) }
    ChromaSlider(
        value = x,
        onValueChange = { x = it },
        modifier = Modifier
            .padding(horizontal = 32.dp)
            .width(600.dp),
        displayValue = { "${x.roundToInt()}" },
        valueRange = 0f..100f,
        thumbHeight = 56.dp,
    )
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChromaSlider(
    value: Float,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier,
    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
    displayValue: (Float) -> String = { "" },
    steps: Int = 0,
    thumbHeight: Dp = 84.dp,
    thumbWidth: Dp = thumbHeight,
) {
    val animatedValue by animateFloatAsState(targetValue = value)
    Slider(
        value = animatedValue,
        onValueChange = onValueChange,
        modifier = modifier.padding(4.dp),
        valueRange = valueRange,
        steps = steps,
        thumb = {
            Box(
                Modifier
                    .pointerHoverIcon(PointerIcon.Hand)
                    .width(thumbWidth)
                    .height(thumbHeight)
                    .padding(8.dp)
                    .shadow(
                        elevation = 10.dp,
                        shape = CircleShape,
                    )
                    .background(
                        White,
                        shape = CircleShape,
                    )
                    .padding(4.dp)
            ) {
                BasicText(
                    text = displayValue(animatedValue),
                    modifier = Modifier.align(Alignment.Center),
                    style = MaterialTheme.typography.labelMedium,
                    color = { Black },
                    autoSize = TextAutoSize.StepBased(minFontSize = 2.sp),
                    maxLines = 1
                )
            }
        },
        track = { sliderState ->
            val fraction by remember {
                derivedStateOf {
                    (sliderState.value - sliderState.valueRange.start) / (sliderState.valueRange.endInclusive - sliderState.valueRange.start)
                }
            }
            BoxWithConstraints(
                Modifier
                    .fillMaxWidth()
                    .height(thumbHeight)
            ) {
                Box(
                    Modifier
                        .requiredWidth(maxWidth + thumbWidth)
                        .height(thumbHeight)
                        .drawBehind {
                            val center = fraction * (maxWidth + thumbWidth).toPx()
                            val radius = 200f
                            drawRoundRect(
                                brush = Brush.horizontalGradient(
                                    colors = listOf(
                                        White.copy(alpha = .1f),
                                        White.copy(alpha = .7f),
                                        White.copy(alpha = .1f),
                                    ),
                                    startX = center - radius,
                                    endX = center + radius
                                ),
                                cornerRadius = CornerRadius(size.height, size.height),
                                style = Stroke(
                                    width = 1.dp.toPx()
                                )
                            )
                        }
                        .padding(4.dp)
                        .background(
                            Slate300.copy(alpha = .05f),
                            CircleShape
                        )
                )
            }
        }
    )
}
Mastodon