Canvas
The Canvas component provides a means for drawing simple 2D graphics. It has many uses, including drawing decorations, backgrounds, data visualization, and animation.
Conceptsβ
Canvasβ
A top-level element, which defines the drawing area. All drawing operations are executed with enabled anti-aliasing.
Hardware Accelerationβ
Drawing on a Canvas component is hardware accelerated by default.
On Android, the following features are not hardware accelerated prior to API 28:
BlendingMode.Darken
BlendingMode.Lighten
BlendingMode.Overlay
Shadow
In order to turn off hardware acceleration, you can pass CanvasLayerType.Software
as an argument to CanvasComponent
. By default, CanvasLayerType.Auto
is used, which means that hardware acceleration will be automatically disabled on Android versions older than API 28 if any of the features from the list above is used. For more information about hardware acceleartion on Android, see the Android drawing models page at developer.android.com.
Coordinate systemβ
The origin of the Canvas coordinate system is in the top-left corner. Y axis is pointing down and X axis is pointing right.
Groupβ
A group allows for grouping of the drawing commands and applying clip/transform to the whole group at once. Groups can be nested within each other and, in contrast to Layer, are not clipped to their bounds by default (there is an attribute that allows for changing that behavior).
Layerβ
A Layer is similar to Group but allocates and redirects drawing to an offscreen rendering target. Elements drawn outside of a Layer region wonβt be visible. It may be useful in such cases as applying transparency or blending modes to a group of drawing commands. Layers should be as small as possible and should be used only when necessary (especially on Android) because they may cause performance issues if used incorrectly.
Coordinate system transformationsβ
Canvas enables the application of affine transformation to Groups and Layers to project its coordinate space. The transformation can be used to specify the position, scale, rotation or skew of a Group or a Layer in the parentβs coordinate space and is applied prior to executing drawing commands inside the Group/Layer, or applying the clip mask.
Shapeβ
Canvas uses shapes to describe what should be drawn on the screen.
A shape is one of:
- Line
- Rect
- Circle
- Ellipse
- Arc
Pathβ
A Path is a special type of Shape. It describes complex geometric shapes consisting of straight line segments, quadratic curves, and cubic curves. It can be drawn on a Canvas, or it can be used to describe the clipping region of a Group or Layer.
Drawingβ
Canvas supports two drawing operations:
- Fill - draws a filled shape
- Stroke - draws a stroked (outlined) shape
Shadingβ
Shading defines the way the pixels of the shape will be drawn.
Canvas supports two shading variants:
- solid color
- gradient (linear or radial)
Examplesβ
Fill shape with color and gradientβ
class DrawShapeCanvasKComponent : KComponent() {
override fun ComponentScope.render(): Component {
return Column {
child(
CanvasComponent(style = Style.widthPercent(100f).height(100.px)) {
// left circle
fill(
shape = Shape.circle(center = Point(40f, 40f), radius = 40f),
shading = Shading.solidColor(Color.RED))
// right circle
fill(
shape = Shape.circle(center = Point(140f, 40f), radius = 40f),
shading =
Shading.linearGradient(
gradient = Gradient(Color.RED, Color.GREEN, Color.BLUE),
startPoint = Point.Zero,
endPoint = Point(180f, 0f)))
})
}
}
}
Fill path with color and gradientβ
class DrawPathCanvasKComponent : KComponent() {
override fun ComponentScope.render(): Component {
// Using useCached to avoid recreating Path object on state updates
val heartPath = useCached {
// A heart shape, the path data taken from some random svg found online
Path {
add(
Path {
moveTo(Point(75f, 40f))
cubicTo(Point(75f, 37f), Point(70f, 25f), Point(50f, 25f))
cubicTo(Point(20f, 25f), Point(20f, 62.5f), Point(20f, 62.5f))
cubicTo(Point(20f, 80f), Point(40f, 102f), Point(75f, 120f))
cubicTo(Point(110f, 102f), Point(130f, 80f), Point(130f, 62.5f))
cubicTo(Point(130f, 62.5f), Point(130f, 25f), Point(100f, 25f))
cubicTo(Point(85f, 25f), Point(75f, 37f), Point(75f, 40f))
},
// the heart path starts at 20,25 so translate it to make it start at 0,0 in order to
// make
// positioning easier
Transform { translate(dx = -20f, dy = -25f) })
}
}
return Column {
child(
CanvasComponent(style = Style.widthPercent(100f).height(100.px)) {
// left heart
fill(shape = Shape.path(heartPath), shading = Shading.solidColor(Color.RED))
// right heart translated using a group
group(transform = Transform { translate(dx = 120f) }) {
fill(
shape = Shape.path(heartPath),
shading =
Shading.linearGradient(
gradient = Gradient(Color.RED, Color.GREEN, Color.BLUE),
startPoint = Point.Zero,
endPoint = Point(110f, 0f)))
}
})
}
}
}
Stroke shape with color and gradientβ
class DrawStrokedShapeCanvasKComponent : KComponent() {
override fun ComponentScope.render(): Component {
return Column {
child(
CanvasComponent(style = Style.widthPercent(100f).height(100.px)) {
// first ellipse
stroke(
shape = Shape.ellipse(Point(10f, 10f), Size(60f, 30f)),
shading = Shading.solidColor(Color.RED),
lineWidth = 4f,
dashLengths = floatArrayOf(8f, 4f))
// second ellipse
stroke(
shape = Shape.ellipse(Point(90f, 10f), Size(60f, 30f)),
shading =
Shading.linearGradient(
gradient = Gradient(Color.RED, Color.GREEN, Color.BLUE),
startPoint = Point(90f, 0f),
endPoint = Point(150f, 0f)),
lineWidth = 4f,
dashLengths = floatArrayOf(8f, 4f))
})
}
}
}
Rotate a Groupβ
class DrawRotatedGroupCanvasKComponent : KComponent() {
override fun ComponentScope.render(): Component {
return Column {
child(
CanvasComponent(style = Style.widthPercent(100f).height(100.px)) {
group(transform = Transform { rotate(degrees = 45f, pivot = Point(50f, 20f)) }) {
// first square
fill(
shape = Shape.rect(topLeft = Point(40f, 10f), size = Size(40f, 40f)),
shading = Shading.solidColor(Color.RED))
// second square
fill(
shape = Shape.rect(topLeft = Point(90f, 10f), size = Size(40f, 40f)),
shading = Shading.solidColor(Color.GREEN))
}
})
}
}
}
Cut a hole in a rectangleβ
class DrawTransparentHoleCanvasKComponent : KComponent() {
override fun ComponentScope.render(): Component {
return Column {
child(
CanvasComponent(style = Style.widthPercent(100f).height(100.px)) {
// gradient background
fill(
// this.size returns the size of the current drawing scope, in this case it'll be
// the size of the canvas
shape = Shape.rect(topLeft = Point.Zero, size = size),
shading =
Shading.linearGradient(
gradient = Gradient(Color.RED, Color.GREEN, Color.BLUE),
startPoint = Point.Zero,
endPoint = Point(size.width, 0f)))
layer(
transform =
Transform { translate(dx = size.width * 0.1f, dy = size.height * 0.1f) },
size = size * 0.8f // 80% of the canvas size
) {
// layer background
fill(
shape = Shape.rect(topLeft = Point.Zero, size = size),
shading = Shading.solidColor(Color.CYAN))
// ellipse with xor blending mode
fill(
shape =
Shape.ellipse(
topLeft = Point(size.width * 0.1f, size.height * 0.1f),
size = size * 0.8f),
shading = Shading.solidColor(Color.BLACK),
blendingMode = BlendingMode.Xor)
}
})
}
}
}
Animationsβ
class DrawAnimatedSquareCanvasKComponent : KComponent() {
override fun ComponentScope.render(): Component {
val rotation = useState { 0f }
val animator = useRef<ValueAnimator?> { null }
val startAnimator: (ClickEvent) -> Unit = {
animator.value?.cancel()
animator.value =
ValueAnimator.ofFloat(0f, 360f).apply {
duration = 2000
interpolator = LinearInterpolator()
repeatCount = ValueAnimator.INFINITE
addUpdateListener { rotation.update(it.animatedValue as Float) }
}
animator.value?.start()
}
return Column {
child(
CanvasComponent(
style = Style.widthPercent(100f).height(100.px).onClick(action = startAnimator)) {
group(
transform =
Transform {
translate(dx = size.center.x, dy = size.center.y)
rotate(rotation.value, pivot = size.center)
}) {
// draw square
fill(
shape = Shape.rect(topLeft = Point(-30f, -30f), size = Size(60f, 60f)),
shading = Shading.solidColor(Color.RED))
}
})
}
}
}