Doing some learning on shapes in compose and I’m r...
# compose
t
Doing some learning on shapes in compose and I’m running into a couple issues. I’m wondering if I could get advice on best practice here. I’m creating a Pin shape that has a set point size and a variable height and width based on content. So far what I’ve tried is creating a custom shape, but I’ve run into issues with the content in the shape. Here’s an example of what I mean ( and continued details in 🧵 ) :
Note how, because my custom background shape takes up a set amount of space at the bottom for the tip, the 8.dp of padding on both objects acts very different (because the padding is actually based off the bottom tip on the custom background shape)
Copy code
@Composable
fun Pin() {
    Column(modifier = Modifier.fillMaxSize()) {

        val pinShape = PinShape(
            cornerRadiusInDp = 8.dp,
            tipHeight = 8.dp,
            tipWidth = 16.dp
        )

        Row(
            modifier = Modifier
                .background(
                    color = Color.Blue, shape = RoundedCornerShape(3.dp)
                )
                .clip(shape = RoundedCornerShape(3.dp))
                .padding(8.dp)
        ) {
            Icon(
                imageVector = Icons.Rounded.Face, contentDescription = "face",
                modifier = Modifier.size(24.dp), tint = Color.White
            )
            Spacer(modifier = Modifier.width(8.dp))
            Text(text = "1328", fontSize = 16.sp, color = Color.White)
        }
        Row(
            modifier = Modifier
                .background(
                    color = Color.Blue, shape = pinShape
                )
                .clip(pinShape)
                .padding(8.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Icon(
                imageVector = Icons.Rounded.Face, contentDescription = "face",
                modifier = Modifier.size(24.dp), tint = Color.White
            )
            Spacer(modifier = Modifier.width(8.dp))
            Text(text = "1328", fontSize = 16.sp, color = Color.White)
        }
    }
}
Here are how those 2 objects are formed in compose ^
And here is how I made the custom shape in compose:
Copy code
class PinShape(private val cornerRadiusInDp: Dp, val tipHeight: Dp, val tipWidth: Dp) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {

        val thInPx = tipHeight.value * density.density
        val twInPx = tipWidth.value * density.density
        val cornerRadius = cornerRadiusInDp.value * density.density

        return Outline.Generic(
            Path().apply {
                addRoundRect(RoundRect(
                    top = 0f + thInPx,
                    bottom = size.height - thInPx,
                    left = 0f,
                    right = size.width,
                    cornerRadius = CornerRadius(cornerRadius)))
                moveTo((size.width / 2f) + (twInPx / 2f), size.height - thInPx)
                lineTo((size.width / 2f), size.height)
                lineTo((size.width / 2f) - (twInPx / 2f), size.height - thInPx)
            }
        )
    }
}
Additionally, I’ll need to draw a border around the entire shape, and as it currently stands, it’s not drawing the border properly
image.png
Copy code
modifier = Modifier
                .background(
                    color = Color.Blue, shape = pinShape
                )
                .clip(pinShape)
                .border(width = 2.dp, color = Color.Red, shape = pinShape)
                .padding(8.dp),
This honestly seems like a relatively simple thing to draw, but compose is making it a pain, so I’ve come looking for better ways to handle this.
r
That's because you've defined your shape as two contours
The move to creates a new contour
1
The so the border rendering is correct in that regard
You need to either create a union of the two contours or not use addRoundRect and do your own line to/quadTo etc.
t
Ah yeah I figured that was likely the issue with the border. So that answers that question, but as far as the other question. Basically if I want the text, padding, etc to respect the shape in the top part, regardless of the point
k
Then you’ll need to set the padding according to where the pointy thing is
t
So basically just add a bottom padding that is equal to the point thing’s size
k
There’s no magic awareness on the part of the child content (text) on how the parent draws itself
t
I still feel there may be a cleaner way, but since I make the top shape start down 8.dp to keep the content aligned in the center since the pointy thing is 8.dp in size, then if i just make sure the vertical padding has 8.dp + my desired padding, then it works like a normal container.
Copy code
Row(
    modifier = Modifier
        .background(
            color = Color.Blue, shape = pinShape
        )
        .clip(pinShape)
        .padding(top = 16.dp, bottom = 16.dp, start = 8.dp, end = 8.dp),

    verticalAlignment = Alignment.CenterVertically
)
k
Sometimes it needs a bit more tweaking to appear visually centered, which is not always the same as “mathematically” centered
👆 2
t
Yeah, that makes sense. As far as best practice standpoint, do people typically prefer to Draw on a Canvas or use Custom Shapes for this sort of thing?
k
No single solution is always the right one. Depends on the complexity of the visuals.
t
Copy code
class PinShape(private val cornerRadiusInDp: Dp, val tipHeight: Dp, val tipWidth: Dp) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {

        val thInPx = tipHeight.value * density.density
        val twInPx = tipWidth.value * density.density
        val cornerRadius = cornerRadiusInDp.value * density.density

        val rectangle = Path().apply {
            addRoundRect(RoundRect(
                top = 0f + thInPx,
                bottom = size.height - thInPx,
                left = 0f,
                right = size.width,
                cornerRadius = CornerRadius(cornerRadius)))
        }

        val point = Path().apply {
            moveTo((size.width / 2f) + (twInPx / 2f), size.height - thInPx)
            lineTo((size.width / 2f), size.height)
            lineTo((size.width / 2f) - (twInPx / 2f), size.height - thInPx)
        }

        return Outline.Generic(
            Path.combine(PathOperation.Union, rectangle, point)
        )
    }
}
Ended up doing the combine of the paths in the shape and the border works! Thanks @romainguy & @Kirill Grouchnikov
👍 2
👍🏾 1