You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Modifier.graphicsLayer{}与Modifier.pointerInput()实现Composable的自然平移、缩放和旋转(非固定位置)

Solution for Natural Transform Gestures in Jetpack Compose

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:

  1. 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.
  2. 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.
  3. 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.

Modifier Order Note:

  • The pointerInput modifier comes before graphicsLayer here 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 pointerInput to 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 19:47:39