Created
January 15, 2024 17:16
-
-
Save KlassenKonstantin/3b3be43ebc6977022f421daa82104f3e to your computer and use it in GitHub Desktop.
Rubber Band Slider Compose
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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