Jetpack Compose semicircle using Canvas - android

I am trying to create a semicircle speed progress bar in Jetpack Compose. Unless the view is square the semicircle will not look as expected, if I use 1:2 width: height it will be flattened. I want a Composable representing half of the circle where I don't have unusable bottom half of the view.
Box(
modifier = modifier
.background(Color.Red)
) {
Canvas(modifier = Modifier.size(300.dp)) {
drawArc(
color = Color.LightGray,
-180f,
180f,
useCenter = false,
style = Stroke(8.dp.toPx(), cap = StrokeCap.Round)
)
}
Text(
modifier = Modifier.align(alignment = Alignment.Center),
text = "20 Mbps",
color = Color.White,
fontSize = 20.sp
)
}
The expected outcome would be a reusable semicircle composable with a height of the actual semicircle so I can easily position other content against it. The expected view size is marked by a dotted green line.

As i mentioned in comments arc uses rectangle if you want a semi arc that covers whole hight just double the height you draw arc with
#Composable
private fun ArcComposable(modifier: Modifier) {
Box(
modifier = modifier
.background(Color.Red)
) {
Canvas(modifier = Modifier
.size(300.dp)
.clipToBounds()) {
drawArc(
color = Color.LightGray,
-180f,
180f,
useCenter = false,
size = Size(size.width, size.height * 2),
style = Stroke(8.dp.toPx(), cap = StrokeCap.Round)
)
}
Text(
modifier = Modifier.align(alignment = Alignment.Center),
text = "20 Mbps",
color = Color.White,
fontSize = 20.sp
)
}
}
I added Modifier.clipToBounds() because of strokeCap round which is added to length of the line by default. You can just reduce size and height few px to match inside the canvas. Canvas by default even if you don't set a modifier with size it draws anything out of its bounds unless you use Modifier.clipToBounds()
private fun ArcComposable(modifier: Modifier) {
Box(
modifier = modifier
.background(Color.Red)
) {
Canvas(
modifier = Modifier
.size(300.dp)
// .clipToBounds()
) {
drawArc(
color = Color.LightGray,
-180f,
180f,
useCenter = false,
topLeft = Offset(4.dp.toPx(), 6.dp.toPx()),
size = Size(size.width - 8.dp.toPx(), size.height * 2 - 20.dp.toPx()),
style = Stroke(8.dp.toPx(), cap = StrokeCap.Round)
)
}
Text(
modifier = Modifier.align(alignment = Alignment.Center),
text = "20 Mbps",
color = Color.White,
fontSize = 20.sp
)
}
}

Related

How to draw one side curve of box in jetpack compose android

I want to draw this shape in jetpack compose, anybody has idea how we can achieve this?
You can simply draw a shape inside a Box.
Something like:
Box(
Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.DarkGray)
.padding(10.dp)
.background(Red)
) {
Canvas(
modifier = Modifier
.fillMaxSize()
) {
val canvasWidth = size.width
val arcHeight = 200f
drawArc(
color = Color.DarkGray,
startAngle = 0f,
sweepAngle = 180f,
useCenter = false,
topLeft = Offset(0f, -arcHeight/2),
size = Size(canvasWidth, arcHeight)
)
}
}

How to draw this easily on compose?

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 = ""
)
}
}
}

How to add left border in a Row/Column/Card Compose

I'm trying to add a left/start vertical border to view (Column), Not able to get the solution. as of now was trying to achieve using a divider inside the column it also need a height, but it depends on the contents inside the column, sometime it may grow.
Column(modifier = Modifier.padding(start = 34.dp)) {
Divider(
color = Color.Red,
modifier = Modifier
.height(100.dp)
.padding(end = 34.dp).width(2.dp)
)
You can use the drawWithCache modifier using the drawLine function.
Something like:
Column(modifier =
Modifier
.padding(start = 34.dp)
.size(100.dp, 75.dp)
.drawWithCache {
onDrawWithContent {
// draw behind the content the vertical line on the left
drawLine(
color = Color.Red,
start = Offset.Zero,
end = Offset(0f, this.size.height),
strokeWidth= 1f
)
// draw the content
drawContent()
}
}
){
//...content
}
If you want to use a Divider you can use fillMaxHeight() applying an intrinsic measurements to its parent container.
Something like:
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
Divider(
color = Color.Red,
modifier = Modifier
.fillMaxHeight() //important
.width(2.dp)
)
Box(Modifier.fillMaxWidth().height(100.dp).background(Yellow))
}
You can achive this with Modifier.drawBehind and drawLine
Code
TextButton(
onClick = {
//Click Functions
},
modifier = Modifier.drawBehind {
val strokeWidth = 1 * density
//Draw line function for left border
drawLine(
Color.LightGray,
Offset(0f, strokeWidth),
Offset(0f, size.height),
strokeWidth
)
}
)
{
Text("Left Border")
}
Output

how to add border on bottom only in jetpack compose

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
}
}

How to have dashed border in Jetpack Compose?

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
)
}
}

Categories

Resources