androidkotlinandroid-jetpack-composeandroid-jetpack-compose-material3

Shapes in Jetpack compose android


I'm trying to create a custom card in Jetpack Compose that has a specific shape. I want the card to resemble a particular shape (as shown in the image below) with rounded corners and a custom contour.

I've started with the code below, but I'm unsure how to adjust the curves and the shape to match the image exactly. Here’s the target card shape image:

Custom Card

How can I make a background similar to the picture?

Here is my code, I have tried but couldn't achieve similar shape.

@Composable
fun CustomCard(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit,
) {
    val radius = with(LocalDensity.current) { 19.dp.toPx() }
    val cornerRadiusDp = 80.dp
    val cornerRadius = with(LocalDensity.current) { cornerRadiusDp.toPx() }

    val padding = 10.dp

    // Create a custom shape
    val shape = remember {
        GenericShape { size, _ ->
            val width = size.width
            val height = size.height

            moveTo(0f, radius)
            quadraticTo(0f, 0f, radius, 0f)
            lineTo(width - cornerRadius - radius, 0f)
            quadraticTo(width - cornerRadius, 0f, width - cornerRadius, radius)

            quadraticTo(width - 1.1f*cornerRadius, 1.1f*cornerRadius, width - radius, cornerRadius)

            quadraticTo(width, cornerRadius, width, cornerRadius + radius)

            lineTo(width, height - radius)
            quadraticTo(width, height, width - radius, height)

            lineTo(radius, height)
            quadraticTo(0f, height, 0f, height - radius)

            lineTo(0f, radius)
        }
    }

    Box(modifier = modifier) {
        Card(
            modifier = Modifier.fillMaxSize(),
            shape = shape,
            colors = CardDefaults.cardColors(
                containerColor = Color(0xFFBFE8FB)
            )
        ) {
            content()
        }

        Box(
            modifier = Modifier
                .size(cornerRadiusDp - padding)
                .align(Alignment.TopEnd)
                .background(
                    color = Color.Blue,
                    shape = CircleShape
                )
        ){
            Text(
                text = "1",
                color = Color.White,
                modifier = Modifier.align(Alignment.Center)
            )
        }
    }
}

@Preview
@Composable
fun ContextCardPreview() {
    Column(modifier = Modifier.background(Color.Red)){
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            CustomCard(modifier = Modifier
                .width(200.dp)
                .height(180.dp)
            ) {

                val price = buildAnnotatedString {
                    withStyle(SpanStyle(color = Color.Green)) { append("$") }
                    append("0.00")
                }

                val labelStyle = remember {
                    TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Light)
                }

                val priceStyle = remember {
                    TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold)
                }

                Column(
                    modifier = Modifier.padding(10.dp)
                ) {
                    Text(text = "Text", fontSize = 18.sp)
                    //Text(text = price, style = priceStyle)

                    Spacer(modifier = Modifier.weight(0.8f))

                    Text(text = "Text", style = labelStyle)

                    Spacer(modifier = Modifier.weight(0.5f))
                    Text(text = price, style = priceStyle)
                    Spacer(modifier = Modifier.weight(0.04f))
                }
            }
        }

    }
}

Solution

  • This is an improved version of this answer, which gives you even more control over the outline of the shape.

    /**
     * cornerRadius : control the corner radius
     * gapCornerRadius : radius of the gap corner
     * gapSize : size of the gap
     * padding : padding between gap and button circle
     * concavityFactor : control the concavity of the gap
     */
    @Composable
    fun CustomCard(
        modifier: Modifier = Modifier,
        cornerRadius : Dp = 20.dp,
        gapCornerRadius : Dp = 30.dp,
        gapSize : Dp = 80.dp,
        padding: Dp = 10.dp,
        concavityFactor: Float = 1.05f,
        content: @Composable () -> Unit,
    ) {
        val cornerRadiusPx = with(LocalDensity.current) { cornerRadius.toPx() }
    
        val gapCornerRadiusPx = with(LocalDensity.current) { gapCornerRadius.toPx() }
    
        val gapSizePx = with(LocalDensity.current) { gapSize.toPx() }
    
        // Create a custom shape
        val shape = remember {
            GenericShape { size, _ ->
                val width = size.width
                val height = size.height
    
                moveTo(0f, cornerRadiusPx)
                quadraticTo(0f, 0f, cornerRadiusPx, 0f)
    
                lineTo(width - gapSizePx - gapCornerRadiusPx, 0f)
                quadraticTo(width - gapSizePx, 0f, width - gapSizePx, gapCornerRadiusPx)
    
                quadraticTo(width - concavityFactor*gapSizePx, concavityFactor*gapSizePx, width - gapCornerRadiusPx, gapSizePx)
    
                quadraticTo(width, gapSizePx, width, gapSizePx + gapCornerRadiusPx)
    
                lineTo(width, height - cornerRadiusPx)
                quadraticTo(width, height, width - cornerRadiusPx, height)
    
                lineTo(cornerRadiusPx, height)
                quadraticTo(0f, height, 0f, height - cornerRadiusPx)
    
                lineTo(0f, cornerRadiusPx)
            }
        }
    
        Box(modifier = modifier) {
            Card(
                modifier = Modifier.fillMaxSize(),
                shape = shape,
                colors = CardDefaults.cardColors(
                    containerColor = Color(0xFFBFE8FB)
                )
            ) {
                content()
            }
    
            Box(
                modifier = Modifier
                    .size(gapSize - padding)
                    .align(Alignment.TopEnd)
                    .background(
                        color = Color.Blue,
                        shape = CircleShape
                    )
            ){
                Text(
                    text = "1",
                    color = Color.White,
                    modifier = Modifier.align(Alignment.Center)
                )
            }
        }
    }
    

    You can play with the CustomCard parameters to see the effect of each argument. This way you can use whatever values ​​you like.

    Here is an image to see each parameter visually:

    enter image description here