4

I want to add a border to the top half of the Card component that has a corner radius (10dp). So, only the bottom part is missing rest of the card has a stroke of 1dp. (kind of like U and inverted U)

And I want to do the same for card that has a bottom corner radius and the top part is missing.

I tried to design a custom shape but it's not respecting the shape of the card.

// this is just a line but it doesn't respect the card corner radius? 

private fun createShape(thickness: Float) : Shape {
    return GenericShape { size, _ ->
        moveTo(0f,0f)
        lineTo(0f, size.height)
        lineTo(thickness, size.height)
        lineTo(thickness, 0f)
        lineTo(0f, thickness)
    }
}


val thickness = with(LocalDensity.current) {
    1.dp.toPx()
}
Card(
    shape = RoundedCornerShape(topEnd = 10.dp, topStart = 10.dp, bottomEnd = 0.dp, bottomStart = 0.dp),
    modifier = Modifier
        .border(BorderStroke(width = 1.dp, color = Color.Black), createShape(thickness))

) {
    ...
}
user3354265
  • 777
  • 1
  • 10
  • 26

1 Answers1

4

Border doesn't seem to allow open shapes, forces shape to be closed even if you draw 3 lines because of that you need to use Modifier.drawBehind or Modifier.drawWithContent.

enter image description here

Created a Modifier that draws u shaped border as

fun Modifier.semiBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed(
    factory = {
        val density = LocalDensity.current
        val strokeWidthPx = density.run { strokeWidth.toPx() }
        val cornerRadius = density.run { cornerRadiusDp.toPx() }

        Modifier.drawBehind {
            val width = size.width
            val height = size.height

            drawLine(
                color = color,
                start = Offset(x = 0f, y = height),
                end = Offset(x = 0f, y = cornerRadius),
                strokeWidth = strokeWidthPx
            )

            // Top left arc
            drawArc(
                color = color,
                startAngle = 180f,
                sweepAngle = 90f,
                useCenter = false,
                topLeft = Offset.Zero,
                size = Size(cornerRadius * 2, cornerRadius * 2),
                style = Stroke(width = strokeWidthPx)
            )


            drawLine(
                color = color,
                start = Offset(x = cornerRadius, y = 0f),
                end = Offset(x = width - cornerRadius, y = 0f),
                strokeWidth = strokeWidthPx
            )

            // Top right arc
            drawArc(
                color = color,
                startAngle = 270f,
                sweepAngle = 90f,
                useCenter = false,
                topLeft = Offset(x = width - cornerRadius * 2, y = 0f),
                size = Size(cornerRadius * 2, cornerRadius * 2),
                style = Stroke(width = strokeWidthPx)
            )

            drawLine(
                color = color,
                start = Offset(x = width, y = height),
                end = Offset(x = width, y = cornerRadius),
                strokeWidth = strokeWidthPx
            )
        }
    }
)

Usage

@Composable
private fun UShapeBorderSample() {
    Card(
        shape = RoundedCornerShape(
            topEnd = 10.dp,
            topStart = 10.dp,
            bottomEnd = 0.dp,
            bottomStart = 0.dp
        ),
        modifier = Modifier
            .semiBorder(1.dp, Color.Black, 10.dp)

    ) {
        Box(
            modifier = Modifier
                .size(150.dp)
                .background(Color.White),
            contentAlignment = Alignment.Center
        ) {
            Text("Hello World")
        }
    }

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

    Card(
        shape = RoundedCornerShape(
            topEnd = 20.dp,
            topStart = 20.dp,
            bottomEnd = 0.dp,
            bottomStart = 0.dp
        ),
        modifier = Modifier
            .semiBorder(1.dp, Color.Black, 20.dp)

    ) {
        Box(
            modifier = Modifier
                .size(150.dp)
                .background(Color.White),
            contentAlignment = Alignment.Center
        ) {
            Text("Hello World")
        }
    }
}

You need to use arc to create rounded corners. And when creating shape you don't pass thickness but radius of shape. Thickness is required when drawing border. What you draw is a rectangle with 1.dp width.

enter image description here

@Composable
private fun createShape(cornerRadius: Dp): Shape {

    val density = LocalDensity.current
    return GenericShape { size, _ ->

        val width = size.width
        val height = size.height
        val cornerRadiusPx = density.run { cornerRadius.toPx() }

        moveTo(0f, height)

        // Vertical line on left size
        lineTo(0f, cornerRadiusPx * 2)

        arcTo(
            rect = Rect(
                offset = Offset.Zero,
                size = Size(cornerRadiusPx * 2, cornerRadiusPx * 2)
            ),
            startAngleDegrees = 180f,
            sweepAngleDegrees = 90f,
            forceMoveTo = false
        )

        lineTo(width - cornerRadiusPx * 2, 0f)
        arcTo(
            rect = Rect(
                offset = Offset(width - cornerRadiusPx * 2, 0f),
                size = Size(cornerRadiusPx * 2, cornerRadiusPx * 2)
            ),
            startAngleDegrees = 270f,
            sweepAngleDegrees = 90f,
            forceMoveTo = false
        )
        // Vertical line on right size
        lineTo(width, height)

    }
}

Usage

@Composable
private fun UShapeBorderSample() {
    Card(
        shape = RoundedCornerShape(
            topEnd = 10.dp,
            topStart = 10.dp,
            bottomEnd = 0.dp,
            bottomStart = 0.dp
        ),
        modifier = Modifier
            .border(BorderStroke(width = 1.dp, color = Color.Black), createShape(10.dp))

    ) {
        Box(modifier = Modifier
            .size(200.dp)
            .background(Color.White),
            contentAlignment = Alignment.Center
        ) {
            Text("Hello World")
        }
    }
}

Border doesn't respect shape because it's a drawing but Card is a Box under the hood that uses shape with Modifier.clip() which itself is Modifier.graphicsLayer{clip} that applies operations on a layer.

You can check out this answer about clip and border for the difference.

https://stackoverflow.com/a/73091667/5457853

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • qq: `semiBorder` actually draws an inverted U shape. I want to draw both the shapes inverted as well as regular U. Would I need to maintain 2 different shapes or I can tweak the existing one to adapt both shapes? – user3354265 Dec 10 '22 at 20:29
  • Do you have an image of what you wish to achieve? From the description above it looks like a `RoundedCornerShape` which is a built-in shape as `RectangleShape`, `CircleShape`, and `CutCornerShape` – Thracian Dec 11 '22 at 05:46
  • Basically I want both U shape and inverted U shape. In some scenarios I want to have just U and in some it'll be inverted U. I can achieve it with maintaining 2 separates shapes but I was wondering if it can be achievable with just 1. – user3354265 Dec 11 '22 at 16:03
  • 1
    It's very easy to rotate what's drawn inside DrawScope. Add a boolean flag for rotation and wrap drawLine and drawArc inside rotate(angle){...//drawing here} as in these answers https://stackoverflow.com/a/74730613/5457853 and https://stackoverflow.com/a/74674284/5457853 – Thracian Dec 11 '22 at 20:39
  • OMG that's a life savior thanks a lot. – user3354265 Dec 12 '22 at 00:18