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.