Skip to content

Instantly share code, notes, and snippets.

@anitaa1990
Last active April 8, 2025 09:05
Show Gist options
  • Save anitaa1990/c2f17982d19d2b5835493596f7755860 to your computer and use it in GitHub Desktop.
Save anitaa1990/c2f17982d19d2b5835493596f7755860 to your computer and use it in GitHub Desktop.
/**
*
* This composable creates a custom, swipeable button that resembles a "slide to book" interaction.
* It is composed of two main parts:
* - The outer track (the full-width button background) which displays the label.
* - The inner slider thumb, which can be dragged from left to right.
*
*
* @param btnText Text to display on the outer button track (e.g., "Book Ride ₹199")
* @param btnTextStyle Text style for the button label (e.g., font weight, color)
* @param outerBtnBackgroundColor Background color for the full-width outer button
* @param sliderBtnBackgroundColor Background color for the draggable thumb button
* @param sliderBtnIcon Icon shown inside the slider thumb (e.g., car or arrow icon)
* @param onBtnSwipe Callback triggered once the user slides to complete the booking
*/
@Composable
fun SlideToBookButton(
btnText: String,
btnTextStyle: TextStyle,
outerBtnBackgroundColor: Color,
sliderBtnBackgroundColor: Color,
@DrawableRes sliderBtnIcon: Int,
onBtnSwipe: () -> Unit
) {
// Slider button width
val sliderButtonWidthDp = 70.dp
/**
* ----------------------------------------
* ✨ Step 1:
* • Convert slider button width into pixels so we can use it in math
* • Define a variable to compute the current horizontal position of the slider button (in pixels)
* • Define a variable to capture the total width of the outer button in pixels
* ----------------------------------------
*/
val density = LocalDensity.current
val sliderButtonWidthPx = with(density) { sliderButtonWidthDp.toPx() }
var sliderPositionPx by remember { mutableFloatStateOf(0f) }
var boxWidthPx by remember { mutableIntStateOf(0) }
/**
* ----------------------------------------
* ✨ Step 12: Add a flag that tells us when we need to show this loading indicator
* ----------------------------------------
*/
var showLoadingIndicator by remember { mutableStateOf(false) }
/**
* ----------------------------------------
* ✨ Step 5: Calculate drag progress percentage (0f to 1f)
* ----------------------------------------
*/
val dragProgress = remember(sliderPositionPx, boxWidthPx) {
if (boxWidthPx > 0) {
(sliderPositionPx / (boxWidthPx - sliderButtonWidthPx)).coerceIn(0f, 1f)
} else {
0f
}
}
/**
* ----------------------------------------
* ✨ Step 6: Alpha value for the button label — 1 when untouched, fades to 0 as drag progresses
* ----------------------------------------
*/
val textAlpha = 1f - dragProgress
/**
* ----------------------------------------
* ✨ Step 8:
* • Add a flag to indicate the slide is complete.
* • Animate the shrinking of the outer button i.e. scale the outer button.
* • Animate the fading of the slider button i.e. make it disappear.
* ----------------------------------------
*/
var sliderComplete by remember { mutableStateOf(false) }
val trackScale by animateFloatAsState(
targetValue = if (sliderComplete) 0f else 1f,
animationSpec = tween(durationMillis = 300), label = "trackScale"
)
val sliderAlpha by animateFloatAsState(
targetValue = if (sliderComplete) 0f else 1f,
animationSpec = tween(durationMillis = 300), label = "sliderAlpha"
)
/**
* ----------------------------------------
* ✨ Step 9: Mark slide as complete once drag passes 80%.
* This calls the onBtnSwipe method so users can perform whatever action is needed for their app.
* ----------------------------------------
*/
LaunchedEffect(dragProgress) {
if (dragProgress >= 0.8f && !sliderComplete) {
sliderComplete = true
showLoading = true // ✨ Step 13: Update flag when slider is completed
onBtnSwipe()
}
}
// The root layout for the button — stretches full width and has fixed height
Box(
modifier = Modifier
.fillMaxWidth()
.height(55.dp)
/**
* ----------------------------------------
* ✨ Step 2: Capture the full width of the button once it's laid out
* ----------------------------------------
*/
.onSizeChanged { size ->
boxWidthPx = size.width
}
) {
// Outer track — acts as the base of the button
Box(
modifier = Modifier
.matchParentSize()
/**
* ----------------------------------------
* ✨ Step 10: Animate scaling/shrinking of the button
* ----------------------------------------
*/
.graphicsLayer(scaleX = trackScale, scaleY = 1f)
.background(
color = outerBtnBackgroundColor,
shape = RoundedCornerShape(12.dp)
)
) {
// The center-aligned button label
Text(
text = btnText,
style = btnTextStyle,
modifier = Modifier.align(Alignment.Center)
/**
* ----------------------------------------
* ✨ Step 7: Apply the dynamic transparency to the label
* ----------------------------------------
*/
.alpha(textAlpha)
)
}
// Slider thumb container, positioned at the left edge of the button
Row(
modifier = Modifier
.align(Alignment.CenterStart)
.padding(1.dp)
/**
* ----------------------------------------
* ✨ Step 3: Shift the slider button based on drag position (px to dp conversion)
* ----------------------------------------
*/
.offset(x = with(density) { sliderPositionPx.toDp() })
/**
* ----------------------------------------
* ✨ Step 4: Handle horizontal drag gestures
* ----------------------------------------
*/
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
// Calculate new potential position
val newPosition = sliderPositionPx + delta
// Clamp it within 0 to (totalWidth - slider button width)
val maxPosition = boxWidthPx - sliderButtonWidthPx
sliderPositionPx = newPosition.coerceIn(0f, maxPosition)
},
onDragStarted = { /* Optional: add feedback or animation here */ },
onDragStopped = {
// TODO: In next step, we’ll trigger onBtnSwipe if drag passes threshold
}
),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
/**
* ----------------------------------------
* ✨ Step 11: Animate fade out of the slider button
* ----------------------------------------
*/
.alpha(sliderAlpha)
.graphicsLayer { alpha = sliderAlpha }
) {
// The draggable thumb itself
SliderButton(
sliderBtnWidth = sliderButtonWidthDp,
sliderBtnBackgroundColor = sliderBtnBackgroundColor,
sliderBtnIcon = sliderBtnIcon
)
}
}
/**
* ----------------------------------------
* ✨ Step 14: Show the loading indicator after the slider reaches the end and the animation completes
* ----------------------------------------
*/
if (showLoading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = Color.Black
)
}
}
}
/**
*
* This composable defines the visual appearance of the slider thumb — a small rounded box
* that contains an icon (usually a car or arrow). It is positioned inside the larger
* SlideToBookButton and will later be made draggable.
*
* @param sliderBtnBackgroundColor Background color for the thumb (distinct from the track)
* @param sliderBtnIcon Icon displayed at the center of the thumb button
*/
@Composable
private fun SliderButton(
sliderBtnWidth: Dp, // Width of the button
sliderBtnBackgroundColor: Color, // Background color for the thumb
@DrawableRes sliderBtnIcon: Int // Icon shown inside the thumb
) {
// Root Box for the slider thumb
Box(
modifier = Modifier
.wrapContentSize()
.width(70.dp)
.height(54.dp)
.background(
color = sliderBtnBackgroundColor,
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier
.padding(start = 10.dp, end = 10.dp)
.align(Alignment.Center),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(sliderBtnIcon),
contentDescription = "Car Icon",
modifier = Modifier.size(36.dp)
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment