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
})
}
}
}
}
}
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.