0

I'm working on implementing a composable that allows for zooming and panning around of its content, while also making the content clickable.

While I've been able to implement both functionalities separately, I'm running into issues when trying to implement both zoomable and clickable composable at the same time.

Im using detectTransformGestures() method inside pointerInput Modifier to detect the zoom/pan gestures.

When the content is clickable, the zoom functionality doesn't work as expected. After some investigation, it seems that the detectTransformGestures function requires "un-consumed" touch events to process. However, making the composable clickable causes it to consume the touch events whenever it's clicked.

ZoomableContent:

@Composable
internal fun ZoomableContent(
    modifier: Modifier = Modifier,
    defaultState: ZoomPanState = remember {
        ZoomPanState(scale = 1f, translationX = 0f, translationY = 0f)
    },
    content: @Composable (zoomPanState: ZoomPanState) -> Unit,
) {

    val scale = remember { mutableStateOf(defaultState.scale) }
    val translateX = remember { mutableStateOf(defaultState.translationX) }
    val translateY = remember { mutableStateOf(defaultState.translationY) }

    Box(
        modifier = modifier
            .pointerInput(Unit) {
                detectTransformGestures { centroid, pan, zoom, _ ->

                    val scaleValue = (scale.value * zoom)
                        .coerceAtLeast(1f)
                        .coerceAtMost(3f)
                    scale.value = scaleValue

                    val modTranslateX = getScaledTranslation(
                        originalSize = this.size.width,
                        scaleFactor = scaleValue
                    )

                    val modTranslateY = getScaledTranslation(
                        originalSize = this.size.height,
                        scaleFactor = scaleValue
                    )

                    translateX.value = (translateX.value + pan.x)
                        .coerceAtLeast(-modTranslateX)
                        .coerceAtMost(modTranslateX)

                    translateY.value = (translateY.value + pan.y)
                        .coerceAtLeast(-modTranslateY)
                        .coerceAtMost(modTranslateY)
                }
            }
    ) {

        content(
            ZoomPanState(
                scale = scale.value,
                translationX = translateX.value,
                translationY = translateY.value,
            )
        )
    }
}

@Stable
private fun getScaledTranslation(
    originalSize: Int,
    scaleFactor: Float,
): Float {
    val scaledWidth = originalSize * scaleFactor
    val widthDiff = (scaledWidth - originalSize).absoluteValue
    return (widthDiff / 2)
}

@Immutable
internal data class ZoomPanState(
    val scale: Float,
    val translationX: Float,
    val translationY: Float,
)

Content that I'm trying to zoom:

@Composable
fun Grid(
    modifier: Modifier = Modifier,
) {

    Column(
        modifier = modifier,
    ) {

        repeat(4) { row ->
            Row(modifier = Modifier.wrapContentSize()) {
                repeat(4) { column ->
                    Cell(
                        modifier = Modifier
                            .size(60.dp)
                            .border(width = 1.dp, color = Color.Black),
                    )
                }
            }
        }
    }
}

@Composable
fun Cell(
    modifier: Modifier = Modifier,
) {

    val bgColor = remember { mutableStateOf(Color.LightGray) }

    Box(
        modifier = modifier
            .clickable { bgColor.value = Color.Black } // Whenever this composable is clickable, the content doesn’t zoom 
            .background(color = bgColor.value),
        contentAlignment = Alignment.Center,
    ) {

        Text(
            modifier = Modifier.wrapContentSize(),
            text = "Cell",
            textAlign = TextAlign.Center,
            fontSize = 12.sp,
        )
    }
}

This is how I'm using both of the composables:

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(color = Color.Gray),
    contentAlignment = Alignment.Center,
) { 

    ZoomableContent(
        modifier = Modifier.fillMaxSize(),
    ) { zoomPanState ->

        Grid(
            modifier = Modifier
                .wrapContentSize()
                .align(Alignment.Center)
                .graphicsLayer { 
                    translationX = zoomPanState.translationX
                    translationY = zoomPanState.translationY
                    scaleX = zoomPanState.scale
                    scaleY = zoomPanState.scale
                },
        )
    }
}                                                                                 

I'm looking for a solution that will allow me to achieve the desired behaviour of having a composable that can zoom its content while still making the content clickable. If you have any suggestions on how to achieve this, I'd greatly appreciate it.

  • 2
    Check [this answer](https://stackoverflow.com/questions/75955466/jetpack-compose-intercept-pinch-zoom-in-child-layout) out by changing PointerEventPass you can make transform gesture receive events first and you can restrict and allow click to receive gesture based on your preference to consume inside detect tranform – Thracian Apr 15 '23 at 11:56

0 Answers0