Как создать перетаскиваемый и вращаемый блок в Jetpack Compose?

Я работаю над приложением 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()
}

Перетаскивание и вращение работают нормально, если они реализованы отдельно, но когда я пытаюсь объединить перетаскивание и вращение, взаимодействие не работает должным образом.

Вот демо-версия проблемы:

Я уверен, что что-то упускаю. Может ли кто-нибудь помочь мне с этим?

3
0
113
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Когда вы применяете модификатор graphicsLayer для поворота синего прямоугольника, это также влияет на координаты всех последующих модификаторов. Когда коробка поворачивается на 180°, верхняя часть становится нижней, а левая становится правой: перемещение коробки теперь инвертируется.

Обычно есть два подхода к решению этой проблемы:

  1. Переведите координаты, полученные синим квадратом pointerInput, обратно, используя функцию, аналогичную calculateRotationAngle.
  2. Примените 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.

Ранее я работал над аналогичным проектом с использованием XML и Views, где решил эту проблему, применив преобразование вращения матрицы к позиции. Я попробовал реализовать тот же подход в Jetpack Compose, но он не сработал должным образом.

Abdo21 10.07.2024 19:28

Вам необходимо применить Modifier.graphicsLayer, offset, rotate перед pointerInput, чтобы иметь возможность применять следующие преобразования к динамической или текущей позиции Composable, в противном случае этот перевод или преобразование будет применено из исходной позиции. 1-й подход и фрагмент в этом ответе неверен. Вам нужно получить переведенную позицию с помощью матрицы вращения при использовании Modifier.graphicsLayer.pointerInput

Thracian 10.07.2024 19:54
Ответ принят как подходящий

Если вы хотите применить какое-либо преобразование к Composable на основе его динамического положения, вам необходимо применить Modifier.graphicsLayer перед pointerInput. Однако в этом случае вам необходимо соответствующим образом рассчитать перемещение центроида.

Использование матрицы вращения для расчета правильного положения решит проблему.

Вы также можете обратиться к моему вопросу, который также добавляет масштабирование к панораме, что усложняет задачу, но только с вращением и переводом проблема не так сложна.

Как обеспечить естественное панорамирование и масштабирование с помощью 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 мне тоже подходит, я предпочитаю этот, поскольку он обеспечивает более общее решение и не зависит от порядка модификаторов.

Abdo21 10.07.2024 20:45

Без правильного позиционирования модификаторов вы не сможете добиться того, чего хотите. Вот как это работает в Jetpack Compose. Если у вас нет GraphicsLayer или перевода, модификаторы вращения перед переводами PointerInput не будут влиять на то, где в настоящее время находится ваш Composable. Вы можете проверить образцы по ссылке, которую я разместил, чтобы увидеть разницу.

Thracian 10.07.2024 20:56

Обязательно правильно установите порядок модификаторов, если вы хотите получать преобразования из динамического положения вместо исходного или стационарного положения. Но в момент вращения, масштабирования или перемещения составных изменений вам необходимо рассчитать следующее на основе текущего. В моем вопросе по ссылке мне удалось сделать это с матрицей вращения для вращения, изменив с ее помощью перевод, но с масштабированием я все еще не могу найти правильное решение и еще не видел его.

Thracian 10.07.2024 20:59

Другие вопросы по теме