Ring Slider

Ring Slider

Subscribe for Live Previews, in your browser

$3 / month

Code

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
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.width
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.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
import kotlin.math.roundToInt


val ringHeight = 56.dp

@Composable
fun RingSliderImpl(modifier: Modifier = Modifier) {

    var value by remember { mutableFloatStateOf(.4f) }
    RingSlider(
        value = value,
        onValueChange = { value = it },
        modifier = modifier.width(400.dp)
    )

}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RingSlider(
    value: Float,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier,
    color: Color = Color(0xFF8afff9),
    shadowColor: Color = MaterialTheme.colorScheme.background
) {

    val animatedValue by animateFloatAsState(targetValue = value)

    val interaction = remember { MutableInteractionSource() }
    val isDragging by interaction.collectIsDraggedAsState()
    var lastDelta by remember { mutableFloatStateOf(0f) }
    val animatedLastDelta by animateFloatAsState(
        targetValue = lastDelta,
        animationSpec = if (lastDelta.absoluteValue < .5f)
            spring(
                stiffness = 25f
            )
        else
            spring(),
        visibilityThreshold = .000001f
    )
    Slider(
        value = animatedValue,
        onValueChange = {
            if (isDragging) lastDelta = (it - value) * 10f
            onValueChange(it)
        },
        modifier = modifier,
        interactionSource = interaction,
        thumb = {},
        track = { sliderState ->

            val sliderFraction by remember {
                derivedStateOf {
                    (animatedValue - sliderState.valueRange.start) / (sliderState.valueRange.endInclusive - sliderState.valueRange.start)
                }
            }

            BoxWithConstraints {
                val scope = rememberCoroutineScope()
                val rings by remember {
                    derivedStateOf {
                        (maxWidth / (ringHeight * .33f)).roundToInt()
                    }
                }
                val ringAnimations = remember(rings) {
                    mutableListOf<Animatable<Float, AnimationVector1D>>().apply {
                        for (i in 0..rings) {
                            add(Animatable(0f))
                        }
                    }
                }

                Box(
                    Modifier
                        .fillMaxWidth()
                        .height(48.dp)
                        .drawBehind {

                            for (i in 0..rings) {
                                val fraction = i / rings.toFloat()
                                val x = (fraction * size.width)
                                if (x <= sliderFraction * size.width) {
                                    if (ringAnimations[i].targetValue != 1f) {
                                        scope.launch {
                                            ringAnimations[i].animateTo(
                                                1f,
                                                animationSpec = spring(
                                                    stiffness = Spring.StiffnessMediumLow,
                                                    dampingRatio = (1f - animatedLastDelta).coerceIn(
                                                        .2f,
                                                        .9f
                                                    ),
                                                    visibilityThreshold = .000001f
                                                ),
                                            )
                                        }
                                    }
                                } else {
                                    if (ringAnimations[i].targetValue != 0f) {
                                        scope.launch {
                                            ringAnimations[i].animateTo(
                                                0f,
                                                animationSpec = spring(
                                                    stiffness = Spring.StiffnessMedium,
                                                    visibilityThreshold = .000001f
                                                ),
                                            )
                                        }
                                    }
                                }


                                scale(
                                    scale = lerp(
                                        start = .2f,
                                        1f,
                                        t = ringAnimations[i].value.coerceAtLeast(0f)
                                    ),
                                    pivot = Offset(x = x, size.height / 2)
                                ) {
                                    translate(
                                        left = when (ringAnimations[i].value) {
                                            in 1f..2f -> {
                                                (ringAnimations[i].value - 1f).coerceAtLeast(0f) * 40.dp.toPx() * animatedLastDelta
                                            }

                                            else -> 0f
                                        }
                                    ) {
                                        val arcWidth = (ringHeight / 2).toPx()
                                        val arcHeight = ringHeight.toPx()
                                        drawArc(
                                            brush = Brush.horizontalGradient(
                                                colors = listOf(
                                                    color,
                                                    shadowColor,
                                                ),
                                                startX = x,
                                                endX = x + arcWidth
                                            ),
                                            startAngle = 0f,
                                            sweepAngle = 360f,
                                            useCenter = false,
                                            topLeft = Offset(
                                                x - (arcWidth / 2),
                                                (size.height / 2) - (arcHeight / 2)
                                            ),
                                            size = Size(
                                                height = arcHeight,
                                                width = arcWidth,
                                            ),
                                            style = Stroke(
                                                width = lerp(
                                                    start = 16.dp.toPx(),
                                                    end = 6.dp.toPx(),
                                                    t = ringAnimations[i].value.coerceAtLeast(0f)
                                                )
                                            )
                                        )
                                    }
                                }
                            }
                        }
                )
            }
        }
    )
}

private fun lerp(start: Float, end: Float, t: Float) = start + t * (end - start)
Mastodon