2

I'm trying to achieve the below card view arc shape on the card view border/stroke. Already tried to search on google but didn't find any relevant answer that suits with requirements.

Any lead or help will be appreciated.

enter image description here

Swapnil Musale
  • 182
  • 1
  • 15
  • 3
    you can customize this by adding a custom shape via a path. check this https://juliensalvi.medium.com/custom-shape-with-jetpack-compose-1cb48a991d42 – Raghunandan Nov 01 '22 at 17:11
  • 2
    just add https://medium.com/swlh/curved-cut-out-bottom-navigation-with-animation-in-android-c630c867958c the bottom curve is what you need to customize. its a bezier curve. – Raghunandan Nov 03 '22 at 06:21

3 Answers3

9

Answer from Cirilo Bido and Raghunandan is good place to start, you round corners of rectangle with arcTo but you can't draw curved edges on top of clipped out shape. You need to use cubicTo to draw rounded edge and curve to clip out bottom shape

   val shape = GenericShape {size: Size, layoutDirection: LayoutDirection ->
                            // draw cubic on left and right sides for button space
                            cubicTo()
                        }

enter image description here

You can check out this answer for drawing with cubic to. By combining both you can draw that path.

Jetpack Compose: How to draw a path / line like this

I created this path based on article shared by
Raghunandan initially, even though that is amazing answer for animating BottomBar it doesn't create a rounded shape if you look closely, at the bottom it's creating a triangular shape at the bottom instead of rounded one and shape OP requires and in article is also different.

So i used sliders to create bezier from the link i shared above. It's available as tutorial here too. Still it can be tweaked to more precise shape if you wish to.

enter image description here

I used x0, y0 as reference point to set control points and created this Path extension function.

fun Path.roundedRectanglePath(
    size: Size,
    cornerRadius: Float,
    fabRadius: Float,
) {

    val centerX = size.width / 2
    val x0 = centerX - fabRadius * 1.15f
    val y0 = 0f

    // offset of the first control point (top part)
    val topControlX = x0 + fabRadius * .5f
    val topControlY = y0

    // offset of the second control point (bottom part)
    val bottomControlX = x0
    val bottomControlY = y0 + fabRadius

    // first curve
    // set the starting point of the curve (P2)
    val firstCurveStart = Offset(x0, y0)

    // set the end point for the first curve (P3)
    val firstCurveEnd = Offset(centerX, fabRadius * 1f)

    // set the first control point (C1)
    val firstCurveControlPoint1 = Offset(
        x = topControlX,
        y = topControlY
    )

    // set the second control point (C2)
    val firstCurveControlPoint2 = Offset(
        x = bottomControlX,
        y = bottomControlY
    )


    // second curve
    // end of first curve and start of second curve is the same (P3)
    val secondCurveStart = Offset(
        x = firstCurveEnd.x,
        y = firstCurveEnd.y
    )

    // end of the second curve (P4)
    val secondCurveEnd = Offset(
        x = centerX + fabRadius * 1.15f,
        y = 0f
    )

    // set the first control point of second curve (C4)
    val secondCurveControlPoint1 = Offset(
        x = secondCurveStart.x + fabRadius,
        y = bottomControlY
    )

    // set the second control point (C3)
    val secondCurveControlPoint2 = Offset(
        x = secondCurveEnd.x - fabRadius / 2,
        y = topControlY
    )


    // Top left arc
    val radius = cornerRadius * 2

    arcTo(
        rect = Rect(
            left = 0f,
            top = 0f,
            right = radius,
            bottom = radius
        ),
        startAngleDegrees = 180.0f,
        sweepAngleDegrees = 90.0f,
        forceMoveTo = false
    )



    lineTo(x = firstCurveStart.x, y = firstCurveStart.y)

    // bezier curve with (P2, C1, C2, P3)
    cubicTo(
        x1 = firstCurveControlPoint1.x,
        y1 = firstCurveControlPoint1.y,
        x2 = firstCurveControlPoint2.x,
        y2 = firstCurveControlPoint2.y,
        x3 = firstCurveEnd.x,
        y3 = firstCurveEnd.y
    )

    // bezier curve with (P3, C4, C3, P4)
    cubicTo(
        x1 = secondCurveControlPoint1.x,
        y1 = secondCurveControlPoint1.y,
        x2 = secondCurveControlPoint2.x,
        y2 = secondCurveControlPoint2.y,
        x3 = secondCurveEnd.x,
        y3 = secondCurveEnd.y
    )

    lineTo(x = size.width - cornerRadius, y = 0f)

    // Top right arc
    arcTo(
        rect = Rect(
            left = size.width - radius,
            top = 0f,
            right = size.width,
            bottom = radius
        ),
        startAngleDegrees = -90.0f,
        sweepAngleDegrees = 90.0f,
        forceMoveTo = false
    )

    lineTo(x = 0f + size.width, y = size.height - cornerRadius)

    // Bottom right arc
    arcTo(
        rect = Rect(
            left = size.width - radius,
            top = size.height - radius,
            right = size.width,
            bottom = size.height
        ),
        startAngleDegrees = 0f,
        sweepAngleDegrees = 90.0f,
        forceMoveTo = false
    )

    lineTo(x = cornerRadius, y = size.height)

    // Bottom left arc
    arcTo(
        rect = Rect(
            left = 0f,
            top = size.height - radius,
            right = radius,
            bottom = size.height
        ),
        startAngleDegrees = 90.0f,
        sweepAngleDegrees = 90.0f,
        forceMoveTo = false
    )

    lineTo(x = 0f, y = cornerRadius)
    close()
}

Composable that uses this shape

@Composable
private fun CustomArcShape(
    modifier: Modifier,
    elevation: Dp = 4.dp,
    color: Color = MaterialTheme.colorScheme.surface,
    contentColor: Color = contentColorFor(color),
    content: @Composable () -> Unit
) {

    val diameter = 60.dp
    val radiusDp = diameter / 2

    val cornerRadiusDp = 10.dp

    val density = LocalDensity.current
    val cutoutRadius = density.run { radiusDp.toPx() }
    val cornerRadius = density.run { cornerRadiusDp.toPx() }

    val shape = remember {
        GenericShape { size: Size, layoutDirection: LayoutDirection ->
            this.roundedRectanglePath(
                size = size,
                cornerRadius = cornerRadius,
                fabRadius = cutoutRadius * 2
            )
        }
    }

    Spacer(modifier = Modifier.height(diameter / 2))

    Box(contentAlignment = Alignment.TopCenter) {
        FloatingActionButton(
            shape = CircleShape,
            containerColor = Color(0xffD32F2F),
            modifier = Modifier
                .offset(y = -diameter / 5)
                .size(diameter)
                .drawBehind {
                    drawCircle(
                        Color.Red.copy(.5f),
                        radius = 1.3f * size.width / 2
                    )

                    drawCircle(
                        Color.Red.copy(.3f),
                        radius = 1.5f * size.width / 2
                    )

                }
                .align(Alignment.TopCenter),
            onClick = { /*TODO*/ }
        ) {
            Icon(
                tint = Color.White,
                imageVector = Icons.Filled.Close,
                contentDescription = "Close"
            )
        }

        Surface(
            modifier = modifier,
            shape = shape,
            shadowElevation = elevation,
            color = color,
            contentColor = contentColor
        ) {
            Column {
                Spacer(modifier = Modifier.height(diameter))
                content()

            }
        }
    }
}

And demonstration

@Composable
private fun CustomArcShapeSample() {
    Column(
        modifier = Modifier
            .fillMaxSize()
    ) {

        CustomArcShape(
            modifier = Modifier
                .padding(10.dp)
                .fillMaxWidth()
                .height(250.dp)
        ) {
            Column(
                modifier = Modifier.fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    "Payment Failed",
                    color = MaterialTheme.colorScheme.error,
                    fontWeight = FontWeight.Bold
                )
                Spacer(modifier = Modifier.height(10.dp))
                Text("Sorry !", fontSize = 24.sp, fontWeight = FontWeight.Bold)
                Spacer(modifier = Modifier.height(10.dp))
                Text("Your transfer to bank failed", color = Color.LightGray)
            }
        }

        Spacer(modifier = Modifier.height(40.dp))

        CustomArcShape(
            modifier = Modifier
                .padding(10.dp)
                .fillMaxWidth()
                .height(250.dp)
        ) {
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .border(1.dp, Color.Green),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    "Payment Failed",
                    color = MaterialTheme.colorScheme.error,
                    fontWeight = FontWeight.Bold
                )
                Spacer(modifier = Modifier.height(10.dp))
                Text("Sorry !", fontSize = 24.sp, fontWeight = FontWeight.Bold)
                Spacer(modifier = Modifier.height(10.dp))
                Text("Your transfer to bank failed", color = Color.LightGray)
            }
        }

    }
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
0

You probably need to draw that arc in a custom composable, I found this article that can help you to understand the process of drawing in compose!

Cirilo Bido
  • 179
  • 1
  • 2