I'm looking to draw an arc type shape in Jetpack Compose but I'm really struggling to figure out the best way to do it. When I use drawArc, I can't seem to start the arc from the bottom left corner, it just starts from left middle. I've also tried drawRoundRect but I can't find a way to just round the top left and top right corners.
I'll attach a picture, I don't need the white handle within the shape. Desired result:
drawRoundRect attempt:
drawArc attempt:
You could make it by using a a canvas with your device width but try to increase arc limits beyond device width. With this code
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasWidth = size.width
val canvasHeight = size.height
val formWidth = (canvasWidth * 2)
val xPos = canvasWidth / 2
drawArc(
Color.Black,
0f,
-180f,
useCenter = true,
size = Size(formWidth, 600f),
topLeft = Offset(x = -xPos, y = canvasHeight - 300)
)
}
Arc at bottom looks like this:
more info
You can draw it like this
Placed it to center for demonstration and border is to show that it doesn't overflow Canvas, if you don't pay attention, Canvas draws out of its borders unless use Modifier.clipbounds() is used on Canvas Modifier.
Trick for drawing half arc is to set two times of Canvas height, since arc is drawn inside a rectangle in principle. Changind height of Modifier of canvas changes height of arc.
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Canvas(
modifier = Modifier
.border(2.dp, Color.Red)
.fillMaxWidth()
.height(80.dp),
) {
val canvasWidth = size.width
val canvasHeight = size.height
drawArc(
Color.Blue,
startAngle = 180f,
sweepAngle = 180f,
useCenter = true,
size = Size(canvasWidth, 2 * canvasHeight)
)
val handleWidth = 200f
val handleHeight = 30f
drawRoundRect(
Color.White,
size = Size(handleWidth, handleHeight),
cornerRadius = CornerRadius(5.dp.toPx(), 5.dp.toPx()),
topLeft = Offset((canvasWidth - handleWidth) / 2, (canvasHeight - handleHeight)/2)
)
}
}
Result
You can check this tutorial about Jetpack Compose which covers Canvas operations in detail.
Related
I am trying to show progress using drawArc for compose. I have tried drawBehind modifier to draw a background circle and not trying to draw another circle on top of it to show the progress. The problems is, no matter what I try, the arc turns out to be drawn at top left corner all the time. If I explicitly define the topLeft value, it only works for 1 screen size. So I am trying to get a dynamic size for both circles and also the strokewidth. So for Tablets only the circles should be increased depending on the screensize and also the thickness should be increased for the same. And for the smaller size devices, the value should be decreasing. Here is my code and output:
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
BoxWithConstraints(
Modifier
.fillMaxSize()
.background(Color.Yellow),
) {
Canvas(modifier = Modifier
.size(maxWidth, maxHeight)
.background(color = Color.Red)) {
drawCircle(
color = Color.Gray,
radius = (maxWidth /4).toPx(),
style = Stroke(width = 14f, cap = StrokeCap.Round),
)
val sweepAngle = progress/100 * 360
drawArc(
size = Size((maxWidth/2).toPx(),(maxWidth/2).toPx()),
color = Color.Green,
startAngle = -90f,
sweepAngle = sweepAngle,
useCenter = false,
style = Stroke(10f, cap = StrokeCap.Round),
)
}
}
}
PS: I cannot use another circle object since I need to have the tip of the circle round aka cap should be Stroke.Round.
I ended up trying BoxWithConstraints so I can have access to maxWidth and maxHeight
You have to calculate the topLeft offset of the arc as center of the circle - radius of the circle.
Then the size of arc is maxWidth/2 instead of maxWidth/4 = radius.
Something like:
val stroke = 5.dp
drawCircle(
color = Color.Gray,
radius = (maxWidth /4).toPx(),
style = Stroke(width = stroke.toPx(), cap = StrokeCap.Round),
)
val sweepAngle = progress/100 * 360
val offsetx = this.center.x - (maxWidth/4).toPx()
val offsety = this.center.y - (maxWidth/4).toPx()
drawArc(
size = Size((maxWidth/2).toPx(),(maxWidth/2).toPx()),
color = Color.Green,
startAngle = 0f,
sweepAngle = -sweepAngle,
topLeft = Offset(offsetx,offsety),
useCenter = false,
style = Stroke(stroke.toPx(), cap = StrokeCap.Round),
)
}
As the title says, I am just trying to figure out how can I round only the bottom 2 corners of the round rect.
drawScope.drawRoundRect(
topLeft = Offset(0f,0f),
size = Size(100f,100f),
cornerRadius = CornerRadius(x = 10f, y = 10f),
color = boxPaint.color
)
This is my current code, which rounds all corners.
Seems like with drawRoundRect you only can setup left or right corner radius.
When you find that the Canvas API is missing something, you can draw almost anything using Path - it has much more flexible API.
val cornerRadius = CornerRadius(10f, 10f)
val path = Path().apply {
addRoundRect(
RoundRect(
rect = Rect(
offset = Offset(0f, 0f),
size = Size(100f, 100f),
),
bottomLeft = cornerRadius,
bottomRight = cornerRadius,
)
)
}
drawPath(path, color = Color.Red)
I've created an Arc with canvas like this:
Canvas(modifier = Modifier.size(255.dp), onDraw = {
rotate(300f) {
drawArc(
color = primary,
startAngle = 0f,
sweepAngle = 180f,
useCenter = false,
style = Stroke(width = 8f)
)
}
})
But, that canvas composable draws in a square shape so that I can't make the edges of arc to touch border of the screen.
Use topLeft = Offset(x, y) in the drawArc function with negative values for x and y if necessary to translate it where you need it.
I'm looking to draw a line on a canvas with a pattern instead of a color. Here's the code I have right now:
drawLine(
color = progressColor,
start = Offset(if (whiteGap) progressCapWidth else startOffsetBg, yOffset),
end = Offset(endOffsetProgress, yOffset),
strokeWidth = progressHeight.toPx(),
cap = if (roundedCorners) StrokeCap.Round else StrokeCap.Butt,
)
It's part of a custom linear progress bar. Per the design I was given, they want the progress to have this pattern:
This is an example of full progress with this diagonally patterned progress. Is it possible to use a drawable and repeat it instead of a color? Is there a way to just apply/create diagonal white gaps directly in when drawing the line?
We're implementing this whole feature using Jetpack Compose, so I can't do something with traditional XML involved.
Here's how you can draw it with Canvas:
Canvas(
Modifier
.padding(top = 100.dp)
.border(1.dp,Color.Black)
.padding(10.dp)
.height(30.dp)
.fillMaxWidth()
.clip(CircleShape)
) {
val step = 10.dp
val angleDegrees = 45f
val stepPx = step.toPx()
val stepsCount = (size.width / stepPx).roundToInt()
val actualStep = size.width / stepsCount
val dotSize = Size(width = actualStep / 2, height = size.height * 2)
for (i in -1..stepsCount) {
val rect = Rect(
offset = Offset(x = i * actualStep, y = (size.height - dotSize.height) / 2),
size = dotSize,
)
rotate(angleDegrees, pivot = rect.center) {
drawRect(
Color.Blue,
topLeft = rect.topLeft,
size = rect.size,
)
}
}
}
Result:
I've looking to draw an arc on a canvas in Jetpack Compose with a little circle on the edge of progress like this picture:
I found how to draw the progress bar with arc canvas but don't know yet how to draw the circle to match with the edge of the arc line. This is my progress code:
#Composable
fun ComposeCircularProgressBar(
modifier: Modifier = Modifier,
percentage: Float,
fillColor: Color,
backgroundColor: Color,
strokeWidth: Dp
) {
Canvas(
modifier = modifier
.padding(strokeWidth / 2)
) {
// Background Line
drawArc(
color = backgroundColor,
135f,
270f,
false,
style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Butt)
)
// Fill Line
drawArc(
color = fillColor,
135f,
270f * percentage,
false,
style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round)
)
}
}
Noted: for now I know to draw that circle is with Canvas.drawCircle(offset = Offset) but I don't know yet how to calculate the Offset(x,y) to match with the edge of progress.
This piece of code below will generate the arc with the circular dot based on the percentage you provide. You did get most of the part right, it was just about solving the Math Equation to find the point on the circle.
I assumed the radius of the circle as Height / 2 of the widget.
Since we are not drawing the full circle, the start angle is at 140 degrees and the maximum sweep angle is 260 degrees. (I found this by hit and trial, so that it looks as close to your image)
Now to draw the small white circle the center a.k.a offset has to be at (x,y) where x & y are given by the formula
x = radius * sin (angle in radians)
y = radius * cos (angle in radians)
#Composable
fun ComposeCircularProgressBar(
modifier: Modifier = Modifier,
percentage: Float,
fillColor: Color,
backgroundColor: Color,
strokeWidth: Dp
) {
Canvas(
modifier = modifier
.size(150.dp)
.padding(10.dp)
) {
// Background Line
drawArc(
color = backgroundColor,
140f,
260f,
false,
style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round),
size = Size(size.width, size.height)
)
drawArc(
color = fillColor,
140f,
percentage * 260f,
false,
style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round),
size = Size(size.width, size.height)
)
var angleInDegrees = (percentage * 260.0) + 50.0
var radius = (size.height / 2)
var x = -(radius * sin(Math.toRadians(angleInDegrees))).toFloat() + (size.width / 2)
var y = (radius * cos(Math.toRadians(angleInDegrees))).toFloat() + (size.height / 2)
drawCircle(
color = Color.White,
radius = 5f,
center = Offset(x, y)
)
}
}
Here are some examples I tried with
#Preview
#Composable
fun PreviewPorgressBar() {
ComposeCircularProgressBar(
percentage = 0.80f,
fillColor = Color(android.graphics.Color.parseColor("#4DB6AC")),
backgroundColor = Color(android.graphics.Color.parseColor("#90A4AE")),
strokeWidth = 10.dp
)
}
#Preview
#Composable
fun PreviewPorgressBar() {
ComposeCircularProgressBar(
percentage = 0.45f,
fillColor = Color(android.graphics.Color.parseColor("#4DB6AC")),
backgroundColor = Color(android.graphics.Color.parseColor("#90A4AE")),
strokeWidth = 10.dp
)
}
#Preview
#Composable
fun PreviewPorgressBar() {
ComposeCircularProgressBar(
percentage = 1f,
fillColor = Color(android.graphics.Color.parseColor("#4DB6AC")),
backgroundColor = Color(android.graphics.Color.parseColor("#90A4AE")),
strokeWidth = 10.dp
)
}
[Update] If you're interested in a step-by-step tutorial you can read it here :
https://blog.droidchef.dev/custom-progress-with-jetpack-compose-tutorial/