Search code examples
android-jetpack-composetouch-event

How to redirect touch event between composables in compose?


I am trying to make a composable that is zoomable and pannable (at certain conditions) and can be scrolled in LazyList. I attempt to utilize pointerInteropFilter. I am waiting for slop first to understand what should handle touch LazyList or transformation. However, pointerInteropFilter does not seem to do anything.

val MAX_ZOOM = 3f
val MIN_ZOOM = 1f
Box(Modifier.padding(innerPadding)) {
    LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
        items(10) {

            var targetScale by remember { mutableStateOf(MIN_ZOOM) }
            var targetOffset by remember { mutableStateOf(Offset.Zero) }
            var slopReached by remember { mutableStateOf(false) }

            val transformableState =
                rememberTransformableState { zoomChange, offsetChange, _ ->
                    // until slope detected do nothing
                    if (!slopReached) return@rememberTransformableState
                    targetScale *= zoomChange
                    targetOffset = Offset(
                        targetOffset.x + offsetChange.x,
                        targetOffset.y + offsetChange.y
                    )

                }
            var slop by remember { mutableStateOf(Offset.Zero) }

            Box(Modifier.pointerInteropFilter() {
                // 2 fingers indicates it is zoom - must be handled by transformableState
                if (it.pointerCount >= 2) return@pointerInteropFilter false
                // zoomed image must be handled by transformableState
                if (targetScale != MIN_ZOOM) return@pointerInteropFilter false
                when (it.actionMasked) {
                    MotionEvent.ACTION_MOVE -> {
                        if (slopReached) {
                            // for unzoomed view
                            // horizontal gesture should be returned to LazyList
                            // vertical gesture should be handled by transformableState
                            return@pointerInteropFilter slop.x.absoluteValue > slop.y.absoluteValue

                        } else {
                            slop = Offset(
                                slop.x + it.x,
                                slop.y + it.y
                            )
                            slopReached =
                                slop.x.absoluteValue > 20 || slop.y.absoluteValue > 20
                            return@pointerInteropFilter false

                        }
                    }
                }
                true

            }) {
                Box(Modifier.transformable(transformableState)) {

                    Box(
                        Modifier
                            .background(Color.Red)
                            .graphicsLayer {
                                this.scaleX = targetScale
                                this.scaleY = targetScale
                                this.translationX = targetOffset.x
                                this.translationY = targetOffset.y
                            })
                }
            }
        }
    }
}

Solution

  • To have a composable that zoom/pan/rotate inside LazyRow/Column or any scrollable you need to have a transformation gesture that consumes under specific conditions. You can write a transform gesture like in this answer and call consume based on pointer count.

    Or you can write a on touch event gesture which is less complicated like

    .pointerInput(Unit) {
        awaitEachGesture {
            // Wait for at least one pointer to press down
            awaitFirstDown()
            do {
    
                val event = awaitPointerEvent()
                // Calculate gestures and consume pointerInputChange
                var zoom = item.zoom.value
                zoom *= event.calculateZoom()
                // Limit zoom between 100% and 300%
                zoom = zoom.coerceIn(1f, 3f)
    
                item.zoom.value = zoom
    
                val pan = event.calculatePan()
    
                val currentOffset = if (zoom == 1f) {
                    Offset.Zero
                } else {
    
                    // This is for limiting pan inside Image bounds
                    val temp = item.offset.value + pan.times(zoom)
                    val maxX = (size.width * (zoom - 1) / 2f)
                    val maxY = (size.height * (zoom - 1) / 2f)
    
                    Offset(
                        temp.x.coerceIn(-maxX, maxX),
                        temp.y.coerceIn(-maxY, maxY)
                    )
                }
    
                item.offset.value = currentOffset
    
                // When image is zoomed consume event and prevent scrolling
                if (zoom > 1f) {
                    event.changes.forEach { pointerInputChange: PointerInputChange ->
                        pointerInputChange.consume()
                    }
    
                }
            } while (event.changes.any { it.pressed })
        }
    }
    

    In this snippet there is no slopPass check but you can implement your logic and call pointerInputChang.consume() based on your condition. It can be when zoomed, user touched more than 1 finger or slop as in you logic. Important thing is consuming prevents other scroll events getting this gesture.