1

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,
    )
}
Basim Sherif
  • 5,384
  • 7
  • 48
  • 90

2 Answers2

4

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)
    }
)
Jan Bína
  • 3,447
  • 14
  • 16
  • Sorry, I didn't notice you removed the need for a custom shape in your answer. This should be the accepted answer. Thank you – Basim Sherif May 10 '23 at 21:11
2

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")
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Hey yeah, I can explain. The question was already answered with perfectly working code before, still you wanted to add something, so lets look at it. What does that lengthy `layout{}` modifier in your first code block does? I think it's exactly the same as `.width(58.dp)`, isn't it? Then there is the bonus offset example. Nice, I guess, but again, you can do that much simpler with alignment: `Alignment { IntOffset(x, y) }`. I don't mean to be rude, your answers are mostly good and I upvote them often, but here you just added some convoluted unnecessary code to already working answer IMO. – Jan Bína May 10 '23 at 16:37
  • It's not because you answered after me of course. I already explained the reason. You say "it removes the need for creating a shape" - so does my answer. The key to this question was `Alignment`, and that was already provided. You added lengthy `.layout {}` block that is nothing but unnecessary and the offset bonus, which is nice, but can be done much simpler, without the `.layout {}`. Again, it's not me "being intolerant for alternatives". Your alternative just makes something super simple (`alignment = CenterStart`) look quite complicated, therefore it's not a good answer in my opinion. – Jan Bína May 10 '23 at 16:59
  • There can be many ways to achieve same goal and adding 9 lines of code in total does not make anything complicated with `Modifier.layout{}`. You can do it in many ways. You can use a layout or canvas, clip with rect or whichever option you prefer. With all due respect, just because you used alignment it doesn't turn it into a key. Honestly, i'd rather seeing a layout answer over something more common as an alternative since you come across solutions with layout less often than anything else. There can be complex or simple alternatives and people can choose from. – Thracian May 10 '23 at 17:11
  • Yeah it seems we won't agree here. Replacing something as integral as `Modifier.weight` with 9 line `.layout{}` is definitely unnecessary and convoluted. "just because you used alignment" - **you** used it **too**! That's what makes your answer work - again, you just added something unnecessary on top of it. – Jan Bína May 10 '23 at 17:54