Canvas & Drawing
Write an ordinary Compose Canvas with real draw commands (shapes, paths, gradients, transforms, and text), fully sandboxed and rendered natively from a KBC bundle.
As of 0.4.13-alpha, KBC ships a full server-driven drawing surface.
Write an ordinary Compose Canvas { ... } with real draw commands
(shapes, paths, gradients, transforms, and text) and it renders natively
on device. The whole surface is sandboxed: there is no
nativeCanvas and no android.graphics escape hatch.
A first Canvas
@KetoyComposable
fun PieChart() {
Canvas(modifier = Modifier.size(160.dp)) {
val radius = size.minDimension / 2f
val mid = center
drawArc(
color = Color(0xFF6200EE),
startAngle = 0f,
sweepAngle = 270f,
useCenter = false,
style = Stroke(width = 24f, cap = StrokeCap.Round),
)
drawCircle(color = Color.White, radius = radius / 3f, center = mid)
}
}Everything inside the Canvas lambda runs against a sandboxed
DrawScope. Geometry reads (size, center), value types, and numeric
math all work.
What's available
Shapes
drawRect, drawCircle, drawLine, drawArc, drawRoundRect,
drawOval, and drawPath. Each accepts a color or a brush
(gradient) overload.
drawRect(color = Color.LightGray, size = size)
drawCircle(color = Color.Red, radius = 40f, center = center)
drawLine(color = Color.Black, start = Offset(0f, 0f), end = size.center())
drawRoundRect(color = Color.Blue, cornerRadius = CornerRadius(12f))Value types
Offset(x, y), Size(w, h), CornerRadius(r),
Stroke(width, cap, join, pathEffect), and Color(0xAARRGGBB).
Paths
Build a path, then draw it with a color or brush:
val triangle = Path().apply {
moveTo(center.x, 0f)
lineTo(size.width, size.height)
lineTo(0f, size.height)
close()
}
drawPath(triangle, brush = Brush.verticalGradient(listOf(Color.Cyan, Color.Blue)))moveTo, lineTo, cubicTo, and close are supported inside
Path().apply { ... }.
Gradients
Brush.linearGradient(...), verticalGradient, horizontalGradient,
and radialGradient.
Path effects
PathEffect.dashPathEffect(...) and cornerPathEffect(...), passed
through Stroke(pathEffect = ...).
Transforms
rotate(deg) { ... }, scale(...) { ... }, translate(...) { ... },
inset(...) { ... }, clipRect { ... }, and clipPath(path) { ... }.
Each one nests a draw block:
rotate(degrees = 45f) {
drawRect(color = Color.Magenta, size = Size(80f, 80f))
}Geometry reads and math
size.width, size.height, size.minDimension, size.maxDimension,
center.x, center.y, plus N.dp.toPx(). Float / Long / Double
arithmetic (size.height / 2f, radius * 0.8f) works inside the draw
block.
Text on a Canvas
Use the real androidx.compose.ui.text.drawText with a
rememberTextMeasurer(). This is idiomatic Compose, with no
Ketoy-specific import:
@KetoyComposable
fun LabeledCanvas() {
val measurer = rememberTextMeasurer()
Canvas(modifier = Modifier.size(200.dp)) {
drawText(
textMeasurer = measurer,
text = "Hello",
topLeft = Offset(16f, 16f),
)
}
}Putting it together
A donut gauge combining an arc track, a gradient progress arc, and a centered label:
@KetoyComposable
fun Gauge(progress: Float) {
val measurer = rememberTextMeasurer()
Canvas(modifier = Modifier.size(180.dp)) {
val stroke = Stroke(width = 20f, cap = StrokeCap.Round)
// Track
drawArc(
color = Color(0xFFE0E0E0),
startAngle = 135f,
sweepAngle = 270f,
useCenter = false,
style = stroke,
)
// Progress
drawArc(
brush = Brush.sweepGradient(listOf(Color.Cyan, Color.Blue)),
startAngle = 135f,
sweepAngle = 270f * progress,
useCenter = false,
style = stroke,
)
drawText(
textMeasurer = measurer,
text = "${(progress * 100).toInt()}%",
topLeft = Offset(center.x - 24f, center.y - 16f),
)
}
}Precautions
Sandboxed only. There is no
drawContext.canvas.nativeCanvasand no directandroid.graphicsaccess. Draw with the DrawScope surface above; anything outside it is a compile-timeDirectAndroidApiAccesserror.
Prefer draw-block math over captures. Geometry reads and numeric math inside the
Canvaslambda compile directly. For values computed outside the block, pass them in as composable parameters (likeprogressabove) rather than relying on captured locals.