Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Jetpack Compose Modifier extension to implement swipe-to-delete via Modifier.draggable
import androidx.animation.IntToVectorConverter
import androidx.animation.tween
import androidx.compose.Composable
import androidx.compose.mutableStateOf
import androidx.compose.remember
import androidx.ui.animation.animatedFloat
import androidx.ui.animation.animatedValue
import androidx.ui.core.*
import androidx.ui.core.gesture.scrollorientationlocking.Orientation
import androidx.ui.foundation.animation.FlingConfig
import androidx.ui.foundation.animation.fling
import androidx.ui.foundation.gestures.draggable
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
/**
* [Modifier] enabling swipe to delete functionality on a Composable view.
*
* NOTE: <b>MUST</b> be added prior to padding modifiers to ensure that the collapse animation
* will run to remove all available space.
*/
@Composable
fun Modifier.swipeToDelete(
constraints: Constraints,
threshold: Float = .8f,
swipeDirection: LayoutDirection? = null,
onDelete: () -> Unit
): Modifier {
val width = constraints.maxWidth
val draggable = remember { mutableStateOf(true) }
val deleted = remember { mutableStateOf(false) }
val positionOffset = animatedFloat(0f)
val collapse = remember { mutableStateOf(0) }
val animatedCollapse = animatedValue(initVal = 0, converter = IntToVectorConverter)
return this + Modifier.draggable(
enabled = draggable.value,
orientation = Orientation.Horizontal,
onDrag = { delta ->
when (swipeDirection) {
LayoutDirection.Ltr -> positionOffset.snapTo((positionOffset.value + delta).coerceAtLeast(0f))
LayoutDirection.Rtl -> positionOffset.snapTo((positionOffset.value + delta).coerceAtMost(0f))
else -> positionOffset.snapTo(positionOffset.value + delta)
}
},
onDragStopped = { velocity ->
val config = FlingConfig(anchors = listOf(-width.toFloat(), 0f, width.toFloat()))
if (positionOffset.value.absoluteValue >= threshold) {
positionOffset.fling(velocity, config) { _, endValue, _ ->
if (endValue != 0f) {
animatedCollapse.snapTo(collapse.value)
animatedCollapse.animateTo(0, onEnd = { _, _ ->
deleted.value = true
draggable.value = false
onDelete()
}, anim = tween(500))
}
}
} else {
draggable.value = true
}
}
) + object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
layoutDirection: LayoutDirection
): MeasureScope.MeasureResult {
val child = measurable.measure(constraints)
positionOffset.setBounds(-width.toFloat(), width.toFloat())
collapse.value = child.height
val placeHeight = if (animatedCollapse.isRunning || deleted.value) {
animatedCollapse.value
} else {
child.height
}
return layout(child.width, placeHeight) {
child.place(positionOffset.value.roundToInt(), 0)
}
}
}
}
@bmc08gt

This comment has been minimized.

Copy link
Owner Author

@bmc08gt bmc08gt commented Aug 2, 2020

Updated to support bidirectional swiping, animations back to initial position, and animating the cell collapse. Thanks to @timo for tips.

@ethanhsuhsu

This comment has been minimized.

Copy link

@ethanhsuhsu ethanhsuhsu commented Sep 16, 2020

@bmc08gt this example is amazing!

@anhtuan23

This comment has been minimized.

Copy link

@anhtuan23 anhtuan23 commented Sep 20, 2020

Hi, I'm trying to use this to implement swipe to delete in a LazyColumn item.
Where can I get the constraints for this modifier?
I tried to simply use Constraints but since in this case constraints.maxWidth is Constraints.Infinity, onDelete() is not trigger at all.
Setting a fix number using Constraints.fixedWidth() works but I want to make it fill the available width.

@bmc08gt

This comment has been minimized.

Copy link
Owner Author

@bmc08gt bmc08gt commented Sep 20, 2020

Hi, I'm trying to use this to implement swipe to delete in a LazyColumn item.
Where can I get the constraints for this modifier?
I tried to simply use Constraints but since in this case constraints.maxWidth is Constraints.Infinity, onDelete() is not trigger at all.
Setting a fix number using Constraints.fixedWidth() works but I want to make it fill the available width.

I would definitely try out https://gist.github.com/bmc08gt/fca95db3bf9fcf255d76f03ec10ea3f9. This gist was created before there was a first class modifier for this available and that one has better support.

@anhtuan23

This comment has been minimized.

Copy link

@anhtuan23 anhtuan23 commented Sep 20, 2020

Thank you. Using SwipeToDismiss is definitely easier.

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