Image cropper with compose

Viewed 134

I'm trying to implement simple image cropper with compose. The overlay can be resized and drag over the image. The problem is, it can be dragged out of the image and out of screen too. How to set limits for the overlay to move and resized over the image only?

The updated code

@Composable
fun ImageCrooper() {
  var size by remember { mutableStateOf(Size.Zero) }
  
  var offsetX by remember { mutableStateOf(0f) }
  var offsetY by remember { mutableStateOf(0f) }
  
  var height by remember { mutableStateOf(140f) }
  var width by remember { mutableStateOf(140f) }
  BoxWithConstraints(
    Modifier
      .fillMaxWidth()
      .height(240.dp)
      .onSizeChanged {
        size = it.toSize()
      }
  ) {
    Image(
      modifier = Modifier
        .fillMaxSize(),
      contentScale = ContentScale.Crop,
      painter = rememberAsyncImagePainter(
        model = "https://i.picsum.photos/id/216/200/200.jpg?hmac=7Weas8POu49YrmUyJ6tWdqVMx-hw6lryzl8HnHZBzjc"
      ),
      contentDescription = "A Content description",
    )
    BoxWithConstraints(
      modifier = Modifier
        .offset {
          IntOffset(offsetX.roundToInt(), offsetY.roundToInt())
        }
        .height(height.dp)
        .width(width.dp)
    ) {
      //      move the overlay
      Box(
        Modifier
          .background(Color.Black.copy(0.6f))
          .fillMaxSize()
          .pointerInput(Unit) {
            detectDragGestures { change, dragAmount ->
              change.consumeAllChanges()
              offsetY = (offsetY + dragAmount.y).coerceIn(0f, 230f)
              offsetX = (offsetX + dragAmount.x).coerceIn(0f, size.width)
            }
          }
      )
      //      resize the overlay
      
      Box(
        Modifier
          .align(Alignment.BottomEnd)
          .background(Color.White)
          .height(20.dp)
          .width(20.dp)
          .pointerInput(Unit) {
            detectDragGestures { change, dragAmount ->
              change.consumeAllChanges()
              width = (width + dragAmount.x).coerceIn(140f, size.width - width)
              height = (height + dragAmount.y).coerceIn(140f, size.height - height)
        
            }
          }
      )
    }
  }
}
1 Answers

You can limit offset variables inside your Composable dimension bounds using coerceIn functions. You can get size of your Composable using

var size by remember { mutableStateOf(Size.Zero) }
Modifier.onSizeChanged {
    size = it.toSize()
}

offsetY = (offsetY + dragAmount.y).coerceIn(0f, size.height)
offsetX = (offsetX + dragAmount.x).coerceIn(0f, size.width)

Updated your code to resize or move, i'm currently building one, you can use it as reference. There are few things to consider

1- You need two rectangles, one for drawing overlay another for clipping rectangle. Zoom, pan and resize is applied only to crop rectangle while draw rectangle is panned and resized.

2- You need to check touch positions with change parameter in drag because each touch position has different action. While moving from left corners to right reduce width of your rectangle moving from right corners to right grow it.

3- You need to calculate touch position to edge of corners. This is for not jumping edge of your rectangle to user's touch position.

These are basics i remember. There are many complexities when zoom, pan and limiting pan inside bounds and animating back to valid bounds are involved.

And please write x coordinate before y, width before height all the time to not put wrong values in wrong variables. Currently it's prone to mistakes.

@Composable
fun ImageCroper() {


    BoxWithConstraints {


        var offsetX by remember { mutableStateOf(0f) }
        var offsetY by remember { mutableStateOf(0f) }

        val minSize = 300f
        var width by remember { mutableStateOf(minSize) }
        var height by remember { mutableStateOf(minSize) }

        val widthInDp:Dp
        val heightInDp:Dp

        with(LocalDensity.current){
            widthInDp = width.toDp()
            heightInDp = height.toDp()
        }


        val imageWidth = constraints.maxWidth
        val imageHeight =  constraints.maxHeight

        Image(
            modifier = Modifier
                .fillMaxSize(),
            contentScale = ContentScale.Crop,
            painter = rememberAsyncImagePainter(
                model = "https://i.picsum.photos/id/216/200/200.jpg?hmac=7Weas8POu49YrmUyJ6tWdqVMx-hw6lryzl8HnHZBzjc"
            ),
            contentDescription = "A Content description",
        )
        BoxWithConstraints(
            modifier = Modifier
                .offset {
                    IntOffset(offsetX.roundToInt(), offsetY.roundToInt())
                }
                .size(widthInDp, heightInDp)
        ) {
            //      move the overlay
            Box(
                Modifier
                    .background(Color.Black.copy(0.6f))
                    .fillMaxSize()
                    .pointerInput(Unit) {
                        detectDragGestures { change, dragAmount ->
                            offsetX = (offsetX + dragAmount.x).coerceIn(
                                0f,
                                (imageWidth- width).coerceAtLeast(0f)
                            )
                            offsetY = (offsetY + dragAmount.y).coerceIn(
                                0f,
                                (imageHeight- height).coerceAtLeast(0f)
                            )
                        }
                    }
            )
            //      resize the overlay

            Box(
                Modifier
                    .align(Alignment.BottomEnd)
                    .background(Color.White)
                    .height(20.dp)
                    .width(20.dp)
                    .pointerInput(Unit) {
                        detectDragGestures { change, dragAmount ->
                            width = (width + dragAmount.x).coerceIn(
                                minSize,
                                (imageWidth -offsetX).coerceAtLeast(minSize)
                            )
                            height = (height + dragAmount.y).coerceIn(
                                minSize,
                                (imageHeight -offsetY).coerceAtLeast(minSize)                            )

                        }
                    }
            )
        }
    }
}
Related