Created
March 7, 2024 21:13
-
-
Save burntcookie90/be719394fa38df8a0f0741b882f13a96 to your computer and use it in GitHub Desktop.
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
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 <T> SwipeableItemRow( | |
modifier: Modifier, | |
item: T, | |
swiped: Boolean, | |
onSwipe: (T, Boolean) -> Unit, | |
onClick: () -> Unit, | |
actionButtons: List<ActionButtonModel>, | |
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), | |
) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment