Skip to content

Instantly share code, notes, and snippets.

@emeowj
Last active April 18, 2024 04:46
Show Gist options
  • Save emeowj/2cfc56de0beace8b44127f300c6eb603 to your computer and use it in GitHub Desktop.
Save emeowj/2cfc56de0beace8b44127f300c6eb603 to your computer and use it in GitHub Desktop.
Custom Slider compoent using anchoredDraggable: https://xiaoming.dev/post/anchored-draggable-for-speed-and-fun
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import dev.xiaoming.compose.example.ExamplePreview
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
@OptIn(ExperimentalFoundationApi::class, FlowPreview::class)
@Composable
fun SpeedControl_setup(modifier: Modifier = Modifier) {
var speed by rememberSaveable {
mutableIntStateOf(10)
}
val valueRange = 8..30
val density = LocalDensity.current
val anchoredDraggableState = remember(density) {
AnchoredDraggableState(
initialValue = speed,
positionalThreshold = { it * 0.1f },
velocityThreshold = { with(density) { 100.dp.toPx() } },
animationSpec = tween()
)
}
val hapticFeedback = LocalHapticFeedback.current
LaunchedEffect(anchoredDraggableState) {
val speedFlow = snapshotFlow {
with(anchoredDraggableState) {
anchors.closestAnchor(offset)
}
}.filterNotNull()
launch {
speedFlow.collect { speed = it }
}
launch {
speedFlow.debounce(timeoutMillis = 100).collect {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
}
}
val color = LocalContentColor.current
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = speed.formatted, style = MaterialTheme.typography.displaySmall)
TriangleIndicator(modifier = Modifier.width(12.dp), color = color)
SpeedBars(
valueRange = valueRange,
anchoredDraggableState = anchoredDraggableState,
modifier = Modifier
.fillMaxWidth()
.height(96.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
) {
val coroutineScope = rememberCoroutineScope()
val speedPreset = listOf(8, 10, 16, 20, 30)
speedPreset.forEach { preset ->
Button(
onClick = {
coroutineScope.launch {
anchoredDraggableState.animateTo(targetValue = preset)
}
}
) {
Text(text = preset.formatted)
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SpeedBars(
valueRange: IntRange,
anchoredDraggableState: AnchoredDraggableState<Int>,
modifier: Modifier = Modifier,
color: Color = LocalContentColor.current
) {
val density = LocalDensity.current
val gap = with(density) { 48.dp.toPx() }
Box(
modifier = modifier
.onSizeChanged { size ->
anchoredDraggableState.updateAnchors(
DraggableAnchors {
valueRange.forEach { value ->
val offset = size.width / 2f - (value - valueRange.first) * gap
value at offset
}
}
)
}
.anchoredDraggable(
state = anchoredDraggableState,
orientation = Orientation.Horizontal
)
.drawWithCache {
val strokeWidth = 6.dp.toPx()
val offset = anchoredDraggableState.offset
val currentValue = with(anchoredDraggableState) {
currentValue + (targetValue - currentValue) * progress
}
onDrawBehind {
var start = offset
for (v in valueRange) {
val distance = (v - currentValue).absoluteValue.coerceAtMost(5f)
val scale = (1f - distance / 5f).coerceAtLeast(0.5f)
val height = size.height * 0.9f * scale
val startOffset = Offset(x = start, y = (size.height - height) / 2)
drawLine(
color = color.copy(alpha = scale),
start = startOffset,
end = startOffset.copy(y = startOffset.y + height),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)
start += gap
}
}
}
)
}
@Composable
private fun TriangleIndicator(
modifier: Modifier = Modifier,
color: Color = LocalContentColor.current
) {
Box(
modifier = modifier
.aspectRatio(ratio = 1.155f)
.drawWithCache {
val path = Path().apply {
moveTo(x = 0f, y = 0f)
lineTo(x = size.width, y = 0f)
lineTo(x = size.width / 2, y = size.height)
lineTo(x = 0f, y = 0f)
close()
}
onDrawBehind {
drawPath(path = path, color = color)
}
}
)
}
private inline val Int.formatted: String
get() = String.format(locale = null, format = "%.1fx", this / 10f)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment