Skip to content

Instantly share code, notes, and snippets.

@burntcookie90
Created March 7, 2024 21:13
Show Gist options
  • Select an option

  • Save burntcookie90/be719394fa38df8a0f0741b882f13a96 to your computer and use it in GitHub Desktop.

Select an option

Save burntcookie90/be719394fa38df8a0f0741b882f13a96 to your computer and use it in GitHub Desktop.

Revisions

  1. burntcookie90 created this gist Mar 7, 2024.
    181 changes: 181 additions & 0 deletions SwipeableItemRow.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,181 @@
    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),
    )
    }
    }
    }