I want to add border on bottom of the layout. I know i can use Divider composable but i just want to learn how to draw a border.
Currently, I can add border for all sides which is not what I want.
Row(
modifier = Modifier
.border(border = BorderStroke(width = 1.dp, Color.LightGray))
) {
TextField(value = "", onValueChange = {}, modifier = Modifier.weight(1f))
Switch(checked = true, onCheckedChange = {})
Icon(Icons.Filled.Close, "Remove", tint = Color.Gray)
}
You can use the drawBehind modifier to draw a line.
Something like:
Row(
modifier = Modifier
.drawBehind {
val strokeWidth = indicatorWidth.value * density
val y = size.height - strokeWidth / 2
drawLine(
Color.LightGray,
Offset(0f, y),
Offset(size.width, y),
strokeWidth
)
}){
//....
}
If you prefer you can build your custom Modifier with the same code above
fun Modifier.bottomBorder(strokeWidth: Dp, color: Color) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height - strokeWidthPx/2
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = width , y = height),
strokeWidth = strokeWidthPx
)
}
}
)
and then just apply it:
Row(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth()
.bottomBorder(1.dp, DarkGray)
){
//Row content
}
You can draw a line in a draw scope. In my opinion, a divider looks cleaner in code.
Row(modifier = Modifier
.drawWithContent {
drawContent()
clipRect { // Not needed if you do not care about painting half stroke outside
val strokeWidth = Stroke.DefaultMiter
val y = size.height // - strokeWidth
// if the whole line should be inside component
drawLine(
brush = SolidColor(Color.Red),
strokeWidth = strokeWidth,
cap = StrokeCap.Square,
start = Offset.Zero.copy(y = y),
end = Offset(x = size.width, y = y)
)
}
}
) {
Text("test")
}
Yeah this oughta do it:-
#Suppress("UnnecessaryComposedModifier")
fun Modifier.topRectBorder(width: Dp = Dp.Hairline, brush: Brush = SolidColor(Color.Black)): Modifier = composed(
factory = {
this.then(
Modifier.drawWithCache {
onDrawWithContent {
drawContent()
drawLine(brush, Offset(width.value, 0f), Offset(size.width - width.value, 0f))
}
}
)
},
inspectorInfo = debugInspectorInfo {
name = "border"
properties["width"] = width
if (brush is SolidColor) {
properties["color"] = brush.value
value = brush.value
} else {
properties["brush"] = brush
}
properties["shape"] = RectangleShape
}
)
You can define a rectangular Shape on the bottom of your element, using the bottom line thickness as parameter:
private fun getBottomLineShape(bottomLineThickness: Float) : Shape {
return GenericShape { size, _ ->
// 1) Bottom-left corner
moveTo(0f, size.height)
// 2) Bottom-right corner
lineTo(size.width, size.height)
// 3) Top-right corner
lineTo(size.width, size.height - bottomLineThickness)
// 4) Top-left corner
lineTo(0f, size.height - bottomLineThickness)
}
}
And then use it in the border modifier like this:
val lineThickness = with(LocalDensity.current) {[desired_thickness_in_dp].toPx()}
Row(
modifier = Modifier
.height(rowHeight)
.border(width = lineThickness,
color = Color.Black,
shape = getBottomLineShape(lineThickness))
) {
// Stuff in the row
}
Using a "Divider" worked for me,
Column {
Divider (
color = Color.White,
modifier = Modifier
.height(1.dp)
.fillMaxHeight()
.fillMaxWidth()
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
) {
// Something else
}
}
Related
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))
) {
...
}
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.
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.
#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
if anyone can tell me how to draw this shape
with an inside text, id greatly appreciate it.
Is there a way to do it in regular xml or any android api
You can use the Canvas to draw any shapes. Below is a sample implementation as per your required shape which you can customize.
Box {
Canvas(
modifier = Modifier
.size(200.dp)
.padding(40.dp)
) {
val trianglePath = Path().let {
it.moveTo(this.size.width * .40f, 0f)
it.lineTo(this.size.width * .50f, -30f)
it.lineTo(this.size.width * .60f, 0f)
it.close()
it
}
drawRoundRect(
Color.LightGray,
size = Size(this.size.width, this.size.height * 0.95f),
cornerRadius = CornerRadius(60f)
)
drawPath(
path = trianglePath,
Color.LightGray,
)
}
}
Easiest way you can do it with shadow and border properties is to create a custom Shape.
If you draw to Canvas alone you will also need to draw shadow and border properties, and need to create offsets inside your Composables bounds.
With GenericShape you can create a Path with roundedRect and add a triangle to top with
fun getBubbleShape(
density: Density,
cornerRadius: Dp,
arrowWidth: Dp,
arrowHeight: Dp,
arrowOffset: Dp
): GenericShape {
val cornerRadiusPx: Float
val arrowWidthPx: Float
val arrowHeightPx: Float
val arrowOffsetPx: Float
with(density) {
cornerRadiusPx = cornerRadius.toPx()
arrowWidthPx = arrowWidth.toPx()
arrowHeightPx = arrowHeight.toPx()
arrowOffsetPx = arrowOffset.toPx()
}
return GenericShape { size: Size, layoutDirection: LayoutDirection ->
this.addRoundRect(
RoundRect(
rect = Rect(
offset = Offset(0f, arrowHeightPx),
size = Size(size.width, size.height - arrowHeightPx)
),
cornerRadius = CornerRadius(cornerRadiusPx, cornerRadiusPx)
)
)
moveTo(arrowOffsetPx, arrowHeightPx)
lineTo(arrowOffsetPx + arrowWidthPx / 2, 0f)
lineTo(arrowOffsetPx + arrowWidthPx, arrowHeightPx)
}
}
If you need to have borders instead of addRoundRect you will need arcs and lines to draw for connecting arrow and lines you can refer this answer for drawing rounded rectangle using arcs and lines. If you set border with current setup you will understand what i mean
Then use it as
#Composable
private fun BubbleShapeSample() {
val density = LocalDensity.current
val arrowHeight = 16.dp
val bubbleShape = remember {
getBubbleShape(
density = density,
cornerRadius = 12.dp,
arrowWidth = 20.dp,
arrowHeight = arrowHeight,
arrowOffset = 30.dp
)
}
Column(
modifier = Modifier
.shadow(5.dp, bubbleShape)
.background(Color.White)
.padding(8.dp)
) {
Spacer(modifier = Modifier.height(arrowHeight))
Row(modifier = Modifier.padding(12.dp)) {
Icon(
modifier = Modifier.size(60.dp),
imageVector = Icons.Default.NotificationsActive,
contentDescription = "",
tint = Color(0xffFFC107)
)
Spacer(modifier = Modifier.width(20.dp))
Text(
"Get updates\n" +
"on questions\n" +
"and answers",
fontSize = 20.sp
)
Spacer(modifier = Modifier.width(20.dp))
Icon(
imageVector = Icons.Default.Close,
contentDescription = ""
)
}
}
}
I am trying to make this simple view in Compose, but cant seem to get it right.
I have tried using dividers, and now switched to canvas, but cant seem to get correct result.
Row{
Line()
ClickableText(text = AnnotatedString("Show replies"),
modifier = Modifier.weight(1f),
onClick = { showReplies.value = true })
Line()
}
#Composable
fun RowScope.Line() {
Canvas(modifier = Modifier.fillMaxSize().weight(1f)) {
// Fetching width and height for
// setting start x and end y
val canvasWidth = size.width
val canvasHeight = size.height
// drawing a line between start(x,y) and end(x,y)
drawLine(
start = Offset(x = 0f, y = canvasHeight/2),
end = Offset(x = canvasWidth, y = canvasHeight/2),
color = Color.Red,
strokeWidth = 5F
)
}
}
I have played with arragments, weights, sizes, but always get some quirky result.
Try this:
#Composable
fun Replies() {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier
.height(2.dp)
.weight(1f)
.background(Color.Gray)) {}
ClickableText(
text = AnnotatedString("Show replies"), onClick = {}, modifier = Modifier.weight(1f),
style = TextStyle(
textAlign = TextAlign.Center
),
)
Box(modifier = Modifier
.height(2.dp)
.weight(1f)
.background(Color.Gray)) {}
}
}
I'm trying to achieve this custom shape with Compose
But for some reason the separator offseted circle is drawn with a dotted line and here is the code
#Preview
#Composable
private fun ReceiptSeparator () {
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp) ,
verticalAlignment = Alignment.CenterVertically ,) {
Box(
modifier = Modifier
.requiredSize(50.dp)
.background(Color.White)
.offset(-40.dp)
.clip(CircleShape)
.border(BorderStroke(2.dp, Color.Gray))
){}
Box(
Modifier
.height(1.dp)
.requiredWidth(250.dp)
.weight(3f)
.background(Color.Gray, shape = DottedShape(step = 20.dp))
){}
Box(
modifier = Modifier
.offset(40.dp)
.clip(CircleShape)
.border(BorderStroke(2.dp, Color.Gray))
.background(Color.White)
.size(50.dp)
){}
}
}
Why the circle is drawn with a dotted line and how to achieve this shape correctly?
Your circle is not drawn correctly, because Modifier.border draws a rectangle border by default, and then you clip it with your Modifier.clip. Instead, if you need to apply shape to the border, you need to pass the shape into Modifier.border, like this:
.border(BorderStroke(2.dp, Color.Gray), shape = CircleShape)
But this won't solve your problem. To draw the shadow correctly like shown in your image, you need to apply a custom Shape to your container.
You can use Modifier.onGloballyPositioned to get position of your cutoffs:
var separatorOffsetY by remember { mutableStateOf<Float?>(null) }
val cornerRadius = 20.dp
Card(
shape = RoundedCutoutShape(separatorOffsetY, cornerRadius),
backgroundColor = Color.White,
modifier = Modifier.padding(10.dp)
) {
Column {
Box(modifier = Modifier.height(200.dp))
Box(
Modifier
.padding(horizontal = cornerRadius)
.height(1.dp)
.requiredWidth(250.dp)
// DottedShape is taken from this answer:
// https://stackoverflow.com/a/68789205/3585796
.background(Color.Gray, shape = DottedShape(step = 20.dp))
.onGloballyPositioned {
separatorOffsetY = it.boundsInParent().center.y
}
)
Box(modifier = Modifier.height(50.dp))
}
}
Using this information you can create a shape like following:
class RoundedCutoutShape(
private val offsetY: Float?,
private val cornerRadiusDp: Dp,
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
) = Outline.Generic(run path#{
val cornerRadius = with(density) { cornerRadiusDp.toPx() }
val rect = Rect(Offset.Zero, size)
val mainPath = Path().apply {
addRoundRect(RoundRect(rect, CornerRadius(cornerRadius)))
}
if (offsetY == null) return#path mainPath
val cutoutPath = Path().apply {
val circleSize = Size(cornerRadius, cornerRadius) * 2f
val visiblePart = 0.25f
val leftOval = Rect(
offset = Offset(
x = 0 - circleSize.width * (1 - visiblePart),
y = offsetY - circleSize.height / 2
),
size = circleSize
)
val rightOval = Rect(
offset = Offset(
x = rect.width - circleSize.width * visiblePart,
y = offsetY - circleSize.height / 2
),
size = circleSize
)
addOval(leftOval)
addOval(rightOval)
}
return#path Path().apply {
op(mainPath, cutoutPath, PathOperation.Difference)
}
})
}
Result:
Get rid of:
shape = DottedShape(step = 20.dp)
I can easily create a normal border using the Modifier.border() but how to create a dashed border as shown in the image below.
There isn't a parameter in Modifier.border() to achieve a dashed path.
However you can use a DrawScope to draw a dashed Path using PathEffect.dashPathEffect.
Something like:
val stroke = Stroke(width = 2f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
You can draw it using the drawBehind modifier:
Box(
Modifier
.size(250.dp,60.dp)
.drawBehind {
drawRoundRect(color = Color.Red, style = stroke)
},
contentAlignment = Alignment.Center
) {
Text(textAlign = TextAlign.Center,text = "Tap here to introduce yourseft")
}
If you want rounded corner just use the cornerRadius attribute in the drawRoundRect method:
drawRoundRect(color = Color.Red,style = stroke, cornerRadius = CornerRadius(8.dp.toPx()))
If you prefer you can build your custom Modifier with the same code above. Something like:
fun Modifier.dashedBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }
this.then(
Modifier.drawWithCache {
onDrawBehind {
val stroke = Stroke(
width = strokeWidthPx,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
drawRoundRect(
color = color,
style = stroke,
cornerRadius = CornerRadius(cornerRadiusPx)
)
}
}
)
}
)
and then just apply it:
Box(
Modifier
.size(250.dp,60.dp)
.dashedBorder(1.dp, Red, 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "Tap here to introduce yourself",
textAlign = TextAlign.Center,
)
}
After some digging in the normal border modifier, I found out that it uses Stroke object which can take a parameter PathEffect that can make it dashed, here is a modified version of the normal border function that takes this parameter.
https://gist.github.com/DavidIbrahim/236dadbccd99c4fd328e53587df35a21
I wrote this extension for the Modifier you can simply use it or modify it.
fun Modifier.dashedBorder(width: Dp, radius: Dp, color: Color) =
drawBehind {
drawIntoCanvas {
val paint = Paint()
.apply {
strokeWidth = width.toPx()
this.color = color
style = PaintingStyle.Stroke
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
}
it.drawRoundRect(
width.toPx(),
width.toPx(),
size.width - width.toPx(),
size.height - width.toPx(),
radius.toPx(),
radius.toPx(),
paint
)
}
}