How to transform Image Composable to match 3 touch points in Compose

659 Views Asked by At

I am currently playing with my old Instant Lab device and I am trying to recreate parts of the old app in jetpack compose.

A feature of the device is to detect 3 touch points on the screen in order to create the border of the image to display.

I was able to dectect the 3 touche points using jetpack compose and find the coordinate (x, y) of each touch points :

enter image description here

Now I would like to display my image between these touch points. I know that I need to use the Image Composable in order to display. But I do not know how to apply the right transformation in order to display this composable between these 3 points using rotation and absolute position (?).

Expected result:

enter image description here

Thank you in advance for your help.

Edit:

I tried using a custom shape I apply to a surface with the following composable :

 @Composable
  private fun Exposing(pointersCoordinates: PointersCoordinates)
  {   
    val exposureShape = GenericShape { _, _ ->
      moveTo(pointersCoordinates.xTopLeft(), pointersCoordinates.yTopLeft())
      lineTo(pointersCoordinates.xTopRight(), pointersCoordinates.yTopRight())
      lineTo(pointersCoordinates.xBottomRight(), pointersCoordinates.yBottomRight())
      lineTo(pointersCoordinates.xBottomLeft(), pointersCoordinates.yBottomLeft())
    }

    Box(
      modifier = Modifier
        .fillMaxSize()
        .background(Color.Black)
    ) {
      Surface(
        modifier = Modifier.fillMaxSize(),
        shape = exposureShape,
        color = Color.Yellow,
        border = BorderStroke(1.dp, Color.Red)
      ) {
        Image(
          modifier = Modifier.fillMaxSize(),
          bitmap = viewModel?.bitmap?.asImageBitmap() ?: ImageBitmap(0, 0),
          contentDescription = "photo"
        )
      }
    }
  }

It's working correctly :) But is it the best way to do it?

2

There are 2 best solutions below

0
On BEST ANSWER

Since you are able to get a Rect from touch points you can use Canvas or Modifier.drawWithContent{}.

Clip image and draw

If you wish to clip your image based on rectangle you can check out this answer. Whit BlendModes you can clip not only to rectangle or shapes that easy to create but shapes you get from web or image

How to clip or cut a Composable?

Another approach for clipping is using clip() function of DrawScope, this approach only clips to a Rect.

Also you can use Modifier.clip() with custom shape to clip it as required as in this answer

Draw without clippin

If you don't want to clip your image draw whole image insider rect you can do it with dstOffset with dstSize or translate with dstSize

@Composable
private fun DrawImageWithTouchSample() {

    val rect = Rect(topLeft = Offset(100f, 100f), bottomRight = Offset(1000f, 1000f))

    val modifier = Modifier
        .fillMaxSize()
        .pointerInput(Unit) {
            detectTapGestures {
                // Tap here to get points
            }
        }
    val image = ImageBitmap.imageResource(id = R.drawable.landscape5)
    Canvas(modifier = modifier) {

        // Clip image
        clipRect(
            left = rect.left,
            top = rect.top,
            right = rect.right,
            bottom = rect.bottom

        ){
            drawImage(image = image)
        }
        // Not clipping image
//        drawImage(
//            image = image,
//            dstOffset = IntOffset(x = rect.left.toInt(), y = rect.top.toInt()),
//            dstSize = IntSize(width = rect.width.toInt(), height = rect.height.toInt())
//        )
//

        translate(
            left = rect.left,
            top = rect.top + 1000
        ){
            drawImage(
                image = image,
                dstSize = IntSize(width = rect.width.toInt(), height = rect.height.toInt())
            )
        }
    }
}

Image on top is clipped with clipRect while second one is scaled to fit inside rect because of dstSize

enter image description here

0
On

You don't need to use Image. A Box clipped to a circle and with a grey-ish background would do. Of course, you'll need an Image to display the actual image you'll be dragging the points on. Here's a sample implementation:

val x by remember { mutableStateOf (Offset(...)) } // Initial Offset
val y by ...
val z by ...

// The image is occupying all of this composable, and these will be positioned ABOVE the image composable using order of positioning.

Layout(
  content = {
    Box(/*Copy From Later*/)
    Box(...)
    Box(...)
  }
) { measurables, constraints ->
  val placeables = measurables.map { it.measure(constraints) } // Default Constraining.
  layout(constraints.maxWidth, constraints.maxHeight){
    measurables[0].place(x.x, x.y)
    measurables[1].place(y.x, y.y)
    measurables[2].place(z.x, z.y)
  }
}

Layout the box like so,

Box(
  modifier = Modifier
               .clip(CircleShape)
               .size(5.dp) // Change per need
               .pointerInput(Unit) {
                  detectDragGestures { change, _ ->
                    x = change.position // similarly y for second, and z for the third box 
                  }
                }
)

This should track/update/position the points wherever you drag them. All the points are individually determined by their own state-holders. You'll need to add this pointerInput logic to every Box, but it would likely be better if you just created a single function to be invoked based on an index, unique to each Box, but that's not something required to be covered here.