Jetpack Compose中的Canvas API 使用起来感觉比传统View中的要简单一些,因为它不需要画笔Paint和画布分开来,大多数直接就是一个函数搞定,当然也有一些限制。
Compose 直接提供了一个叫 Canvas
的 Composable
组件,可以在任何 Composable
组件中直接使用,在 Canvas
的DrawScope
作用域中就可以使用其提供的各种绘制Api进行绘制了。这比传统View要方便的多,传统View中,你只能继承一个View控件,才有机会覆写其onDraw()
方法。
基本图形绘制
常用的API一览表:
API | 描述 |
---|---|
drawLine | 绘制一条线 |
drawRect | 绘制一个矩形 |
drawImage | 绘制一张图片 |
drawRoundRect | 绘制一个圆角矩形 |
drawCircle | 绘制一个圆 |
drawOval | 绘制一个椭圆 |
drawArc | 绘制一条弧线 |
drawPath | 绘制一条路径 |
drawPoints | 绘制一些点 |
这些基本图形的绘制比较简单,基本上尝试一下就知道如何使用了。Compose中的Canvas坐标体系跟传统View一样,也是也左上角为坐标原点的,因此如果是设置偏移量都是针对Canvas左上角而言的。
drawLine
@Composable
fun DrawLineExample() {
TutorialText2(text = "strokeWidth")
Canvas(modifier = canvasModifier) {
drawLine(
start = Offset(x = 100f, y = 30f),
end = Offset(x = size.width - 100f, y = 30f),
color = Color.Red,
)
drawLine(
start = Offset(x = 100f, y = 70f),
end = Offset(x = size.width - 100f, y = 70f),
color = Color.Red,
strokeWidth = 5f
)
drawLine(
start = Offset(x = 100f, y = 110f),
end = Offset(x = size.width - 100f, y = 110f),
color = Color.Red,
strokeWidth = 10f
)
}
Spacer(modifier = Modifier.height(10.dp))
TutorialText2(text = "StrokeCap")
Canvas(modifier = canvasModifier) {
drawLine(
cap = StrokeCap.Round,
start = Offset(x = 100f, y = 30f),
end = Offset(x = size.width - 100f, y = 30f),
color = Color.Red,
strokeWidth = 20f
)
drawLine(
cap = StrokeCap.Butt,
start = Offset(x = 100f, y = 70f),
end = Offset(x = size.width - 100f, y = 70f),
color = Color.Red,
strokeWidth = 20f
)
drawLine(
cap = StrokeCap.Square,
start = Offset(x = 100f, y = 110f),
end = Offset(x = size.width - 100f, y = 110f),
color = Color.Red,
strokeWidth = 20f
)
}
Spacer(modifier = Modifier.height(10.dp))
TutorialText2(text = "Brush")
Canvas(modifier = canvasModifier) {
drawLine(
brush = Brush.linearGradient(
colors = listOf(Color.Red, Color.Green)
),
start = Offset(x = 100f, y = 30f),
end = Offset(x = size.width - 100f, y = 30f),
strokeWidth = 20f,
)
drawLine(
brush = Brush.radialGradient(
colors = listOf(Color.Red, Color.Green, Color.Blue)
),
start = Offset(x = 100f, y = 70f),
end = Offset(x = size.width - 100f, y = 70f),
strokeWidth = 20f,
)
drawLine(
brush = Brush.sweepGradient(
colors = listOf(Color.Red, Color.Green, Color.Blue)
),
start = Offset(x = 100f, y = 110f),
end = Offset(x = size.width - 100f, y = 110f),
strokeWidth = 20f,
)
}
Spacer(modifier = Modifier.height(10.dp))
TutorialText2(text = "PathEffect")
Canvas(
modifier = Modifier
.padding(8.dp)
.shadow(1.dp)
.background(Color.White)
.fillMaxWidth()
.height(120.dp)
) {
drawLine(
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)),
start = Offset(x = 100f, y = 30f),
end = Offset(x = size.width - 100f, y = 30f),
color = Color.Red,
strokeWidth = 10f
)
drawLine(
pathEffect = PathEffect.dashPathEffect(floatArrayOf(40f, 10f)),
start = Offset(x = 100f, y = 70f),
end = Offset(x = size.width - 100f, y = 70f),
color = Color.Red,
strokeWidth = 10f
)
drawLine(
pathEffect = PathEffect.dashPathEffect(floatArrayOf(70f, 40f)),
start = Offset(x = 100f, y = 110f),
end = Offset(x = size.width - 100f, y = 110f),
cap = StrokeCap.Round,
color = Color.Red,
strokeWidth = 15f
)
val path = Path().apply {
moveTo(10f, 0f)
lineTo(20f, 10f)
lineTo(10f, 20f)
lineTo(0f, 10f)
}
drawLine(
pathEffect = PathEffect.stampedPathEffect(
shape = path,
advance = 30f,
phase = 30f,
style = StampedPathEffectStyle.Rotate
),
start = Offset(x = 100f, y = 150f),
end = Offset(x = size.width - 100f, y = 150f),
color = Color.Green,
strokeWidth = 10f
)
drawLine(
pathEffect = PathEffect.stampedPathEffect(
shape = path,
advance = 30f,
phase = 10f,
style = StampedPathEffectStyle.Morph
),
start = Offset(x = 100f, y = 190f),
end = Offset(x = size.width - 100f, y = 190f),
color = Color.Green,
strokeWidth = 10f
)
}
}
drawCircle & drawOval
@Composable
fun DrawCircleExample() {
TutorialText2(text = "Oval and Circle")
Canvas(modifier = canvasModifier2) {
val canvasWidth = size.width
val canvasHeight = size.height
val radius = canvasHeight / 2
drawOval(
color = Color.Blue,
topLeft = Offset.Zero,
size = Size(1.2f * canvasHeight, canvasHeight)
)
drawOval(
color = Color.Green,
topLeft = Offset(1.5f * canvasHeight, 0f),
size = Size(canvasHeight / 1.5f, canvasHeight)
)
drawCircle(
Color.Red,
center = Offset(canvasWidth - 2 * radius, canvasHeight / 2),
radius = radius * 0.8f,
)
}
Spacer(modifier = Modifier.height(10.dp))
TutorialText2(text = "DrawStyle")
Canvas(modifier = canvasModifier2) {
val canvasWidth = size.width
val canvasHeight = size.height
val radius = canvasHeight / 2
val space = (canvasWidth - 6 * radius) / 4
drawCircle(
color = Color.Red,
radius = radius,
center = Offset(space + radius, canvasHeight / 2),
style = Stroke(width = 5.dp.toPx())
)
drawCircle(
color = Color.Red,
radius = radius,
center = Offset(2 * space + 3 * radius, canvasHeight / 2),
style = Stroke(
width = 5.dp.toPx(),
join = StrokeJoin.Round,
cap = StrokeCap.Round,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f))
)
)
val path = Path().apply {
moveTo(10f, 0f)
lineTo(20f, 10f)
lineTo(10f, 20f)
lineTo(0f, 10f)
}
val pathEffect = PathEffect.stampedPathEffect(
shape = path,
advance = 20f,
phase = 20f,
style = StampedPathEffectStyle.Morph
)
drawCircle(
color = Color.Red,
radius = radius,
center = Offset(canvasWidth - space - radius, canvasHeight / 2),
style = Stroke(
width = 5.dp.toPx(),
join = StrokeJoin.Round,
cap = StrokeCap.Round,
pathEffect = pathEffect
)
)
}
Spacer(modifier = Modifier.height(10.dp))
TutorialText2(text = "Brush")
Canvas(modifier = canvasModifier2) {
val canvasWidth = size.width
val canvasHeight = size.height
val radius = canvasHeight / 2
val space = (canvasWidth - 6 * radius) / 4
drawCircle(
brush = Brush.linearGradient(
colors = listOf(Color.Red, Color.Green),
start = Offset(radius * .3f, radius * .1f),
end = Offset(radius * 2f, radius * 2f)
),
radius = radius,
center = Offset(space + radius, canvasHeight / 2),
)
drawCircle(
brush = Brush.radialGradient(
colors = listOf(Color.Red, Color.Green)
),
radius = radius,
center = Offset(2 * space + 3 * radius, canvasHeight / 2),
)
drawCircle(
brush = Brush.verticalGradient(
colors = listOf(
Color.Red,
Color.Green,
Color.Yellow,
Color.Blue,
Color.Cyan,
Color.Magenta
),
),
radius = radius,
center = Offset(canvasWidth - space - radius, canvasHeight / 2)
)
}
Spacer(modifier = Modifier.height(10.dp))
Canvas(modifier = canvasModifier2) {
val canvasWidth = size.width
val canvasHeight = size.height
val radius = canvasHeight / 2
val space = (canvasWidth - 6 * radius) / 4
drawCircle(
brush = Brush.sweepGradient(
colors = listOf(
Color.Green,
Color.Red,
Color.Blue
),
center = Offset(space + radius, canvasHeight / 2),
),
radius = radius,
center = Offset(space + radius, canvasHeight / 2),
)
drawCircle(
brush = Brush.sweepGradient(
colors = listOf(
Color.Green,
Color.Cyan,
Color.Red,
Color.Blue,
Color.Yellow,
Color.Magenta,
),
// Offset for this gradient is not at center, a little bit left of center
center = Offset(2 * space + 2.7f * radius, canvasHeight / 2),
),
radius = radius,
center = Offset(2 * space + 3 * radius, canvasHeight / 2),
)
drawCircle(
brush = Brush.sweepGradient(
colors = gradientColors,
center = Offset(canvasWidth - space - radius, canvasHeight / 2),
),
radius = radius,
center = Offset(canvasWidth - space - radius, canvasHeight / 2)
)
}
}
drawRect
@Composable
private fun DrawRectangleExample() {
Spacer(modifier = Modifier.height(10.dp))
TutorialText2(text = "Rectangle")
Canvas(modifier = canvasModifier2) {
val canvasWidth = size.width
val canvasHeight = size.height
val space = 60f
val rectHeight = canvasHeight / 2
val rectWidth = (canvasWidth - 4 * space) / 3
drawRect(
color = Color.Blue,
topLeft = Offset(space, rectHeight / 2),
size = Size(rectWidth, rectHeight)
)
drawRect(
color = Color.Green,
topLeft = Offset(2 * space + rectWidth, rectHeight / 2),
size = Size(rectWidth, rectHeight),
style = Stroke(width = 12.dp.toPx())
)
drawRect(
color = Color.Red,
topLeft = Offset(3 * space + 2 * rectWidth, rectHeight / 2),
size = Size(rectWidth, rectHeight),
style = Stroke(width = 2.dp.toPx())
)
}
TutorialText2(text = "RoundedRect")
Canvas(modifier = canvasModifier2) {
val canvasWidth = size.width
val canvasHeight = size.height
val space = 60f
val rectHeight = canvasHeight / 2
val rectWidth = (canvasWidth - 4 * space) / 3
drawRoundRect(
color = Color.Blue,
topLeft = Offset(space, rectHeight / 2),
size = Size(rectWidth, rectHeight),
cornerRadius = CornerRadius(8.dp.toPx(), 8.dp.toPx())
)
drawRoundRect(
color = Color.Green,
topLeft = Offset(2 * space + rectWidth, rectHeight / 2),
size = Size(rectWidth, rectHeight),
cornerRadius = CornerRadius(70f, 70f)
)
drawRoundRect(
color = Color.Red,
topLeft = Offset(3 * space + 2 * rectWidth, rectHeight / 2),
size = Size(rectWidth, rectHeight),
cornerRadius = CornerRadius(50f, 25f)
)
}
Spacer(modifier = Modifier.height(10.dp))
TutorialText2(text = "DrawStyle")
Canvas(modifier = canvasModifier2) {
val canvasWidth = size.width
val canvasHeight = size.height
val space = 30f
val rectHeight = canvasHeight / 2
val rectWidth = (canvasWidth - 4 * space) / 3
drawRect(
color = Color.Blue,
topLeft = Offset(space, rectHeight / 2),
size = Size(rectWidth, rectHeight),
style = Stroke(
width = 2.dp.toPx(),
join = StrokeJoin.Miter,
cap = StrokeCap.Butt,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(15f, 15f))
)
)
drawRect(
color = Color.Green,
topLeft = Offset(2 * space + rectWidth, rectHeight / 2),
size = Size(rectWidth, rectHeight),
style = Stroke(
width = 2.dp.toPx(),
join = StrokeJoin.Bevel,
cap = StrokeCap.Square,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(15f, 15f))
)
)
drawRect(
color = Color.Red,
topLeft = Offset(3 * space + 2 * rectWidth, rectHeight / 2),
size = Size(rectWidth, rectHeight),
style = Stroke(
width = 2.dp.toPx(),
join = StrokeJoin.Round,
cap = StrokeCap.Round,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(15f, 15f))
)
)
}
Spacer(modifier = Modifier.height(10.dp))
TutorialText2(text = "Brush")
Canvas(modifier = canvasModifier2) {
val canvasWidth = size.width
val canvasHeight = size.height
val space = 30f
val rectHeight = canvasHeight / 2
val rectWidth = (canvasWidth - 4 * space) / 3
drawRect(
brush = Brush.radialGradient(
colors = listOf(
Color.Green,
Color.Red,
Color.Blue,
Color.Yellow,
Color.Magenta
),
center = Offset(space + .5f * rectWidth, rectHeight),
tileMode = TileMode.Mirror,
radius = 20f
),
topLeft = Offset(space, rectHeight / 2),
size = Size(rectWidth, rectHeight)
)
drawRect(
brush = Brush.radialGradient(
colors = listOf(
Color.Green,
Color.Red,
Color.Blue,
Color.Yellow,
Color.Magenta
),
center = Offset(2 * space + 1.5f * rectWidth, rectHeight),
tileMode = TileMode.Repeated,
radius = 20f
),
topLeft = Offset(2 * space + rectWidth, rectHeight / 2),
size = Size(rectWidth, rectHeight)
)
drawRect(
brush = Brush.radialGradient(
colors = listOf(
Color.Green,
Color.Red,
Color.Blue,
Color.Yellow,
Color.Magenta
),
center = Offset(3 * space + 2.5f * rectWidth, rectHeight),
tileMode = TileMode.Decal,
radius = rectHeight / 2
),
topLeft = Offset(3 * space + 2 * rectWidth, rectHeight / 2),
size = Size(rectWidth, rectHeight)
)
}
}
drawPoints
@Composable
fun DrawPointsExample() {
Spacer(modifier = Modifier.height(10.dp))
TutorialText2(text = "PointMode")
Canvas(modifier = canvasModifier2) {
val middleW = size.width / 2
val middleH = size.height / 2
drawLine(Color.Gray, Offset(0f, middleH), Offset(size.width - 1, middleH))
drawLine(Color.Gray, Offset(middleW, 0f), Offset(middleW, size.height - 1))
val points1 = getSinusoidalPoints(size)
drawPoints(
color = Color.Blue,
points = points1,
cap = StrokeCap.Round,
pointMode = PointMode.Points,
strokeWidth = 10f
)
val points2 = getSinusoidalPoints(size, 100f)
drawPoints(
color = Color.Green,
points = points2,
cap = StrokeCap.Round,
pointMode = PointMode.Lines,
strokeWidth = 10f
)
val points3 = getSinusoidalPoints(size, 200f)
drawPoints(
color = Color.Red,
points = points3,
cap = StrokeCap.Round,
pointMode = PointMode.Polygon,
strokeWidth = 10f
)
}
Spacer(modifier = Modifier.height(10.dp))
TutorialText2(text = "Brush")
Canvas(modifier = canvasModifier2) {
val middleW = size.width / 2
val middleH = size.height / 2
drawLine(Color.Gray, Offset(0f, middleH), Offset(size.width - 1, middleH))
drawLine(Color.Gray, Offset(middleW, 0f), Offset(middleW, size.height - 1))
val points1 = getSinusoidalPoints(size)
drawPoints(
brush = Brush.linearGradient(
colors = listOf(Color.Red, Color.Green)
),
points = points1,
cap = StrokeCap.Round,
pointMode = PointMode.Points,
strokeWidth = 10f
)
val points2 = getSinusoidalPoints(size, 100f)
drawPoints(
brush = Brush.linearGradient(
colors = listOf(Color.Green, Color.Magenta)
),
points = points2,
cap = StrokeCap.Round,
pointMode = PointMode.Lines,
strokeWidth = 10f
)
val points3 = getSinusoidalPoints(size, 200f)
drawPoints(
brush = Brush.linearGradient(
colors = listOf(Color.Red, Color.Yellow)
),
points = points3,
cap = StrokeCap.Round,
pointMode = PointMode.Polygon,
strokeWidth = 10f
)
}
}
fun getSinusoidalPoints(size: Size, horizontalOffset: Float = 0f): MutableList<Offset> {
val points = mutableListOf<Offset>()
val verticalCenter = size.height / 2
for (x in 0 until size.width.toInt() step 20) {
val y = (sin(x * (2f * PI / size.width)) * verticalCenter + verticalCenter).toFloat()
points.add(Offset(x.toFloat() + horizontalOffset, y))
}
return points
}
drawArc
@Composable
fun DrawNegativeArc() {
var startAngle by remember {
mutableStateOf(0f) }
var sweepAngle by remember {
mutableStateOf(60f) }
var useCenter by remember {
mutableStateOf(true) }
Canvas(modifier = canvasModifier) {
val canvasWidth = size.width
val canvasHeight = size.height
drawArc(
color = Red400,
startAngle,
sweepAngle,
useCenter,
topLeft = Offset((canvasWidth - canvasHeight) / 2, 0f),
size = Size(canvasHeight, canvasHeight)
)
}
Column(modifier = Modifier.padding(horizontal = 20.dp)) {
Text(text = "StartAngle ${
startAngle.roundToInt()}")
Slider(
value = startAngle,
onValueChange = {
startAngle = it },
valueRange = -180f..180f,
)
Text(text = "SweepAngle ${
sweepAngle.roundToInt()}")
Slider(
value = sweepAngle,
onValueChange = {
sweepAngle = it },
valueRange = -180f..180f,
)
CheckBoxWithTextRippleFullRow(label = "useCenter", useCenter) {
useCenter = it
}
}
}
在上面的代码中,需要留意的一点是drawArc
函数中的startAngle
和sweepAngle
参数,它们的值正值代表的是顺时针方向,而负值代表的是逆时针方向的。
通过多个drawArc
绘制饼图:
@Composable
private fun DrawMultipleArcs() {
var startAngleBlue by remember {
mutableStateOf(0f) }
var sweepAngleBlue by remember {
mutableStateOf(120f) }
var startAngleRed by remember {
mutableStateOf(120f) }
var sweepAngleRed by remember {
mutableStateOf(120f) }
var startAngleGreen by remember {
mutableStateOf(240f) }
var sweepAngleGreen by remember {
mutableStateOf(120f) }
var isFill by remember {
mutableStateOf(true) }
Canvas(modifier = canvasModifier) {
val canvasWidth = size.width
val canvasHeight = size.height
val arcHeight = canvasHeight - 20.dp.toPx()
val arcStrokeWidth = 10.dp.toPx()
val style = if (isFill) Fill else Stroke(arcStrokeWidth)
drawArc(
color = Blue400,
startAngleBlue,
sweepAngleBlue,
true,
topLeft = Offset(
(canvasWidth - canvasHeight) / 2,
(canvasHeight - arcHeight) / 2
),
size = Size(arcHeight, arcHeight),
style = style
)
drawArc(
color = Red400,
startAngleRed,
sweepAngleRed,
true,
topLeft = Offset(
(canvasWidth - canvasHeight) / 2,
(canvasHeight - arcHeight) / 2
),
size = Size(arcHeight, arcHeight),
style = style
)
drawArc(
color = Green400,
startAngleGreen,
sweepAngleGreen,
true,
topLeft = Offset(
(canvasWidth - canvasHeight) / 2,
(canvasHeight - arcHeight) / 2
),
size = Size(arcHeight, arcHeight),
style = style
)
}
CheckBoxWithTextRippleFullRow(label = "Fill Style", isFill) {
isFill = it
}
Column(modifier = Modifier.padding(horizontal = 20.dp)) {
Text(text = "StartAngle ${
startAngleBlue.roundToInt()}", color = Blue400)
Slider(
value = startAngleBlue,
onValueChange = {
startAngleBlue = it },
valueRange = 0f..360f,
)
Text(text = "SweepAngle ${
sweepAngleBlue.roundToInt()}", color = Blue400)
Slider(
value = sweepAngleBlue,
onValueChange = {
sweepAngleBlue = it },
valueRange = 0f..360f,
)
}
Column(modifier = Modifier.padding(horizontal = 20.dp)) {
Text(text = "StartAngle ${
startAngleRed.roundToInt()}", color = Red400)
Slider(
value = startAngleRed,
onValueChange = {
startAngleRed = it },
valueRange = 0f..360f,
)
Text(text = "SweepAngle ${
sweepAngleRed.roundToInt()}", color = Red400)
Slider(
value = sweepAngleRed,
onValueChange = {
sweepAngleRed = it },
valueRange = 0f..360f,
)
}
Column(modifier = Modifier.padding(horizontal = 20.dp)) {
Text(text = "StartAngle ${
startAngleGreen.roundToInt()}", color = Green400)
Slider(
value = startAngleGreen,
onValueChange = {
startAngleGreen = it },
valueRange = 0f..360f,
)
Text(text = "SweepAngle ${
sweepAngleGreen.roundToInt()}", color = Green400)
Slider(
value = sweepAngleGreen,
onValueChange = {
sweepAngleGreen = it },
valueRange = 0f..360f,
)
}
}
drawPath
@Composable
fun DrawPath() {
val path1 = remember {
Path() }
val path2 = remember {
Path() }
Canvas(modifier = canvasModifier) {
// Since we remember paths from each recomposition we reset them to have fresh ones
// You can create paths here if you want to have new path instances
path1.reset()
path2.reset()
path1.moveTo(100f, 100f)
// Draw a line from top right corner (100, 100) to (100,300)
path1.lineTo(100f, 300f)
// Draw a line from (100, 300) to (300,300)
path1.lineTo(300f, 300f)
// Draw a line from (300, 300) to (300,100)
path1.lineTo(300f, 100f)
// Draw a line from (300, 100) to (100,100)
path1.lineTo(100f, 100f)
// Using relatives to draw blue path, relative is based on previous position of path
path2.relativeMoveTo(100f, 100f)
// Draw a line from (100,100) from (100, 300)
path2.relativeLineTo(0f, 200f)
// Draw a line from (100, 300) to (300,300)
path2.relativeLineTo(200f, 0f)
// Draw a line from (300, 300) to (300,100)
path2.relativeLineTo(0f, -200f)
// Draw a line from (300, 100) to (100,100)
path2.relativeLineTo(-200f, 0f)
// Add rounded rectangle to path1
path1.addRoundRect(
RoundRect(
left = 400f,
top = 200f,
right = 600f,
bottom = 400f,
topLeftCornerRadius = CornerRadius(10f, 10f),
topRightCornerRadius = CornerRadius(30f, 30f),
bottomLeftCornerRadius = CornerRadius(50f, 20f),
bottomRightCornerRadius = CornerRadius(0f, 0f)
)
)
// Add rounded rectangle to path2
path2.addRoundRect(
RoundRect(
left = 700f,
top = 200f,
right = 900f,
bottom = 400f,
radiusX = 20f,
radiusY = 20f
)
)
path1.addOval(Rect(left = 400f, top = 50f, right = 500f, bottom = 150f))
path2.addArc(
Rect(400f, top = 50f, right = 500f, bottom = 150f),
startAngleDegrees = 0f,
sweepAngleDegrees = 180f
)
drawPath(
color = Color.Red,
path = path1,
style = Stroke(
width = 2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f))
)
)
drawPath(
color = Color.Blue,
path = path2,
style = Stroke(
width = 2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 10f))
)
)
}
}
path.arcTo
@Composable
fun DrawArcToPath() {
val path1 = remember {
Path() }
val path2 = remember {
Path() }
var startAngle by remember {
mutableStateOf(0f) }
var sweepAngle by remember {
mutableStateOf(90f) }
Canvas(modifier = canvasModifier) {
// Since we remember paths from each recomposition we reset them to have fresh ones
// You can create paths here if you want to have new path instances
path1.reset()
path2.reset()
val rect = Rect(0f, 0f, size.width, size.height)
path1.addRect(rect)
path2.arcTo(
rect,
startAngleDegrees = startAngle,
sweepAngleDegrees = sweepAngle,
forceMoveTo = false
)
drawPath(
color = Color.Red,
path = path1,
style = Stroke(
width = 2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f))
)
)
drawPath(
color = Color.Blue,
path = path2,
style = Stroke(width = 2.dp.toPx())
)
}
Column(modifier = Modifier.padding(horizontal = 20.dp)) {
Text(text = "StartAngle ${
startAngle.roundToInt()}")
Slider(
value = startAngle,
onValueChange = {
startAngle = it },
valueRange = -360f..360f,
)
Text(text = "SweepAngle ${
sweepAngle.roundToInt()}")
Slider(
value = sweepAngle,
onValueChange = {
sweepAngle = it },
valueRange = -360f..360f,
)
}
}
DrawTicketPath
@Composable
private fun DrawTicketPathWithArc() {
Canvas(modifier = canvasModifier) {
val canvasWidth = size.width
val canvasHeight = size.height
// Black background
val ticketBackgroundWidth = canvasWidth * .8f
val horizontalSpace = (canvasWidth - ticketBackgroundWidth) / 2
val ticketBackgroundHeight = canvasHeight * .8f
val verticalSpace = (canvasHeight - ticketBackgroundHeight) / 2
// Get ticket path for background
val path1 = ticketPath(
topLeft = Offset(horizontalSpace, verticalSpace),
size = Size(ticketBackgroundWidth, ticketBackgroundHeight),
cornerRadius = 20.dp.toPx()
)
drawPath(path1, color = Color.Black)
// Dashed path in foreground
val ticketForegroundWidth = ticketBackgroundWidth * .95f
val horizontalSpace2 = (canvasWidth - ticketForegroundWidth) / 2
val ticketForegroundHeight = ticketBackgroundHeight * .9f
val verticalSpace2 = (canvasHeight - ticketForegroundHeight) / 2
// Get ticket path for background
val path2 = ticketPath(
topLeft = Offset(horizontalSpace2, verticalSpace2),
size = Size(ticketForegroundWidth, ticketForegroundHeight),
cornerRadius = 20.dp.toPx()
)
drawPath(
path2,
color = Color.Red,
style = Stroke(
width = 2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(20f, 20f)
)
)
)
}
}
/**
* Create a ticket path with given size and corner radius in px with offset [topLeft].
*
* Refer [this link](https://juliensalvi.medium.com/custom-shape-with-jetpack-compose-1cb48a991d42)
* for implementation details.
*/
fun ticketPath(topLeft: Offset = Offset.Zero, size: Size, cornerRadius: Float): Path {
return Path().apply {
reset()
// Top left arc
arcTo(
rect = Rect(
left = topLeft.x + -cornerRadius,
top = topLeft.y + -cornerRadius,
right = topLeft.x + cornerRadius,
bottom = topLeft.y + cornerRadius
),
startAngleDegrees = 90.0f,
sweepAngleDegrees = -90.0f,
forceMoveTo = false
)
lineTo(x = topLeft.x + size.width - cornerRadius, y = topLeft.y)
// Top right arc
arcTo(
rect = Rect(
left = topLeft.x + size.width - cornerRadius,
top = topLeft.y + -cornerRadius,
right = topLeft.x + size.width + cornerRadius,
bottom = topLeft.y + cornerRadius
),
startAngleDegrees = 180.0f