如何使用Modifier.graphicsLayer{}与Modifier.pointerInput()实现Composable的自然平移、缩放和旋转(非固定位置)
I understand your goal: you want a Composable that can be panned, rotated, and scaled naturally, with gestures that feel intuitive even when the element is moved away from its initial position. The key issue with your previous attempts was correctly adjusting the offset to maintain the gesture centroid during scaling and rotation, and properly converting pan deltas to the rotated coordinate system.
Here's a complete, working implementation that addresses all these requirements:
import androidx.compose.foundation.Image import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.painterResource import kotlin.math.cos import kotlin.math.sin @Composable fun TransformableImage() { // Track the current transform state var scale by remember { mutableStateOf(1f) } var rotation by remember { mutableStateOf(0f) } var offset by remember { mutableStateOf(Offset.Zero) } Box(modifier = Modifier.fillMaxSize()) { Image( painter = painterResource(id = R.drawable.landscape1), contentDescription = "Transformable image", modifier = Modifier .pointerInput(Unit) { detectTransformGestures( onGesture = { centroid, pan, zoom, rotationDelta -> val oldScale = scale val newScale = (scale * zoom).coerceIn(0.5f..5f) // Limit scale range // 1. Adjust offset to keep gesture centroid fixed during scaling val localCentroid = (centroid - offset) / oldScale offset = centroid - localCentroid * newScale // 2. Adjust offset to keep gesture centroid fixed during rotation val rotatedLocalCentroid = localCentroid.rotateBy(rotationDelta) offset = centroid - rotatedLocalCentroid * newScale // 3. Apply pan, adjusted for current rotation offset += pan.rotateBy(-rotation) // Update state variables rotation += rotationDelta scale = newScale } ) } .graphicsLayer { // Apply the computed transforms translationX = offset.x translationY = offset.y scaleX = scale scaleY = scale rotationZ = rotation // Set clip = true if you want to restrict drawing to original layout bounds // clip = true } ) } } /** * Rotates an Offset around the origin by the given angle (in degrees). * Positive angles rotate counterclockwise. */ fun Offset.rotateBy(angleDegrees: Float): Offset { val angleRadians = Math.toRadians(angleDegrees.toDouble()).toFloat() val cosAngle = cos(angleRadians) val sinAngle = sin(angleRadians) return Offset( x = x * cosAngle - y * sinAngle, y = x * sinAngle + y * cosAngle ) }
Key Explanations:
Scaling with Centroid Fix:
- We first calculate where the gesture centroid sits in the image's local coordinate system (before scaling).
- We then adjust the offset so that this local point remains at the same screen position after scaling, ensuring zoom feels natural around your fingers.
Rotation with Centroid Fix:
- After scaling, we rotate the local centroid by the gesture's rotation delta.
- We adjust the offset again to keep this rotated local point at the same screen position, so rotation happens around your fingers.
Panning in Rotated Space:
- The pan delta from the gesture is in screen coordinates. We convert this to the image's rotated local coordinate system using
rotateBy(-rotation). - This ensures that dragging your finger in a direction moves the image in the same relative direction, even when it's rotated.
- The pan delta from the gesture is in screen coordinates. We convert this to the image's rotated local coordinate system using
Modifier Order Note:
- The
pointerInputmodifier comes beforegraphicsLayerhere because we want to receive gesture events in the screen's coordinate system (not the transformed image's local space). This makes it easier to calculate the correct offset adjustments. - If you want the touch detection to only respond to taps on the visible image (not the entire layout), you can add a hit test check inside
pointerInputto verify if the touch point is within the transformed image bounds.
This implementation should give you the natural, intuitive transform behavior you're looking for, matching the example you referenced.
内容的提问来源于stack exchange,提问作者Thracian

