Skip to content

Instantly share code, notes, and snippets.

@RhymezxCode
Forked from KlassenKonstantin/RubberBandSlider.kt
Created April 25, 2024 08:07
Show Gist options
  • Save RhymezxCode/dea68b506997cf1615c359fd2e314891 to your computer and use it in GitHub Desktop.
Save RhymezxCode/dea68b506997cf1615c359fd2e314891 to your computer and use it in GitHub Desktop.
Rubber Band Slider Compose
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
setContent {
RubberBandSliderTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Box(contentAlignment = Alignment.Center) {
RubberBandSlider(modifier = Modifier
.height(200.dp)
.width(100.dp))
}
}
}
}
}
}
@Composable
fun RubberBandSlider(
modifier: Modifier
) {
/** y value of the pointer. Excessive drag amount beyond the slider's bounds is added to overdrag **/
var offsetY by remember { mutableFloatStateOf(0f) }
/** Height of the slider **/
var height by remember { mutableFloatStateOf(0f) }
/** Slider progress between 0f and 1f **/
val progress = remember { Animatable(0.5f) }
/** Excessive drag amount beyond the slider's bounds **/
val overdrag = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
fun updateProgress(height: Float, offsetY: Float) {
scope.launch {
progress.animateTo(1f - offsetY / height)
}
}
// Background
Box(
modifier = modifier
.onGloballyPositioned {
height = it.size.height.toFloat()
}
// Pointer DOWN
.pointerInput(Unit) {
detectTapGestures(onPress = {
offsetY = it.y
updateProgress(height, offsetY)
})
}
// Pointer MOVE
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState {
offsetY += it
updateProgress(height, offsetY)
when {
offsetY <= 0f -> offsetY
offsetY > height -> offsetY - height
else -> 0f
}.let {
scope.launch {
overdrag.animateTo(it)
}
}
},
// Pointer UP
onDragStopped = {
scope.launch {
overdrag.animateTo(0f)
}
}
)
.graphicsLayer {
// Default is (0.5f, 0.5f) which would scale the shape around the center, uniformly in all directions. We want the slider to expand
// in direction of the drag
transformOrigin = TransformOrigin(0.5f, if (overdrag.value < 0) 1f else 0f)
// Slow down the overdrag and take the absolute value because we want the same stretch in both directions
val adjustedOverdrag = 1f - height / (height - sqrt(overdrag.value.absoluteValue) * 2f)
// Stretch in y direction
scaleY = 1f - adjustedOverdrag
// Shrink in x direction
scaleX = 1f + adjustedOverdrag * 1.5f
// Move the slider slightly in drag direction
translationY = sqrt(overdrag.value.absoluteValue * 5f) * if (overdrag.value < 0) -1f else 1f
shape = RoundedCornerShape(24.dp)
clip = true
}
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.BottomStart
) {
// Foreground
Box(modifier = Modifier
.fillMaxHeight(progress.value)
.fillMaxWidth(1f)
.background(MaterialTheme.colorScheme.primary)
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment