Skip to content

Instantly share code, notes, and snippets.

@oleggreen
Created March 18, 2024 18:02
Show Gist options
  • Save oleggreen/cd61f15db25a698d7e70c7d88727357d to your computer and use it in GitHub Desktop.
Save oleggreen/cd61f15db25a698d7e70c7d88727357d to your computer and use it in GitHub Desktop.
Swipable button example in Jetpack Compose
dependencies {
...
implementation(platform("androidx.compose:compose-bom:2024.01.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material:material")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
...
}
package com.oleg.green.swipe_button
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
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.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight
import androidx.compose.material.icons.outlined.Done
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlin.math.roundToInt
private val swipingBoxSize = 41.dp
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SwipeButton(
modifier: Modifier = Modifier,
onSwiped: () -> Unit
) {
Surface(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
.heightIn(min = 60.dp),
color = MaterialTheme.colors.primary,
contentColor = Color.White,
shape = MaterialTheme.shapes.small
) {
var swipeCompleted by remember { mutableStateOf(false) }
var containerWidth by remember { mutableStateOf(Int.MAX_VALUE) }
Box(
modifier = Modifier
.padding(10.dp)
.onGloballyPositioned {
containerWidth = it.size.width
},
contentAlignment = Alignment.Center
) {
val icon = if (swipeCompleted) {
Icons.Outlined.Done
} else {
Icons.AutoMirrored.Outlined.KeyboardArrowRight
}
val swipingBoxPx = with(LocalDensity.current) { swipingBoxSize.toPx() }
val maxSwipeDistance = containerWidth - swipingBoxPx
val context = LocalContext.current
LaunchedEffect(swipeCompleted) {
if (swipeCompleted) {
//maybe add vibration here
delay(1000)
onSwiped()
}
}
val state = remember {
AnchoredDraggableState(
initialValue = DragAnchors.Start,
positionalThreshold = { distance: Float -> distance * 0.99f },
velocityThreshold = { Float.MAX_VALUE },
animationSpec = tween(),
).apply {
updateAnchors(
DraggableAnchors {
DragAnchors.Start at 0f
DragAnchors.End at maxSwipeDistance
}
)
}
}
LaunchedEffect(maxSwipeDistance) {
state.updateAnchors(
DraggableAnchors {
DragAnchors.Start at 0f
DragAnchors.End at maxSwipeDistance
}
)
}
val dragAnchorOffset = IntOffset(
x = state.requireOffset().roundToInt(),
y = 0
)
if (state.requireOffset() >= maxSwipeDistance) {
swipeCompleted = true
}
Box(
modifier = Modifier
.align(Alignment.CenterStart)
.offset { dragAnchorOffset }
.anchoredDraggable(
state = state,
enabled = !swipeCompleted,
orientation = Orientation.Horizontal
)
.size(swipingBoxSize)
.background(color = Color.Gray),
) {
Crossfade(
modifier = Modifier.align(Alignment.Center),
label = "animate icon transition",
targetState = icon
) { targetState ->
Icon(
imageVector = targetState,
tint = Color.White,
contentDescription = null
)
}
}
Text(
modifier = Modifier.offset { dragAnchorOffset },
text = "Finish",
fontSize = 18.sp,
fontWeight = FontWeight.W400,
)
AnimatedVisibility(swipeCompleted) {
Text(
text = "Success",
fontSize = 18.sp,
fontWeight = FontWeight.W400,
)
}
}
}
}
enum class DragAnchors {
Start,
End,
}
@Preview(
showBackground = true, heightDp = 650, showSystemUi = false,
uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Normal"
)
@Composable
private fun SwipeButtonPreview() {
SwipeButton(
onSwiped = {},
)
}
@oleggreen
Copy link
Author

swipe_button

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment