This is not the duplicate of using Modifier.graphicsLayer{}
to rotate or pan Image. I want move, rotate and scale a Composable on screen not in a stationary position.
What i want to achieve is as in this image
Order of Modifier.graphicsLayer{}
and Modifier.pointerInput()
matter. If Modifier.pointerInput()
is placed after Modifier.graphicsLayer{}
Composable's touch area is scaled, translated and rotated.
@Composable
private fun Test() {
var offsetLeft by remember { mutableStateOf(Offset.Zero) }
var offsetRight by remember { mutableStateOf(Offset.Zero) }
var rotationLeft by remember { mutableStateOf(0f) }
var rotationRight by remember { mutableStateOf(0f) }
val modifierLeft = Modifier
.border(3.dp, Color.Red)
.graphicsLayer {
translationX = offsetLeft.x
translationY = offsetLeft.y
rotationZ = rotationLeft
}
.border(2.dp, Color.Green)
.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotation ->
offsetLeft += pan
rotationLeft += rotation
}
}
val modifierRight = Modifier
.border(3.dp, Color.Red)
.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotation ->
offsetRight += pan
rotationRight += rotation
}
}
.border(2.dp, Color.Green)
.graphicsLayer {
translationX = offsetRight.x
translationY = offsetRight.y
rotationZ = rotationRight
}
Row(modifier = Modifier.fillMaxSize()) {
Image(
painter = painterResource(id = R.drawable.landscape1),
contentDescription = "",
modifierLeft.aspectRatio(1f).weight(1f)
)
Image(
painter = painterResource(id = R.drawable.landscape1),
contentDescription = "",
modifierRight.aspectRatio(1f).weight(1f)
)
}
}
I want to be able to move, rotate and a Composable like the left Image in gif from its current transformation.
What i did so far, i rotate pan by Rotation Matrix via
/**
* 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()
It works for panning, pan works fine but as translation from original position increase rotation and scaling doesn't seem to work. I'm not able translate position correctly.
@Composable
private fun MyComposable() {
var offset by remember { mutableStateOf(Offset.Zero) }
var zoom by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var text by remember { mutableStateOf("") }
var centroid by remember { mutableStateOf(Offset.Zero) }
val modifier = Modifier
.border(3.dp, Color.Green)
.fillMaxWidth()
.aspectRatio(4 / 3f)
.graphicsLayer {
this.translationX = offset.x * zoom
this.translationY = offset.y * zoom
this.scaleX = zoom
this.scaleY = zoom
this.rotationZ = rotation
// TransformOrigin(0f, 0f)
}
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { gestureCentroid, gesturePan, gestureZoom, gestureRotate ->
rotation += gestureRotate
zoom *= gestureZoom
offset += gesturePan.rotateBy(rotation)
centroid = gestureCentroid
}
)
}
.drawWithContent {
drawContent()
drawCircle(color = Color.Red, center = centroid, radius = 20f)
}
Image(
modifier = modifier,
painter = painterResource(id = R.drawable.landscape1),
contentDescription = null,
contentScale = ContentScale.FillBounds
)
}
I tried changing TransformOrigin(). Also updated sample code that works for stationary touch position, this is modified version of the one available in developer.android page
@Composable
private fun MyComposable3() {
var zoom by remember { mutableStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }
var centroid by remember { mutableStateOf(Offset.Zero) }
var centroidOld by remember { mutableStateOf(Offset.Zero) }
var centroidNew by remember { mutableStateOf(Offset.Zero) }
var angle by remember { mutableStateOf(0f) }
val imageModifier = Modifier
.border(3.dp, Color.Green)
.graphicsLayer {
translationX = -offset.x * zoom
translationY = -offset.y * zoom
scaleX = zoom
scaleY = zoom
rotationZ = angle
TransformOrigin(0f, 0f).also { transformOrigin = it }
}
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { gestureCentroid, gesturePan, gestureZoom, gestureRotate ->
val oldScale = zoom
val newScale = (zoom * gestureZoom).coerceIn(0.5f..5f)
angle += gestureRotate
// For natural zooming and rotating, the centroid of the gesture should
// be the fixed point where zooming and rotating occurs.
// We compute where the centroid was (in the pre-transformed coordinate
// space),
// and then compute where it will be after this delta.
// We then compute what the new offset should be to keep the centroid
// visually stationary for rotating and zooming, and also apply the pan.
centroidOld = (offset + gestureCentroid / oldScale).rotateBy(gestureRotate)
centroidNew =
(gestureCentroid / newScale + gesturePan.rotateBy(angle) / oldScale)
offset = centroidOld - centroidNew
zoom = newScale
centroid = gestureCentroid
}
)
}
.border(3.dp, Color.Red)
.drawWithContent {
drawContent()
drawCircle(color = Color.Red, center = centroid, radius = 20f)
drawCircle(color = Color.Green, center = centroidOld, radius = 20f)
drawCircle(color = Color.Blue, center = centroidNew, radius = 20f)
}
Image(
modifier = imageModifier
.fillMaxWidth()
.aspectRatio(4 / 3f),
painter = painterResource(id = R.drawable.landscape2),
contentDescription = null,
contentScale = ContentScale.FillBounds
)
}
This one is not working either.
The problem is here:
.graphicsLayer {
this.translationX = offset.x*zoom
this.translationY = offset.y*zoom
this.scaleX = zoom
this.scaleY = zoom
this.rotationZ = rotation
}
Remove the zoom
multiplication from this part and change it to:
.graphicsLayer {
this.translationX = offset.x
this.translationY = offset.y
this.scaleX = zoom
this.scaleY = zoom
this.rotationZ = rotation
}
Do the multiplication in the detectTransformGestures
function instead:
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { gestureCentroid, gesturePan, gestureZoom, gestureRotate ->
rotation += gestureRotate
zoom *= gestureZoom
offset += gesturePan.rotateBy(rotation)*zoom
centroid = gestureCentroid
}
)
}
Here's the full updated code:
@Composable
private fun MyComposable() {
var offset by remember { mutableStateOf(Offset.Zero) }
var zoom by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var text by remember { mutableStateOf("") }
var centroid by remember { mutableStateOf(Offset.Zero) }
val modifier = Modifier
.border(3.dp, Color.Green)
.fillMaxWidth()
.aspectRatio(4 / 3f)
.graphicsLayer {
this.translationX = offset.x
this.translationY = offset.y
this.scaleX = zoom
this.scaleY = zoom
this.rotationZ = rotation
// TransformOrigin(0f, 0f)
}
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { gestureCentroid, gesturePan, gestureZoom, gestureRotate ->
rotation += gestureRotate
zoom *= gestureZoom
offset += gesturePan.rotateBy(rotation)*zoom
centroid = gestureCentroid
}
)
}
.drawWithContent {
drawContent()
drawCircle(color = Color.Blue, center = centroid, radius = 20f)
}
Image(
modifier = modifier.background(Color.Gray),
painter = painterResource(id = R.drawable.img),
contentDescription = null,
contentScale = ContentScale.FillBounds
)
}
Edit : make the image stationary when rotating or zooming
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { gestureCentroid, gesturePan, gestureZoom, gestureRotate ->
rotation += gestureRotate
zoom *= gestureZoom
if (gestureRotate == 0f && gestureZoom == 1f) { // only allow movement if not rotating or zooming
offset += gesturePan.rotateBy(rotation) * zoom
}
centroid = gestureCentroid
}
)
}