Я работаю над приложением Jetpack Compose и хочу создать Box
, который можно будет как перетаскивать, так и вращать с помощью мыши. Я должен иметь возможность щелкнуть и перетащить весь блок, чтобы переместить его по экрану. также я хочу добавить небольшую ручку в верхней центральной части коробки. Когда я перетаскиваю этот маркер, Box
должен вращаться вокруг его центра.
Вот что я пробовал до сих пор:
@Composable
fun DragRotateBox() {
var rotation by remember { mutableStateOf(0f) }
var position by remember { mutableStateOf(Offset.Zero) }
var initialTouch = Offset.Zero
val boxSize = 100.dp
val handleSize = 20.dp
val boxSizePx = with(LocalDensity.current) { boxSize.toPx() }
val center = Offset(boxSizePx, boxSizePx)
// Main Box
Box(
modifier = Modifier
.graphicsLayer(
rotationZ = rotation,
translationX = position.x,
translationY = position.y
)
.background(Color.Blue)
.size(boxSize)
.pointerInput(Unit) {
detectDragGestures(
onDrag = {change, dragAmount ->
change.consume()
position += dragAmount
}
)
}
) {
// Rotation handler
Box(
modifier = Modifier
.size(handleSize)
.background(Color.Red)
.align(Alignment.TopCenter)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
initialTouch = offset
},
onDrag = { change, dragAmount ->
change.consume()
val angle = calculateRotationAngle(center, initialTouch, change.position)
rotation += angle
}
)
}
)
}
}
// Generated by ChatGPT!
fun calculateRotationAngle(pivot: Offset, initialTouch: Offset, currentTouch: Offset): Float {
val initialVector = initialTouch - pivot
val currentVector = currentTouch - pivot
val initialAngle = atan2(initialVector.y, initialVector.x)
val currentAngle = atan2(currentVector.y, currentVector.x)
return Math.toDegrees((currentAngle - initialAngle).toDouble()).toFloat()
}
Перетаскивание и вращение работают нормально, если они реализованы отдельно, но когда я пытаюсь объединить перетаскивание и вращение, взаимодействие не работает должным образом.
Вот демо-версия проблемы:
Я уверен, что что-то упускаю. Может ли кто-нибудь помочь мне с этим?
Когда вы применяете модификатор graphicsLayer
для поворота синего прямоугольника, это также влияет на координаты всех последующих модификаторов. Когда коробка поворачивается на 180°, верхняя часть становится нижней, а левая становится правой: перемещение коробки теперь инвертируется.
Обычно есть два подхода к решению этой проблемы:
pointerInput
, обратно, используя функцию, аналогичную calculateRotationAngle
.pointerInput
до того, как graphicsLayer
испортит координаты.Я бы предпочел решение 2, потому что оно проще. Но имейте в виду: если вы просто переместите синий прямоугольник pointerInput
в переднюю часть цепочки модификаторов, жест перетаскивания будет обнаружен только тогда, когда вы щелкнете по исходному положению поля в левом верхнем углу. Это связано с тем, что не только поворот еще не применен (что было задумано), но и позиционный перевод еще не применен, поэтому с точки зрения pointerInput
ящик никогда не перемещался. Только после обнаружения жестов перетаскивания применяется позиционный перевод в graphicsLayer
.
Чтобы это исправить, вам нужно разделить позиционный перевод и поворот, применив один модификатор graphicsLayer
перед pointerInput
, который только переводит позицию, и еще один после, чтобы выполнить поворот. На самом деле существует специальный модификатор offset
, который можно использовать для позиционного перевода, и еще один модификатор, просто rotate
для текущего элемента, поэтому вам следует использовать их вместо graphicsLayer
.
Когда вы измените цепочку модификаторов синего прямоугольника на эту, все должно работать как положено:
modifier = Modifier
.offset { position.round() }
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, dragAmount ->
change.consume()
position += dragAmount
}
)
}
.rotate(rotation)
.background(Color.Blue)
.size(boxSize)
Я только что понял: одно из предостережений этого решения заключается в том, что вы можете захватить синий ящик только за его исходную не повернутую ориентацию, даже если он повернут: при повороте на 45 ° (угол вверх) вы не можете захватить ни один из синих углов. , но вы можете захватить немного белого фона ближе к середине краев. Только представьте, как в таком положении будет выглядеть не повернутый ящик, вот за что можно схватиться.
Это невозможно исправить при использовании предложенного мной решения 2, поэтому вам, возможно, все-таки придется использовать решение 1.
Вам необходимо применить Modifier.graphicsLayer, offset
, rotate
перед pointerInput
, чтобы иметь возможность применять следующие преобразования к динамической или текущей позиции Composable, в противном случае этот перевод или преобразование будет применено из исходной позиции. 1-й подход и фрагмент в этом ответе неверен. Вам нужно получить переведенную позицию с помощью матрицы вращения при использовании Modifier.graphicsLayer.pointerInput
Если вы хотите применить какое-либо преобразование к Composable на основе его динамического положения, вам необходимо применить Modifier.graphicsLayer перед pointerInput. Однако в этом случае вам необходимо соответствующим образом рассчитать перемещение центроида.
Использование матрицы вращения для расчета правильного положения решит проблему.
Вы также можете обратиться к моему вопросу, который также добавляет масштабирование к панораме, что усложняет задачу, но только с вращением и переводом проблема не так сложна.
@Preview
@Composable
fun DragRotateBox() {
Column(
modifier = Modifier.fillMaxSize()
) {
var rotation by remember { mutableStateOf(0f) }
var position by remember { mutableStateOf(Offset.Zero) }
val boxSize = 100.dp
val handleSize = 20.dp
var initialTouch = Offset.Zero
val boxSizePx = with(LocalDensity.current) { boxSize.toPx() }
val center = Offset(boxSizePx, boxSizePx)
// Main Box
Box(
modifier = Modifier
.graphicsLayer(
rotationZ = rotation,
translationX = position.x,
translationY = position.y
)
.background(Color.Blue)
.size(boxSize)
.pointerInput(Unit) {
detectTransformGestures { _, pan, _, _ ->
position += pan.rotateBy(rotation)
}
}
) {
// Rotation handler
Box(
modifier = Modifier
.size(handleSize)
.background(Color.Red)
.align(Alignment.TopCenter)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
initialTouch = offset
},
onDrag = { change, dragAmount ->
change.consume()
val angle = calculateRotationAngle(center, initialTouch, change.position)
rotation += angle
}
)
}
)
}
}
}
// Generated by ChatGPT!
fun calculateRotationAngle(pivot: Offset, initialTouch: Offset, currentTouch: Offset): Float {
val initialVector = initialTouch - pivot
val currentVector = currentTouch - pivot
val initialAngle = atan2(initialVector.y, initialVector.x)
val currentAngle = atan2(currentVector.y, currentVector.x)
return Math.toDegrees((currentAngle - initialAngle).toDouble()).toFloat()
}
/**
* 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 = ROTATION_CONST * angle
val newX = x * cos(angleInRadians) - y * sin(angleInRadians)
val newY = x * sin(angleInRadians) + y * cos(angleInRadians)
return Offset(newX, newY)
}
internal const val ROTATION_CONST = (Math.PI / 180f).toFloat()
Спасибо! Это решение идеально. Хотя подход @Thracian мне тоже подходит, я предпочитаю этот, поскольку он обеспечивает более общее решение и не зависит от порядка модификаторов.
Без правильного позиционирования модификаторов вы не сможете добиться того, чего хотите. Вот как это работает в Jetpack Compose. Если у вас нет GraphicsLayer или перевода, модификаторы вращения перед переводами PointerInput не будут влиять на то, где в настоящее время находится ваш Composable. Вы можете проверить образцы по ссылке, которую я разместил, чтобы увидеть разницу.
Обязательно правильно установите порядок модификаторов, если вы хотите получать преобразования из динамического положения вместо исходного или стационарного положения. Но в момент вращения, масштабирования или перемещения составных изменений вам необходимо рассчитать следующее на основе текущего. В моем вопросе по ссылке мне удалось сделать это с матрицей вращения для вращения, изменив с ее помощью перевод, но с масштабированием я все еще не могу найти правильное решение и еще не видел его.
Ранее я работал над аналогичным проектом с использованием XML и Views, где решил эту проблему, применив преобразование вращения матрицы к позиции. Я попробовал реализовать тот же подход в Jetpack Compose, но он не сработал должным образом.