I'm trying to draw partial or one-side open rect rounded border to achieve this effect:

After playing around a bit I got this:

This is done via:
RoundedCornerShape(topStartPercent = 50, bottomStartPercent = 50) // start
RoundedCornerShape(topEndPercent = 50, bottomEndPercent = 50) // end
RectangleShape // for middle
What I want is to remove the connecting vertical lines between the cells.
You can draw border behind Composables using Modifier.drawBehind and checking if your Composable is at the start, center or end
enum class BorderOrder {
Start, Center, End
}
fun Modifier.drawSegmentedBorder(
strokeWidth: Dp,
color: Color,
cornerPercent: Int,
borderOrder: BorderOrder,
drawDivider: Boolean = false
) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height
val cornerRadius = height * cornerPercent / 100
when (borderOrder) {
BorderOrder.Start -> {
drawLine(
color = color,
start = Offset(x = width, y = 0f),
end = Offset(x = cornerRadius, y = 0f),
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 = 0f, y = cornerRadius),
end = Offset(x = 0f, y = height - cornerRadius),
strokeWidth = strokeWidthPx
)
// Bottom left arc
drawArc(
color = color,
startAngle = 90f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(x = 0f, y = height - 2 * cornerRadius),
size = Size(cornerRadius * 2, cornerRadius * 2),
style = Stroke(width = strokeWidthPx)
)
drawLine(
color = color,
start = Offset(x = cornerRadius, y = height),
end = Offset(x = width, y = height),
strokeWidth = strokeWidthPx
)
}
BorderOrder.Center -> {
drawLine(
color = color,
start = Offset(x = 0f, y = 0f),
end = Offset(x = width, y = 0f),
strokeWidth = strokeWidthPx
)
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = width, y = height),
strokeWidth = strokeWidthPx
)
if (drawDivider) {
drawLine(
color = color,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = height),
strokeWidth = strokeWidthPx
)
}
}
else -> {
if (drawDivider) {
drawLine(
color = color,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = height),
strokeWidth = strokeWidthPx
)
}
drawLine(
color = color,
start = Offset(x = 0f, 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 = cornerRadius),
end = Offset(x = width, y = height - cornerRadius),
strokeWidth = strokeWidthPx
)
// Bottom right arc
drawArc(
color = color,
startAngle = 0f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(
x = width - 2 * cornerRadius,
y = height - 2 * cornerRadius
),
size = Size(cornerRadius * 2, cornerRadius * 2),
style = Stroke(width = strokeWidthPx)
)
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = width -cornerRadius, y = height),
strokeWidth = strokeWidthPx
)
}
}
}
}
)
Usage
@Composable
private fun SegmentedBorderSample() {
Row {
repeat(3) {
val order = when (it) {
0 -> BorderOrder.Start
2 -> BorderOrder.End
else -> BorderOrder.Center
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(40.dp)
.drawSegmentedBorder(
strokeWidth = 2.dp,
color = Color.Green,
borderOrder = order,
cornerPercent = 40,
drawDivider = false
)
.padding(4.dp)
) {
Text(text = "$it")
}
}
}
Row {
repeat(4) {
val order = when (it) {
0 -> BorderOrder.Start
3 -> BorderOrder.End
else -> BorderOrder.Center
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(40.dp)
.drawSegmentedBorder(
strokeWidth = 2.dp,
color = Color.Cyan,
borderOrder = order,
cornerPercent = 50,
drawDivider = true
)
.padding(4.dp)
) {
Text(text = "$it")
}
}
}
}
Result

You can use the offset modifier to avoid the double border:
Something like:
val itemsList = (0..4).toList()
Row() {
itemsList.forEachIndexed { index, item ->
OutlinedButton(
onClick = { /** do something */},
modifier = when (index) {
0 ->
Modifier
.offset(0.dp, 0.dp)
else ->
Modifier
.offset((-1 * index).dp, 0.dp)
},
shape = when (index) {
// left outer button
0 -> RoundedCornerShape(topStart = cornerRadius, topEnd = 0.dp, bottomStart = cornerRadius, bottomEnd = 0.dp)
// right outer button
itemsList.size - 1 -> RoundedCornerShape(topStart = 0.dp, topEnd = cornerRadius, bottomStart = 0.dp, bottomEnd = cornerRadius)
// middle button
else -> RoundedCornerShape(0.dp)
},
border = BorderStroke(1.dp, Blue500)
) {}
}
}

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With