import androidx.annotation.DrawableRes import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight 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.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.rememberSwipeableState import androidx.compose.material.swipeable import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import kotlin.math.roundToInt @OptIn(ExperimentalMaterialApi::class) @Composable fun SwipeableItemRow( modifier: Modifier, item: T, swiped: Boolean, onSwipe: (T, Boolean) -> Unit, onClick: () -> Unit, actionButtons: List, itemRow: @Composable (Modifier) -> Unit, ) { val swipeableState = rememberSwipeableState( initialValue = false, confirmStateChange = { onSwipe(item, it) it }, ) LaunchedEffect(swiped) { swipeableState.animateTo(swiped) } BoxWithConstraints( modifier = modifier .wrapContentHeight(), ) { val (revealedRatio, swipedRatio) = actionButtons.size.let { require(it <= 3) { "Only 3 action buttons are supported" } when (it) { 1 -> 0.70f to 0.3f 2 -> 0.50f to 0.5f 3 -> 0.30f to 0.7f else -> error("Unhandled") } } val swipeWidth = constraints.maxWidth * swipedRatio val boxScope = this Box( modifier = Modifier .wrapContentHeight() .swipeable( state = swipeableState, anchors = mapOf(-swipeWidth to true, 0f to false), orientation = Orientation.Horizontal, ), ) { Row( modifier = Modifier .matchParentSize() .align(Alignment.CenterEnd) .padding(start = boxScope.maxWidth * revealedRatio), verticalAlignment = Alignment.CenterVertically, ) { actionButtons.forEach { ActionButton( modifier = Modifier .fillMaxHeight() .weight(1f), model = it ) } } val baseCornerRadius = 8.dp val fractionalCornerRadius = baseCornerRadius * swipeableState.progress.fraction val cornerCalculation = when { swipeableState.progress.to -> fractionalCornerRadius swipeableState.progress.from -> baseCornerRadius - fractionalCornerRadius else -> 0.dp } val baseElevation = 8.dp val fractionalElevation = baseElevation * swipeableState.progress.fraction val elevationCalculation = when { swipeableState.progress.to -> fractionalElevation swipeableState.progress.from -> baseElevation - fractionalElevation else -> 0.dp } val cornerRadius = animateDpAsState(targetValue = cornerCalculation, label = "rowCorner") val elevation = animateDpAsState(targetValue = elevationCalculation, label = "rowElevation") val shape = RoundedCornerShape(topEnd = cornerRadius.value, bottomEnd = cornerRadius.value) Column { itemRow( Modifier .offset { IntOffset( x = swipeableState.offset.value.roundToInt(), y = 0, ) } .clickable { onClick() } .clip(shape) .shadow( elevation = elevation.value, shape = shape, ), ) } } } } data class ActionButtonModel( val backgroundColor: Color, val onClick: () -> Unit, @DrawableRes val iconResId: Int, val iconTint: Color = Color.Unspecified, val text: String, ) @Composable fun ActionButton( modifier: Modifier = Modifier, model: ActionButtonModel ) { Button( modifier = modifier, colors = ButtonDefaults.buttonColors(backgroundColor = model.backgroundColor), shape = RectangleShape, onClick = { model.onClick() }, ) { Column( modifier = Modifier.fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Icon( modifier = Modifier.size(16.dp), painter = painterResource(id = model.iconResId), contentDescription = null, tint = model.iconTint, ) Spacer(modifier = Modifier.size(8.dp)) Text( text = model.text, style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Bold), ) } } }