Created a swipeToDelete modifier with `draggable` ...
# compose
b
Created a swipeToDelete modifier with
draggable
:)
👍 9
👍🏼 8
m
Wow, great work! I love how simple it is! @calintat is just in progress of making something similar available in our codebase: https://android-review.googlesource.com/c/platform/frameworks/support/+/1352507 Any feedback about it or about
draggable
in general is highly appreciated :)
b
Thanks! 🙂
oh slick! I'll look this over. draggable was pretty easy to use actually.
👍 1
Animation back to default position is the only real thing that would be a nice hook to have a callback for
but i can handle that animation back from offsetPosition -> 0 locally i think
h
Have you looked at FlingConfig? That might help to have a more fluid transition after swipe is completed or canceled.
b
interesting I haven't but will now
t
Here my swipeToRemove with callback and smooth remove animation:
Copy code
@Composable
fun Modifier.swipeToRemove(
    orientation: Orientation = Orientation.Horizontal,
    onRemoved: () -> Unit,
    enabled: Boolean = true
) = composed {
    val positionOffset = animatedFloat(0f)
    val animatedCollapseSize = animatedValue(initVal = 0, converter = IntToVectorConverter)
    var removed by state { false }
    var swipeSizeF by state { 0f }
    var collapseSize by state { 0 }

    Modifier.draggable(
        startDragImmediately = positionOffset.isRunning,
        orientation = orientation,
        onDragStopped = { velocity ->
            val config = FlingConfig(anchors = listOf(-swipeSizeF, 0f, swipeSizeF))
            positionOffset.fling(velocity, config) { _, endValue, _ ->
                //log("Fling end $endValue")
                if (endValue != 0f) {
                    animatedCollapseSize.snapTo(collapseSize)
                    animatedCollapseSize.animateTo(0, onEnd = { _, _ ->
                        //log("Collapsed")
                        removed = true
                        onRemoved()
                    }, anim = tween(500))
                }
            }
        },
        onDrag = { delta ->
            positionOffset.snapTo(positionOffset.value + delta)
        },
        enabled = enabled
    ) + object : LayoutModifier {
        override fun MeasureScope.measure(
            measurable: Measurable, constraints: Constraints,
            layoutDirection: LayoutDirection
        ): MeasureScope.MeasureResult {
            val childPlaceable = measurable.measure(constraints)
            val swipeSize = when (orientation) {
                Orientation.Horizontal -> {
                    collapseSize = childPlaceable.height
                    constraints.maxWidth
                }
                Orientation.Vertical -> {
                    collapseSize = childPlaceable.width
                    constraints.maxHeight
                }
            }
            swipeSizeF = swipeSize.toFloat()
            positionOffset.setBounds(-swipeSizeF, swipeSizeF)
            val lwidth = when (orientation) {
                Orientation.Horizontal -> childPlaceable.width
                Orientation.Vertical -> if (animatedCollapseSize.isRunning || removed) animatedCollapseSize.value else childPlaceable.width
            }
            val lheight = when (orientation) {
                Orientation.Vertical -> childPlaceable.height
                Orientation.Horizontal -> if (animatedCollapseSize.isRunning || removed) animatedCollapseSize.value else childPlaceable.height
            }
            return layout(lwidth, lheight) {
                if (orientation == Orientation.Vertical)
                    childPlaceable.place(0, positionOffset.value.roundToInt())
                else
                    childPlaceable.place(positionOffset.value.roundToInt(), 0)
            }
        }
    }
}
b
awesome!
t
Maybe instead of showing a dialog before deleting in my case you could show a snackbar with an undo function. So the user can undo the remove
b
Yep that's the plan @Timo Drick
@Timo Drick thanks for the snippet; found some useful stuff in there. https://gist.github.com/bmc08gt/f130c78580ccfb269795e74bcef96aa6
Add bidirectional support to mine with specifiying swipe direction
👍 1
c
Hey @brandonmcansh @Timo Drick! This is awesome! Just to let you know, I have added a new
Modifier.swipeable
for handling swiping between a set of anchors with built-in fling support. This is the original CL: https://android-review.googlesource.com/c/platform/frameworks/support/+/1362598, but it’s still a work in progress and more stuff will be added soon like resistance. Also, I have added a Material component SwipeToDismiss (https://android-review.googlesource.com/c/platform/frameworks/support/+/1352507). These will both be available in the next release of Compose, so if you’re interested in giving them a go, I would love to hear your feedback. I had a look at your swipeToDelete/swipeToRemove modifiers, and I think SwipeToDismiss should cover your use cases, but of course let me know if it’s missing anything.
❤️ 4
t
@calintat i looked into the code and for the SwipeToDismiss there is a callback: confirmStateChange: (DismissValue) -> Boolean = { true } Can we also use this to be informed when a dismiss happens? Because in my app i need to modify my data model when the user dismissed a entry.
And also i am not sure but maybe it should be an Modifier (In my first implementation i did it also as a composable but now i though it is maybe better to have a modifier.) But not sure about that
c
@Timo Drick The confirmStateChange callback will be invoked when the user lets go and the SwipeToDismiss will try to fling to a new state. If you return true, the SwipeToDismiss will animate to the new state. If you return false, it will animate to the previous state (i.e. Default). The best way to be notified when something is dismissed is to use the
DismissState
, and listen to changes to its value using
onCommit(dismissState.value)
I made it a Composable since it has a background slot
b
Awesome work @calintat looking forward to trying it out once it hits a dev release :)
❤️ 1