Code
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
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.fillMaxHeight
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.foundation.shape.RoundedCornerShape
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.draw.shadow
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.TileMode
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.Orange200
import theme.Orange400
import theme.Orange500
import theme.Orange600
import theme.Orange700
import theme.Orange800
import theme.Orange900
import theme.Orange950
import theme.Red200
import theme.Red300
import theme.Red400
import theme.Transparent
import theme.Yellow300
import theme.Yellow400
import theme.Zinc400
import theme.Zinc900
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
@Composable
fun StripeSliderImpl(modifier: Modifier = Modifier) {
var value by remember { mutableFloatStateOf(.5f) }
StripeSlider(
value = value,
onValueChange = { value = it },
modifier = modifier.width(400.dp),
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StripeSlider(
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 = {
Box(
Modifier.height(48.dp),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.width(12.dp)
.fillMaxHeight(.5f)
.border(
width = 1.dp,
color = Orange600,
CircleShape
)
.shadow(elevation = 10.dp, shape = CircleShape)
.background(
color = Orange900,
CircleShape
)
)
}
},
track = { sliderPositions ->
val progress by remember {
derivedStateOf {
sliderPositions.value / sliderPositions.valueRange.endInclusive
}
}
BoxWithConstraints(Modifier.fillMaxWidth()) {
Box(
Modifier.requiredWidth(maxWidth + 24.dp)
) {
Box(
Modifier
.fillMaxWidth()
.height(48.dp)
.background(
color = Orange600.copy(alpha = .02f),
shape = RoundedCornerShape(12.dp),
)
.border(
width = Dp.Hairline,
shape = RoundedCornerShape(12.dp),
brush = Brush.horizontalGradient(
colors = listOf(
Transparent,
Orange500.copy(alpha = .4f),
)
)
)
)
val phase by rememberInfiniteTransition().animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
)
)
Box(
Modifier
.width((this@BoxWithConstraints.maxWidth * (progress)) + 24.dp)
.height(48.dp)
.border(
width = 1.dp,
color = Orange600,
shape = RoundedCornerShape(12.dp),
)
.background(
brush = Brush.stripes(
Orange900.copy(alpha = .3f) to 1f,
Orange800.copy(alpha = .3f) to 1f,
width = 50f,
angle = 30f,
phase = phase,
),
shape = RoundedCornerShape(12.dp),
)
.background(
brush = Brush.verticalGradient(
0f to Orange900.copy(alpha = .4f),
.3f to Transparent,
.7f to Transparent,
1f to Orange900.copy(alpha = .4f),
),
shape = RoundedCornerShape(12.dp),
)
)
}
}
}
)
}
fun Brush.Companion.stripes(
vararg stripes: Pair<Color, Float>,
width: Float = 20f,
angle: Float = 45f,
phase: Float = 0f,
): Brush {
val totalWeight = stripes.sumOf {
it.second.toDouble()
}.toFloat()
val colorStops = mutableListOf<Pair<Float, Color>>()
var currentPosition = 0f
stripes.forEach { (color, weight) ->
val proportion = weight / totalWeight
colorStops.add(currentPosition to color)
currentPosition += proportion
colorStops.add(currentPosition to color)
}
val angleInRadians = angle * (PI / 180)
val endX = (width * cos(angleInRadians)).toFloat()
val endY = (width * sin(angleInRadians)).toFloat()
val phaseOffsetX = endX * phase
val phaseOffsetY = endY * phase
return linearGradient(
colorStops = colorStops.toTypedArray(),
start = Offset(-phaseOffsetX, -phaseOffsetY),
end = Offset(endX - phaseOffsetX, endY - phaseOffsetY),
tileMode = TileMode.Repeated,
)
}