Jetpack Compose元素拖拽旋转缩放问题:左侧锚点缩放失效
Jetpack Compose 可拖拽缩放旋转元素:左侧锚点缩放问题修复
问题背景
需实现一个包含主体和左右两个锚点的可组合项,核心功能需求:
- 拖拽主体可移动整个元素
- 拖拽任意锚点可缩放元素
- 拖拽单个锚点可绕另一个锚点旋转元素
当前已实现主体拖拽、右侧锚点的缩放与旋转功能,但左侧锚点(Anchor 1)的缩放逻辑失效,以下是完整实现代码及尝试的修复逻辑。
完整实现代码
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import java.lang.Math.toDegrees import java.lang.Math.toRadians import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.cos import kotlin.math.sin class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { Scaffold { paddingValues -> Box(modifier = Modifier .fillMaxSize() .padding(paddingValues) ) { Element() } } } } } @Composable fun Element() { var position by remember { mutableStateOf(Offset(100f, 100f)) } var size by remember { mutableStateOf(Size(300f, 60f)) } var rotation by remember { mutableFloatStateOf(0f) } var pivot by remember { mutableStateOf(Offset.Zero) } val anchor1 by rememberUpdatedState( Rect( offset = Offset(0f, 0f), size = Size(size.height, size.height) ) ) val anchor2 by rememberUpdatedState( Rect( offset = Offset(size.width - size.height, 0f), size = Size(size.height, size.height) ) ) val strokeColor = Color.Black Box(modifier = Modifier .size(size.toDp()) .graphicsLayer( translationX = position.x, translationY = position.y, rotationZ = rotation, transformOrigin = pivot.toTransformOrigin(size) ) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() position += dragAmount.rotateBy(rotation) } } ) { // Element body Box( modifier = Modifier .size(size.toDp()) .border(width = 1.dp, color = strokeColor) .drawBehind { drawLine( color = strokeColor, start = anchor1.center, end = anchor2.center, strokeWidth = 1.dp.toPx() ) } ) // Anchor 1 (left side) Box(modifier = Modifier .size(anchor1.size.toDp()) .align(Alignment.CenterStart) .border(width = 1.dp, color = strokeColor, shape = CircleShape) .pointerInput(Unit) { detectDragGestures( onDragStart = { val c1 = anchor1.center val c2 = anchor2.center val w = abs(c1.x - c2.x) // Set pivot to anchor 2 (right side) and adjust position to prevent jump if (pivot != c2) { pivot = c2 val radian = toRadians(rotation.toDouble()) position += Offset( w * (cos(radian).toFloat() - 1), w * sin(radian).toFloat() ) } }, onDrag = { change, dragAmount -> change.consume() val c1 = anchor1.center // Adjust the width (commented out due to failure) //val newWidth = size.width - dragAmount.x //size = size.copy(width = newWidth) // Adjust the position to keep pin 2 in place (commented out) //position += Offset(dragAmount.x, 0f).rotateBy(rotation) rotation += pivot.angleBetween(c1, c1 + dragAmount) } ) } ) // Anchor 2 (right side) Box(modifier = Modifier .size(anchor2.size.toDp()) .align(Alignment.CenterEnd) .border(width = 1.dp, color = strokeColor, shape = CircleShape) .pointerInput(Unit) { detectDragGestures( onDragStart = { val c1 = anchor1.center val c2 = anchor2.center val w = abs(c1.x - c2.x) if (pivot != c1) { pivot = c1 val radian = toRadians(rotation.toDouble()) position -= Offset( w * (cos(radian).toFloat() - 1), w * sin(radian).toFloat() ) } }, onDrag = { change, dragAmount -> change.consume() val c2 = anchor2.center // Adjust the width size = size.copy(width = size.width + dragAmount.x) rotation += pivot.angleBetween(c2, c2 + dragAmount) } ) } ) } } // -------------------- Utilities methods ------------------------------ fun Offset.toTransformOrigin(size: Size) = TransformOrigin( pivotFractionX = x / size.width, pivotFractionY = y / size.height ) // Calculate the angle between two points relative to this Offset fun Offset.angleBetween(p1: Offset, p2: Offset) : Float { val initialAngle = atan2(p1.y - y, p1.x - x) val newAngle = atan2(p2.y - y, p2.x - x) val angleDifference = toDegrees((newAngle - initialAngle).toDouble()).toFloat() return angleDifference } // Rotate an Offset by the given angle fun Offset.rotateBy( angle: Float ): Offset { val angleInRadians = toRadians(angle.toDouble()) val cosAngle = cos(angleInRadians) val sinAngle = sin(angleInRadians) val newX = x * cosAngle - y * sinAngle val newY = x * sinAngle + y * cosAngle return Offset(newX.toFloat(), newY.toFloat()) } @Composable fun Size.toDp() = with(LocalDensity.current) { DpSize( width = width.toDp(), height = height.toDp() ) }
尝试的修复逻辑(未生效)
// Anchor 1 (left side) Box(modifier = Modifier //... .pointerInput(Unit) { detectDragGestures( onDragStart = { //... }, onDrag = { change, dragAmount -> change.consume() val c1 = anchor1.center // Adjust the width val newWidth = size.width - dragAmount.x size = size.copy(width = newWidth) // Adjust the position to keep pin 2 fix position += Offset(dragAmount.x, 0f).rotateBy(rotation) rotation += pivot.angleBetween(c1, c1 + dragAmount) } ) } )
解决方案
问题出在左侧锚点缩放时的坐标变换逻辑:当元素处于旋转状态时,直接使用原始的dragAmount.x调整宽度和位置会导致坐标不匹配,需要先将拖拽量逆旋转到元素的局部坐标系,再进行计算。
修改后的左侧锚点拖拽逻辑如下:
// Anchor 1 (left side) Box(modifier = Modifier .size(anchor1.size.toDp()) .align(Alignment.CenterStart) .border(width = 1.dp, color = strokeColor, shape = CircleShape) .pointerInput(Unit) { detectDragGestures( onDragStart = { val c1 = anchor1.center val c2 = anchor2.center val w = abs(c1.x - c2.x) if (pivot != c2) { pivot = c2 val radian = toRadians(rotation.toDouble()) position += Offset( w * (cos(radian).toFloat() - 1), w * sin(radian).toFloat() ) } }, onDrag = { change, dragAmount -> change.consume() // 将拖拽量逆旋转到元素局部坐标系 val localDrag = dragAmount.rotateBy(-rotation) // 调整宽度:左侧锚点左移时宽度增加,右移时宽度减少 val newWidth = size.width - localDrag.x if (newWidth >= size.height) { // 限制最小宽度为锚点直径,避免元素消失 size = size.copy(width = newWidth) // 调整位置,保持右侧锚点固定 position += Offset(localDrag.x, 0f).rotateBy(rotation) } // 计算旋转角度 val c1 = anchor1.center rotation += pivot.angleBetween(c1, c1 + dragAmount) } ) } )
关键修改说明
- 逆旋转拖拽量:使用
dragAmount.rotateBy(-rotation)将屏幕坐标系的拖拽转换为元素自身局部坐标系的拖拽,确保缩放逻辑不受当前旋转角度影响。 - 宽度调整逻辑:左侧锚点的局部x轴拖拽量直接对应宽度变化,左移(localDrag.x为负)时宽度增加,右移(localDrag.x为正)时宽度减少。
- 位置补偿:使用局部坐标系的x拖拽量旋转后调整元素位置,确保右侧锚点始终保持在原屏幕位置,避免缩放时元素偏移。
- 最小宽度限制:添加
newWidth >= size.height判断,防止宽度小于锚点直径导致元素异常。
内容的提问来源于stack exchange,提问作者Abdo21
相关产品推荐
相关产品推荐

