In jetpack Compose default PointerEventPass is Main which as you can see in this answer, gestures propagate from descendant to ancestor while you want transform gesture to propagate from ancestor to descendant.
You need to use PointerEventPass.Initial for this. Using Final won't work when you touch the buttons because they will consume events. Now, Cyan background will allow pinch gestures before buttons consume them also you can consume events when number of pointers is bigger than 1 to not set click or long click as
// Consume touch when multiple fingers down
// This prevents click and long click if your finger touches a
// button while pinch gesture is being invoked
val size = changes.size
if (size>1){
changes.forEach { it.consume() }
}
Result

The code you should use for transform is
suspend fun PointerInputScope.detectTransformGestures(
panZoomLock: Boolean = false,
consume: Boolean = true,
pass: PointerEventPass = PointerEventPass.Main,
onGestureStart: (PointerInputChange) -> Unit = {},
onGesture: (
centroid: Offset,
pan: Offset,
zoom: Float,
rotation: Float,
mainPointer: PointerInputChange,
changes: List<PointerInputChange>
) -> Unit,
onGestureEnd: (PointerInputChange) -> Unit = {}
) {
awaitEachGesture {
var rotation = 0f
var zoom = 1f
var pan = Offset.Zero
var pastTouchSlop = false
val touchSlop = viewConfiguration.touchSlop
var lockedToPanZoom = false
// Wait for at least one pointer to press down, and set first contact position
val down: PointerInputChange = awaitFirstDown(
requireUnconsumed = false,
pass = pass
)
onGestureStart(down)
var pointer = down
// Main pointer is the one that is down initially
var pointerId = down.id
do {
val event = awaitPointerEvent(pass = pass)
// If any position change is consumed from another PointerInputChange
// or pointer count requirement is not fulfilled
val canceled =
event.changes.any { it.isConsumed }
if (!canceled) {
// Get pointer that is down, if first pointer is up
// get another and use it if other pointers are also down
// event.changes.first() doesn't return same order
val pointerInputChange =
event.changes.firstOrNull { it.id == pointerId }
?: event.changes.first()
// Next time will check same pointer with this id
pointerId = pointerInputChange.id
pointer = pointerInputChange
val zoomChange = event.calculateZoom()
val rotationChange = event.calculateRotation()
val panChange = event.calculatePan()
if (!pastTouchSlop) {
zoom *= zoomChange
rotation += rotationChange
pan += panChange
val centroidSize = event.calculateCentroidSize(useCurrent = false)
val zoomMotion = abs(1 - zoom) * centroidSize
val rotationMotion =
abs(rotation * kotlin.math.PI.toFloat() * centroidSize / 180f)
val panMotion = pan.getDistance()
if (zoomMotion > touchSlop ||
rotationMotion > touchSlop ||
panMotion > touchSlop
) {
pastTouchSlop = true
lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
}
}
if (pastTouchSlop) {
val centroid = event.calculateCentroid(useCurrent = false)
val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
if (effectiveRotation != 0f ||
zoomChange != 1f ||
panChange != Offset.Zero
) {
onGesture(
centroid,
panChange,
zoomChange,
effectiveRotation,
pointer,
event.changes
)
}
if (consume) {
event.changes.forEach {
if (it.positionChanged()) {
it.consume()
}
}
}
}
}
} while (!canceled && event.changes.any { it.pressed })
onGestureEnd(pointer)
}
}
Usage
@OptIn(ExperimentalFoundationApi::class)
@Preview
@Composable
private fun TouchComposable() {
var zoom by remember { mutableStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val outer = (1..60).toList().chunked(6)
Box(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
//zoom in/out and move around
detectTransformGestures(
pass = PointerEventPass.Initial,
onGesture = { gestureCentroid: Offset,
gesturePan: Offset,
gestureZoom: Float,
_,
_,
changes: List<PointerInputChange> ->
val oldScale = zoom
val newScale = (zoom * gestureZoom).coerceIn(0.5f..5f)
offset =
(offset + gestureCentroid / oldScale) - (gestureCentroid / newScale + gesturePan / oldScale)
zoom = newScale
// Consume touch when multiple fingers down
// This prevents click and long click if your finger touches a
// button while pinch gesture is being invoked
val size = changes.size
if (size > 1) {
changes.forEach { it.consume() }
}
}
)
}) {
Box(
Modifier
.graphicsLayer {
translationX = -offset.x * zoom
translationY = -offset.y * zoom
scaleX = zoom
scaleY = zoom
}
.background(Color.Cyan)
) {
Column {
outer.forEach { inner ->
Row {
inner.forEach { tile ->
var text by remember {
mutableStateOf(tile.toString())
}
Text(text,
Modifier
.padding(8.dp)
.combinedClickable(
onClick = {
text = "CL"
},
onLongClick = {
text = "LO"
}
)
.background(Color.Green)
.padding(8.dp)
)
}
}
}
}
}
}
}
You can also find this gesture and some other gestures in this library
https://github.com/SmartToolFactory/Compose-Extended-Gestures
And more about gestures in this tutorial
https://github.com/SmartToolFactory/Jetpack-Compose-Tutorials