You can use this to detect when user starts transform gesture, when it ends and have option to consume transform events to enable gesture propagation to scroll or other drag events.
/**
* A gesture detector for rotation, panning, and zoom. Once touch slop has been reached, the
* user can use rotation, panning and zoom gestures. [onGesture] will be called when any of the
* rotation, zoom or pan occurs, passing the rotation angle in degrees, zoom in scale factor and
* pan as an offset in pixels. Each of these changes is a difference between the previous call
* and the current gesture. This will consume all position changes after touch slop has
* been reached. [onGesture] will also provide centroid of all the pointers that are down.
*
* After gesture started when last pointer is up [onGestureEnd] is triggered.
*
* @param consume flag consume [PointerInputChange]s this gesture uses. Consuming
* returns [PointerInputChange.isConsumed] true which is observed by other gestures such
* as drag, scroll and transform. When this flag is true other gesture don't receive events
* @param onGestureStart callback for notifying transform gesture has started with initial
* pointer
* @param onGesture callback for passing centroid, pan, zoom, rotation and main pointer and
* pointer size to caller. Main pointer is the one that touches screen first. If it's lifted
* next one that is down is the main pointer.
* @param onGestureEnd callback that notifies last pointer is up and gesture is ended
*
*/
suspend fun PointerInputScope.detectTransformGestures(
panZoomLock: Boolean = false,
consume: Boolean = true,
onGestureStart: (PointerInputChange) -> Unit = {},
onGesture: (
centroid: Offset,
pan: Offset,
zoom: Float,
rotation: Float,
mainPointer: PointerInputChange,
changes: List<PointerInputChange>
) -> Unit,
onGestureEnd: (PointerInputChange) -> Unit = {}
) {
forEachGesture {
awaitPointerEventScope {
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)
onGestureStart(down)
var pointer = down
// Main pointer is the one that is down initially
var pointerId = down.id
do {
val event = awaitPointerEvent()
// If any position change is consumed from another PointerInputChange
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 * 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)
}
}
}
also it let's you to get mainPointer, first pointer initially then the one that is returned from pointer list with id, and all pointers that are down
which let's you to detect how many fingers user invoking gesture with
Modifier.pointerInput(Unit) {
detectTransformGestures(
onGestureStart = {
transformDetailText = "GESTURE START"
},
onGesture = { gestureCentroid: Offset,
gesturePan: Offset,
gestureZoom: Float,
gestureRotate: Float,
mainPointerInputChange: PointerInputChange,
pointerList: List<PointerInputChange> ->
},
onGestureEnd = {
borderColor = Color.LightGray
transformDetailText = "GESTURE END"
}
)
}
Full library code that has onEvent counterpart, touch delegate and demo is available in github repository.