Skip to main content

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