I have a custom canvas component. I want to test canvas UI objects like rect, line, text, etc. How should I test this sample UI?
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(canvasHeight.dp)
.pointerInput(Unit) {
detectTapGestures { offset ->
touchPosition.value = offset
}
}
)
{
drawArc(
color = Color(0xFFf04231),
startAngle = -90f,
sweepAngle = 180f,
useCenter = true,
size = Size(size.width * .50f, size.height * .50f),
topLeft = Offset(size.width * .25f, 0f)
)
drawPath(path = path, color = Color.White.copy(alpha = .90f))
drawCircle(
brush = Brush.verticalGradient(sunColor),
radius = width.times(.17f),
center = Offset(width.times(.35f), height.times(.35f))
)
}
Related
I'm trying to implement the following component in compose
This is what I have so far
#Composable
fun CircularLoader(
size: Dp = 40.dp,
strokeWidth: Dp = 4.dp
) {
val stroke = with(LocalDensity.current) {
Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
}
// draw on canvas
Canvas(
Modifier
.progressSemantics()
.size(size)
.padding(strokeWidth / 2)
) {
drawArc(
startAngle = 0f,
sweepAngle = 300f,
useCenter = false,
brush = Brush.linearGradient(listOf(
Color(0xFFEF7B7B),
Color(0x00EF7B7B)
)),
style = stroke
)
}
}
As Gabriele Mariotti said, try to use sweepGradient:
#Composable
#Preview
fun CircularLoaderPreview() {
Box(modifier = Modifier.background(Color(0xFF0F1A30))) {
Box(modifier = Modifier.padding(16.dp)) {
CircularLoader(size = 120.dp, strokeWidth = 18.dp)
}
}
}
#Composable
fun CircularLoader(
size: Dp,
strokeWidth: Dp
) {
val stroke = with(LocalDensity.current) {
Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
}
// draw on canvas
Canvas(
modifier = Modifier
.progressSemantics()
.size(size)
.padding(strokeWidth / 2)
) {
drawArc(
startAngle = 0f,
sweepAngle = 300f,
useCenter = false,
brush = Brush.sweepGradient( // !!! that what
0f to Color(0x00EF7B7B),
0.9f to Color(0xFFEF7B7B),
0.91f to Color(0x00EF7B7B), // there was a problem with start of the gradient, maybe there way to solve it better
1f to Color(0x00EF7B7B)
),
style = stroke
)
}
}
Result:
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
I want to make a progress bar similar to this one with jetpack compose by canvas, and I've made some Shape, but I'm having trouble implementing the progress section.
I add a shape in drawWithContent of Box
Image
val path = Path()
path.moveTo(x = startOffset.x, y = startOffset.y)
path.addRoundRect(
RoundRect(
left = 0F,
top = 0F,
right = this.size.width,
bottom = this.size.height,
cornerRadius = CornerRadius(x = 16.dp.toPx(), y = 16.dp.toPx())
)
)
clipPath(
path = path,
clipOp = ClipOp.Intersect
) {
drawPath(
path = path,
style = Stroke(5.dp.toPx(), 16.dp.toPx(), cap = StrokeCap.Round),
brush = SolidColor(Color.Red),
)
}
You can try something like that
#Composable
private fun Q74121342() {
val density = LocalDensity.current
val strokeWidth = remember { with(density) { 2.dp.toPx() } } // Convert to px for much needed stroke width
val rotateTransition = rememberInfiniteTransition()
val rotateAnimateValue = rotateTransition.animateValue(
initialValue = 0f,
targetValue = 360f,
typeConverter = Float.VectorConverter,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
)
) // Creating infinite animation. You can replace with finitive one and indicate current progress outside of composable
Image(
modifier = Modifier
.size(96.dp)
.padding(2.dp)
.drawBehind {
rotate(
degrees = rotateAnimateValue.value
) {
drawArc(
color = Color.Red,
startAngle = 0f,
sweepAngle = 180f,
useCenter = false,
style = Stroke(
width = strokeWidth,
cap = StrokeCap.Round,
join = StrokeJoin.Round,
)
) // Can be replaced with path but don't forget change image shape
}
} // Applying our custom paint
.padding(2.dp)
.clip(CircleShape), // Crop original image to much circle shape
painter = painterResource(R.drawable.img_profile),
contentDescription = null,
contentScale = ContentScale.Crop
)
}
You can see the demo on gist
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.
I'm not sure how can I do the calculation in order to center this arc on the canvas? Can someone point me in the proper direction?
Canvas(modifier = Modifier
.background(Color.LightGray)
.fillMaxWidth()
.height(300.dp)
) {
drawArc(
color = Color.Blue,
startAngle = 30f,
sweepAngle = 300f,
useCenter = false,
style = Stroke(width = 50f, cap = StrokeCap.Round),
size = size/2.25F
)
}
Use the topLeft parameter in the drawArc method.
Something like:
val sizeArc = size/2.25F
drawArc(
color = Color.Blue,
startAngle = 30f,
sweepAngle = 300f,
topLeft = Offset((size.width - sizeArc.width)/2f,(size.height - sizeArc.height)/2f),
useCenter = false,
style = Stroke(width = 50f, cap = StrokeCap.Round),
size = sizeArc
)
#Composable
fun CustomArc() {
Canvas(modifier = Modifier.fillMaxSize()) {
val arcRadius = 200f
val canvasWidth = size.width
val canvasHeight = size.height
drawArc(
color = Color.Red,
startAngle = -90f, //start angle is always in clockwise direction
sweepAngle = 270f, // angle formed between the start angle
useCenter = false,
size = Size(arcRadius, arcRadius),
topLeft = Offset(
(canvasWidth / 2) - (arcRadius / 2),
canvasHeight / 2 - (arcRadius / 2)
),
style = Stroke(width = 10f, cap = StrokeCap.Round)
)
}
}