usuiat / Zoomable

Jetpack Compose library that enables contents zooming with pinch gesture.
https://usuiat.github.io/Zoomable/
Apache License 2.0
414 stars 21 forks source link

Question about Feasibility: Implementing Swipe-to-Dismiss Gesture for Full-Screen Image Viewer #86

Closed leinardi closed 1 year ago

leinardi commented 1 year ago

First and foremost, I want to express my gratitude for the wonderful library you have created. The feature that allows zooming directly to the pinch location instead of the center of the image is something I have been seeking for a long time, and I truly appreciate it.

I have a question regarding the feasibility of a particular enhancement: I am utilizing your library to develop a full-screen image viewer for an application, and I am interested in incorporating the ability to dismiss the image by swiping it down when it is not zoomed. Essentially, I would like to replicate the behavior seen in Google Photos.

My image viewer will be a dedicated compose destination, which may limit the extent to which I can implement intricate animations like those in Google Photos, where the image shrinks and reposition itself on top of the grid. However, I would be content with a swipe-down animation causing the image to exit the screen and subsequently triggering a "navigate back" action on the NavHostController. My main concern is whether implementing this swipe gesture might interfere with the library's existing gestures. It's important to note that I only require this gesture to be active when the image is not zoomed, so there should be no conflict with the library's gestures at that particular moment.

Do you think that could be easily implemented or does it sound like something complex to build on top of this library?

usuiat commented 1 year ago

@leinardi Thank you for using Zoomable.

You can place Modifier.pointerInput() and/or other modifier functions BEFORE Modifier.zoomable() to detect swipe gestures. The order is important. Because gestures are handled in the order from the end of the modifier chain.

When the image is not zoomed, Modifier.zoomable() does not consume the one finger swipe gesture events. These events that are not consumed by zoomable propagates to the modifier functions that are placed before zoomable.

I tried to detect downward drag gesture and hide the image when the drag gesture ends by placing Modifier.pointerInput before zoomable. Of course, this is just a sample and the actual application code would be more complex. Feel free to ask questions if zoomable does not work as expected.

@Composable
fun Sample() {
    val painter = painterResource(id = R.drawable.penguin)
    val zoomState = rememberZoomState(
        contentSize = painter.intrinsicSize,
    )
    var scale by remember { mutableStateOf(1f) }
    var dragY by remember { mutableStateOf(0f) }
    Image(
        painter = painter,
        contentDescription = "Zoomable image",
        contentScale = ContentScale.Fit,
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                detectVerticalDragGestures(
                    onDragStart = {
                        if (zoomState.scale == 1f) {
                            scale = 0.8f
                        }
                    },
                    onVerticalDrag = { _, dragAmount ->
                        if (zoomState.scale == 1f) {
                            dragY = max(0f, dragY + dragAmount)
                        }
                    },
                    onDragEnd = {
                        if (dragY > 100f) {
                            // Navigation back can be implemented here.
                            // This is a sample so I just hide the image.
                            scale = 0f
                        } else {
                            dragY = 0f
                            scale = 1f
                        }
                    },
                    onDragCancel = {
                        dragY = 0f
                        scale = 1f
                    }
                )
            }
            .graphicsLayer {
                translationY = dragY
                scaleX = scale
                scaleY = scale
            }
            .zoomable(zoomState)
    )
}

https://github.com/usuiat/Zoomable/assets/17829294/dc1639c7-afe1-40e1-a35f-96cb25c4387e

leinardi commented 1 year ago

Hey thanks a lot for the answer! Yeah I also realized that it was necessary to add the modifier functions BEFORE Modifier.zoomable(), before doing that I was getting some strange behavior...

I managed to use the new AnchoredDraggable APIs to handle the gesture:


    val configuration = LocalConfiguration.current
    val anchors = remember {
        DraggableAnchors {
            -1 at -configuration.screenHeightDp.toPx
            0 at 0f
            1 at configuration.screenHeightDp.toPx
        }
    }
    val density = LocalDensity.current
    val swipeToDismissState = remember {
        AnchoredDraggableState(
            initialValue = 0,
            anchors = anchors,
            positionalThreshold = SwipeToRevealDefaults.FixedPositionalThreshold,
            velocityThreshold = { with(density) { 125.dp.toPx() } },
            animationSpec = tween(),
        )
    }
        val zoomState = rememberZoomState(maxScale = 10f)
 SubcomposeAsyncImage(
            model = ImageRequest.Builder(LocalContext.current)
                .data(url)
                .crossfade(true)
                .build(),
            onSuccess = { zoomState.setContentSize(it.painter.intrinsicSize) },
            contentDescription = null,
            modifier = Modifier
                .fillMaxSize()
                .anchoredDraggable(
                    state = swipeToDismissState,
                    orientation = Orientation.Vertical,
                    enabled = zoomState.scale == 1f,
                )
                .zoomable(
                    zoomState = zoomState,
                )
                .offset {
                    IntOffset(
                        0,
                        swipeToDismissState
                            .requireOffset()
                            .roundToInt(),
                    )
                },
        )
}
usuiat commented 1 year ago

I managed to use the new AnchoredDraggable APIs to handle the gesture:

It's cool 😎 In fact, I have not yet tried Zoomable with Compose v1.6.0, so please let me know if there are any problems.

leinardi commented 1 year ago

Sure! So far it works pretty well :+1: