A gesture detector for rotation, panning, and zoom.
@Composable
fun DetectTransformGestures() {
/**
* Rotates the given offset around the origin by the given angle in degrees.
*
* A positive angle indicates a counterclockwise rotation around the right-handed 2D Cartesian
* coordinate system.
*
* See: [Rotation matrix](https://en.wikipedia.org/wiki/Rotation_matrix)
*/
fun Offset.rotateBy(angle: Float): Offset {
val angleInRadians = angle * (PI / 180)
val cos = cos(angleInRadians)
val sin = sin(angleInRadians)
return Offset((x * cos - y * sin).toFloat(), (x * sin + y * cos).toFloat())
}
var offset by remember { mutableStateOf(Offset.Zero) }
var zoom by remember { mutableStateOf(1f) }
var angle by remember { mutableStateOf(0f) }
Box(
Modifier.pointerInput(Unit) {
detectTransformGestures(
onGesture = { centroid, pan, gestureZoom, gestureRotate ->
val oldScale = zoom
val newScale = zoom * gestureZoom
// For natural zooming and rotating, the centroid of the gesture should
// be the fixed point where zooming and rotating occurs.
// We compute where the centroid was (in the pre-transformed coordinate
// space), and then compute where it will be after this delta.
// We then compute what the new offset should be to keep the centroid
// visually stationary for rotating and zooming, and also apply the pan.
offset =
(offset + centroid / oldScale).rotateBy(gestureRotate) -
(centroid / newScale + pan / oldScale)
zoom = newScale
angle += gestureRotate
}
)
}
.graphicsLayer {
translationX = -offset.x * zoom
translationY = -offset.y * zoom
scaleX = zoom
scaleY = zoom
rotationZ = angle
transformOrigin = TransformOrigin(0f, 0f)
}
.background(Color.Blue)
.fillMaxSize()
)
}