Jetpack Compose - Draw Line with Pattern - android

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:

Related

How to make rounded corner in drawLine in jetpack compose

I am using Canvas Api to draw something. I want to draw line with rounded corner. Line is draw without any problem. But I cannot figure out which attribute for corner radius.
val boxSize = 30.dp
Box(modifier = Modifier
.background(Color.LightGray)
.height(height = boxSize)
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
) {
val canvasWidth = size.width
drawLine(
start = Offset(x = 0f, y = (boxSize / 2).toPx()),
end = Offset(x = canvasWidth, y = (boxSize / 2).toPx()),
color = Color.Black,
strokeWidth = 8.dp.toPx()
)
}
}
My view is simple without corner radius.
I want my Black line to be corner for each side with specific radius.
You need to add the cap argument to drawLine and set it to StrokeCap.Round.
drawLine(
start = Offset(x = 0f, y = (boxSize / 2).toPx()),
end = Offset(x = canvasWidth, y = (boxSize / 2).toPx()),
color = Color.Black,
strokeWidth = 8.dp.toPx(),
cap = StrokeCap.Round, //add this line for rounded edges
)

Android: compose drawArc would not get centered within `Box`

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

Jetpack Compose - CardView with Arc Shape on border

I'm trying to achieve below cardview arc shape on cardview border/stroke.
Already tried to search on google but didn't find any relevant answer that suits with requirement.
Any lead or help will be appriciated.
Answer from Cirilo Bido and Raghunandan is good place to start, you round corners of rectangle with arcTo but you can't draw curved edges on top of clipped out shape. You need to use cubicTo to draw rounded edge and curve to clip out bottom shape
val shape = GenericShape {size: Size, layoutDirection: LayoutDirection ->
// draw cubic on left and right sides for button space
cubicTo()
}
You can check out this answer for drawing with cubic to. By combining both you can draw that path.
Jetpack Compose: How to draw a path / line like this
I created this path based on article shared by
Raghunandan initially, even though that is amazing answer for animating BottomBar it doesn't create a rounded shape if you look closely, at the bottom it's creating a triangular shape at the bottom instead of rounded one and shape OP requires and in article is also different.
So i used sliders to create bezier from the link i shared above. It's available as tutorial here too. Still it can be tweaked to more precise shape if you wish to.
I used x0, y0 as reference point to set control points and created this Path extension function.
fun Path.roundedRectanglePath(
size: Size,
cornerRadius: Float,
fabRadius: Float,
) {
val centerX = size.width / 2
val x0 = centerX - fabRadius * 1.15f
val y0 = 0f
// offset of the first control point (top part)
val topControlX = x0 + fabRadius * .5f
val topControlY = y0
// offset of the second control point (bottom part)
val bottomControlX = x0
val bottomControlY = y0 + fabRadius
// first curve
// set the starting point of the curve (P2)
val firstCurveStart = Offset(x0, y0)
// set the end point for the first curve (P3)
val firstCurveEnd = Offset(centerX, fabRadius * 1f)
// set the first control point (C1)
val firstCurveControlPoint1 = Offset(
x = topControlX,
y = topControlY
)
// set the second control point (C2)
val firstCurveControlPoint2 = Offset(
x = bottomControlX,
y = bottomControlY
)
// second curve
// end of first curve and start of second curve is the same (P3)
val secondCurveStart = Offset(
x = firstCurveEnd.x,
y = firstCurveEnd.y
)
// end of the second curve (P4)
val secondCurveEnd = Offset(
x = centerX + fabRadius * 1.15f,
y = 0f
)
// set the first control point of second curve (C4)
val secondCurveControlPoint1 = Offset(
x = secondCurveStart.x + fabRadius,
y = bottomControlY
)
// set the second control point (C3)
val secondCurveControlPoint2 = Offset(
x = secondCurveEnd.x - fabRadius / 2,
y = topControlY
)
// Top left arc
val radius = cornerRadius * 2
arcTo(
rect = Rect(
left = 0f,
top = 0f,
right = radius,
bottom = radius
),
startAngleDegrees = 180.0f,
sweepAngleDegrees = 90.0f,
forceMoveTo = false
)
lineTo(x = firstCurveStart.x, y = firstCurveStart.y)
// bezier curve with (P2, C1, C2, P3)
cubicTo(
x1 = firstCurveControlPoint1.x,
y1 = firstCurveControlPoint1.y,
x2 = firstCurveControlPoint2.x,
y2 = firstCurveControlPoint2.y,
x3 = firstCurveEnd.x,
y3 = firstCurveEnd.y
)
// bezier curve with (P3, C4, C3, P4)
cubicTo(
x1 = secondCurveControlPoint1.x,
y1 = secondCurveControlPoint1.y,
x2 = secondCurveControlPoint2.x,
y2 = secondCurveControlPoint2.y,
x3 = secondCurveEnd.x,
y3 = secondCurveEnd.y
)
lineTo(x = size.width - cornerRadius, y = 0f)
// Top right arc
arcTo(
rect = Rect(
left = size.width - radius,
top = 0f,
right = size.width,
bottom = radius
),
startAngleDegrees = -90.0f,
sweepAngleDegrees = 90.0f,
forceMoveTo = false
)
lineTo(x = 0f + size.width, y = size.height - cornerRadius)
// Bottom right arc
arcTo(
rect = Rect(
left = size.width - radius,
top = size.height - radius,
right = size.width,
bottom = size.height
),
startAngleDegrees = 0f,
sweepAngleDegrees = 90.0f,
forceMoveTo = false
)
lineTo(x = cornerRadius, y = size.height)
// Bottom left arc
arcTo(
rect = Rect(
left = 0f,
top = size.height - radius,
right = radius,
bottom = size.height
),
startAngleDegrees = 90.0f,
sweepAngleDegrees = 90.0f,
forceMoveTo = false
)
lineTo(x = 0f, y = cornerRadius)
close()
}
Composable that uses this shape
#Composable
private fun CustomArcShape(
modifier: Modifier,
elevation: Dp = 4.dp,
color: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = contentColorFor(color),
content: #Composable () -> Unit
) {
val diameter = 60.dp
val radiusDp = diameter / 2
val cornerRadiusDp = 10.dp
val density = LocalDensity.current
val cutoutRadius = density.run { radiusDp.toPx() }
val cornerRadius = density.run { cornerRadiusDp.toPx() }
val shape = remember {
GenericShape { size: Size, layoutDirection: LayoutDirection ->
this.roundedRectanglePath(
size = size,
cornerRadius = cornerRadius,
fabRadius = cutoutRadius * 2
)
}
}
Spacer(modifier = Modifier.height(diameter / 2))
Box(contentAlignment = Alignment.TopCenter) {
FloatingActionButton(
shape = CircleShape,
containerColor = Color(0xffD32F2F),
modifier = Modifier
.offset(y = -diameter / 5)
.size(diameter)
.drawBehind {
drawCircle(
Color.Red.copy(.5f),
radius = 1.3f * size.width / 2
)
drawCircle(
Color.Red.copy(.3f),
radius = 1.5f * size.width / 2
)
}
.align(Alignment.TopCenter),
onClick = { /*TODO*/ }
) {
Icon(
tint = Color.White,
imageVector = Icons.Filled.Close,
contentDescription = "Close"
)
}
Surface(
modifier = modifier,
shape = shape,
shadowElevation = elevation,
color = color,
contentColor = contentColor
) {
Column {
Spacer(modifier = Modifier.height(diameter))
content()
}
}
}
}
And demonstration
#Composable
private fun CustomArcShapeSample() {
Column(
modifier = Modifier
.fillMaxSize()
) {
CustomArcShape(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
.height(250.dp)
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"Payment Failed",
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(10.dp))
Text("Sorry !", fontSize = 24.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(10.dp))
Text("Your transfer to bank failed", color = Color.LightGray)
}
}
Spacer(modifier = Modifier.height(40.dp))
CustomArcShape(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
.height(250.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.border(1.dp, Color.Green),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"Payment Failed",
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(10.dp))
Text("Sorry !", fontSize = 24.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(10.dp))
Text("Your transfer to bank failed", color = Color.LightGray)
}
}
}
}
You probably need to draw that arc in a custom composable, I found this article that can help you to understand the process of drawing in compose!
turtorial article: https://github.com/JeckOnly/passage/blob/master/Android/Widget/Compose/%E9%A1%B6%E9%83%A8%E5%87%B9%E9%99%B7Shape.md
code:https://gist.github.com/JeckOnly/54936415d1670103a4d400f66c8b31a1
hope this can help you though it is in Chinese language.

Jetpack Compose Draw Arc with Dot Circle

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/

Arc/Semicircle Shape in Jetpack Compose

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.

Categories

Resources