Glow Slider

Glow Slider

Subscribe for Live Previews, in your browser

$3 / month

Code

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.withSaveLayer
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.util.lerp
import theme.Black
import theme.Blue300
import theme.Blue400
import theme.Green300
import theme.Green400
import theme.Red300
import theme.Red400
import theme.Transparent
import theme.Yellow300
import theme.Yellow400
import theme.Zinc900

@Composable
fun GlowSliderImpl(modifier: Modifier = Modifier) {
    var value by remember { mutableFloatStateOf(.5f) }
    GlowSlider(
        value = value,
        onValueChange = { value = it },
        modifier = modifier.width(400.dp),
    )
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GlowSlider(
    value: Float,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier,
    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
) {
    val interaction = remember { MutableInteractionSource() }
    val isDragged = interaction.collectIsDraggedAsState().value

    val animatedValue by animateFloatAsState(
        targetValue = value,
        animationSpec = if (isDragged) spring(stiffness = Spring.StiffnessHigh)
        else spring(dampingRatio = Spring.DampingRatioLowBouncy),
        visibilityThreshold = .0001f
    )

    Slider(
        value = animatedValue,
        onValueChange = onValueChange,
        modifier = modifier.pointerHoverIcon(PointerIcon.Hand),
        interactionSource = interaction,
        valueRange = valueRange,
        thumb = {
            val scale by animateFloatAsState(
                targetValue = if (isDragged) 1.1f else 1f,
                animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
            )
            Box(
                Modifier
                    .scale(scale)
                    .size(48.dp),
                contentAlignment = Alignment.Center
            ) {
                Text(
                    text = (it.value * 100).toInt().toString(),
                    style = MaterialTheme.typography.labelMedium
                )
            }
        },
        track = { sliderPositions ->
            val progress by remember {
                derivedStateOf {
                    sliderPositions.value / sliderPositions.valueRange.endInclusive
                }
            }
            BoxWithConstraints(
                Modifier.fillMaxWidth()
            ) {
                Box(
                    Modifier.requiredWidth(maxWidth + 48.dp)
                ) {
                    Box(
                        Modifier
                            .fillMaxWidth()
                            .height(48.dp)
                            .background(
                                color = Color.White.copy(alpha = .02f),
                                shape = CircleShape,
                            )
                            .border(
                                width = Dp.Hairline,
                                shape = CircleShape,
                                brush = Brush.horizontalGradient(
                                    colors = listOf(
                                        Color.Transparent,
                                        Color.White.copy(alpha = .3f),
                                    )
                                )
                            )
                    )
                    val fullCapacityAnimation by animateFloatAsState(
                        targetValue = if (progress >= 1f) 1f else 0f,
                        animationSpec = spring(stiffness = Spring.StiffnessLow)
                    )
                    Box(
                        Modifier
                            .defaultMinSize(minWidth = 48.dp)
                            .width((this@BoxWithConstraints.maxWidth * (progress)) + 48.dp)
                            .drawWithContent {
                                val rect = Rect(
                                    offset = Offset(-60.dp.toPx(), -60.dp.toPx()),
                                    size = Size(
                                        size.width + 120.dp.toPx(),
                                        120.dp.toPx() + size.height
                                    )
                                )
                                layer(
                                    bounds = rect
                                ) {
                                    this@drawWithContent.drawContent()
                                    drawRect(
                                        brush = Brush.verticalGradient(
                                            colors = listOf(
                                                Transparent,
                                                Black,
                                                Black,
                                                Black,
                                                Transparent,
                                            ),
                                            startY = rect.top,
                                            endY = rect.bottom,
                                        ),
                                        topLeft = rect.topLeft,
                                        size = rect.size,
                                        blendMode = BlendMode.DstIn
                                    )
                                }
                            }
                            .height(48.dp)
                            .border(
                                width = 1.dp,
                                brush = Brush.horizontalGradient(
                                    colors = listOf(
                                        Red300,
                                        Yellow300,
                                        Green300,
                                        Blue300,
                                    )
                                ),
                                shape = CircleShape,
                            )
                            .dropShadow(
                                shape = CircleShape,
                            ) {
                                brush = Brush.horizontalGradient(
                                    colors = listOf(
                                        Red300,
                                        Yellow300,
                                        Green300,
                                        Blue300,
                                    )
                                )
                                radius = 70f
                                alpha = .2f
                            }
                            .dropShadow(shape = CircleShape) {
                                brush = Brush.horizontalGradient(
                                    colors = listOf(
                                        Red400.copy(alpha = .2f),
                                        Yellow400.copy(alpha = .3f),
                                        Green400.copy(alpha = .6f),
                                        Blue400,
                                    )
                                )
                                radius = 50.dp.toPx()
                                alpha = lerp(.2f, .7f, fullCapacityAnimation)
                                spread = lerp(0f, 10.dp.toPx(), fullCapacityAnimation)
                            }
                            .background(
                                color = Zinc900,
                                shape = CircleShape,
                            )
                    )
                }
            }
        }
    )
}

private fun DrawScope.layer(
    bounds: Rect = size.toRect(),
    block: DrawScope.() -> Unit
) =
    drawIntoCanvas { canvas ->
        canvas.withSaveLayer(
            bounds = bounds,
            paint = Paint(),
        ) { block() }
    }
Mastodon