Clip image in Jetpack Compose

3.1k Views Asked by At

I am using clip to trim an Image using Compose, to show only the left edge part of the image. But it maintains the width with blank space. How can I crop the right part(marked in red)?

enter image description here

I tried setting a custom width for Image but its not working as expected.

enter image description here

Here is the code I am using,

object CropShape : Shape {
    override fun createOutline(
        size: androidx.compose.ui.geometry.Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline = Outline.Rectangle(
        Rect(Offset.Zero, androidx.compose.ui.geometry.Size((58 * density.density), size.height))
    )
}

@Composable
private fun test(){

Image(
        modifier = Modifier
            .height(142.dp)
            .clip(CropShape)
            .padding(start = 20.dp, bottom = 20.dp)
            .rotate(TiltedImageRotation)
        painter = resourcePainter(R.drawable.image),
        contentScale = ContentScale.FillHeight,
        contentDescription = null,
    )
}
2

There are 2 best solutions below

1
On BEST ANSWER

Setting the width is the right approach, you just need proper alignment I think - use alignment = Alignment.CenterStart to see the start of your image and not the center like on your second picture:

Image(
    modifier = Modifier
        .height(142.dp)
        .width(58.dp)
        .padding(start = 20.dp, bottom = 20.dp)
        .rotate(TiltedImageRotation)
    painter = resourcePainter(R.drawable.image),
    contentScale = ContentScale.FillHeight,
    alignment = Alignment.CenterStart,
    contentDescription = null,
)

If you want to align with some offset, as suggested in the other answer, that can be done with Alignment too, and more easily:

val density = LocalDensity.current
Image(
    alignment = Alignment { _, _, _ ->
        val xOffset = density.run { 58.dp.toPx() / 2 }.roundToInt()
        IntOffset(x = -xOffset, 0)
    }
)
4
On

Clipping does not change dimensions of a Composable, it changes which section of a Composable is drawn. Without clip or clipToBounds you can draw anything out of a Composable via draw modifiers even if size of a Composable is zero.

As in example below with a shape with 200px

 val shape = GenericShape { size: Size, layoutDirection: LayoutDirection ->
     addRect(Rect(0f, 0f, 200f, 200f))
 }

we limit drawing area only to 200px while Box with Image in snippet below covers 200.dp, but not 200px. 200.dp is 200px * density of device.

enter image description here

Row(modifier = Modifier.border(2.dp, Color.Blue)) {
     Box(
         modifier = Modifier
             .clip(shape)
             .clickable {

             }
             .size(100.dp)
             .background(Color.Green)
     ) {
         Image(
             modifier = Modifier.size(100.dp),
             painter = painterResource(id = R.drawable.landscape1),
             contentScale = ContentScale.FillBounds,
             contentDescription = null
         )
     }

     Box(
         modifier = Modifier
             .size(100.dp)
             .background(Color.Yellow)
     )
 }

Limiting drawing to size while drawing only portion can be achieved in any way there is no perfect or key to this.

You can do it by using Modifier.layout{} by measuring a Placeable full size while placing it with size to crop such as

modifier = Modifier
    .clipToBounds()
    .layout { measurable, constraints ->
        val width = (58 * density).toInt()

        val placeable = measurable.measure(
            constraints
        )

        layout(width, placeable.height) {
            placeable.place(0, 0)
        }
    }

And since we clip the area out of layout's width we only have 58.dp composable that draws from (0,0) to position desired.

If you measure the composable with width specified above you need to also change alignment = Alignment.TopCenter because image uses Alignment.Center by default. Adding aligment to second Image will fix the issue.

enter image description here

Image(
    modifier = Modifier.height(142.dp),
    painter = painterResource(R.drawable.landscape1),
    contentScale = ContentScale.FillHeight,
    contentDescription = null,
)

Row(
    modifier = Modifier.border(2.dp, Color.Green)
) {
    Image(
        modifier = Modifier
            .layout { measurable, constraints ->
                val width = (58 * density).toInt()

                val placeable = measurable.measure(
                    constraints.copy(
                        minWidth = width,
                        maxWidth = width
                    )
                )

                layout(placeable.width, placeable.height) {
                    placeable.place(0, 0)
                }
            }
            .height(142.dp),
        painter = painterResource(R.drawable.landscape1),
        contentScale = ContentScale.FillHeight,
        contentDescription = null,
    )
    Text(text = "Some Text After Image")
}

Row(
    modifier = Modifier.border(2.dp, Color.Blue)
) {
    Image(
        modifier = Modifier
            .clipToBounds()
            .layout { measurable, constraints ->
                val width = (58 * density).toInt()

                val placeable = measurable.measure(
                    constraints
                )

                layout(width, placeable.height) {
                    placeable.place(0, 0)
                }
            }
            .height(142.dp),
        painter = painterResource(R.drawable.landscape1),
        contentScale = ContentScale.FillHeight,
        contentDescription = null,
    )
    Text(text = "Some Text After Image")
}

You can also use Canvas or Modifier.drawBehind to achieve same result . The thing to consider we clip to rect in desired size while drawing intrinsic width which is the width of actual Painter or Bitmap while we get height from Composable.

val painter = painterResource(R.drawable.landscape1)

Row(
    modifier = Modifier.border(2.dp, Color.Yellow)
) {
    Box(
        modifier = Modifier
            .width(58.dp)
            .drawBehind {
                clipRect(
                    left = 0f,
                    right = width
                ) {

                    with(painter) {
                        draw(size = Size(painter.intrinsicSize.width, size.height))
                    }
                }
            }
            .height(142.dp),
    )
    Text(text = "Some Text After Image")