Introduction
When creating an Uno Platform application, developers might want to create elaborate 2D graphics using a library such as Skia or Cairo, rather than using, for example, a simple Canvas. To support this use case, SkiaSharp comes with an SKXamlCanvas element that allows for drawing in an area using SkiaSharp.
On Uno Platform Skia targets, we can utilize the pre-existing internal Skia canvas used to render the application window instead of creating additional Skia surfaces. Unlike SKXamlCanvas
which doesn't support yet hardware acceleration on Skia targets, hardware acceleration comes out of the box if the Uno application is already using OpenGL to render. Moreover, SKXamlCanvas
has to make additional buffer copying, which can be skipped with this implementation.
Important
This functionality is only available on Skia targets.
SKCanvasElement
SKCanvasElement
is an abstract FrameworkElement
for 2D drawing with Skia. To use SKCanvasElement
, create a subclass of SKCanvasElement
and override the RenderOverride
method, which takes the canvas that will be drawn on and the clipping area inside the canvas.
protected abstract void RenderOverride(SKCanvas canvas, Size area);
When adding your drawing logic in RenderOverride
on the provided canvas, you can assume that the origin is already translated so that 0,0
is the origin of the element, not the entire window. Drawing outside this area will be clipped.
Additionally, SKCanvasElement
has an Invalidate
method that can be used at any time to tell the Uno Platform runtime to redraw the window, calling RenderOverride
in the process.
Since SKCanvasElement
is just a FrameworkElement, controlling the dimensions of the drawing area is done by manipulating the layout of the element, e.g. by overriding MeasureOverride and ArrangeOverride.
Full example
To see this in action, here's a complete sample that uses SKCanvasElement
to draw 1 of 3 different drawings based on the value of a Slider. Note how you have to be careful with surrounding all the Skia-related logic in platform-specific guards. This is the case for both the XAML and the code-behind.
XAML:
<!-- SKCanvasElementExample.xaml -->
<UserControl x:Class="BlankApp.SKCanvasElementExample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:BlankApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:skia="http://uno.ui/skia"
xmlns:not_skia="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
mc:Ignorable="skia">
<Grid>
<skia:Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Slider Grid.Row="0" x:Name="slider" Header="Sample" Minimum="0" Maximum="{x:Bind MaxSampleIndex}" />
<local:SKCanvasElementImpl Grid.Row="1" Sample="{x:Bind slider.Value, Mode=OneWay}" />
</skia:Grid>
<not_skia:TextBlock Text="This sample is only supported on skia." />
</Grid>
</UserControl>
Code-behind:
// SKCanvasElementExample.xaml.cs
using Uno.UI.Samples.Controls;
using Microsoft.UI.Xaml.Controls;
namespace BlankApp
{
public sealed partial class SKCanvasElement_Simple : UserControl
{
#if HAS_UNO_SKIA
public int MaxSampleIndex => SKCanvasElementImpl.SampleCount - 1;
#endif
public SKCanvasElement_Simple()
{
this.InitializeComponent();
}
}
}
// SKCanvasElementImpl.skia.cs <-- NOTICE the `.skia`
using System;
using Windows.Foundation;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using SkiaSharp;
namespace BlankApp;
public class SKCanvasElementImpl : SKCanvasElement
{
public static int SampleCount => 3;
public static DependencyProperty SampleProperty { get; } = DependencyProperty.Register(
nameof(Sample),
typeof(int),
typeof(SKCanvasElementImpl),
new PropertyMetadata(0, (o, args) => ((SKCanvasElementImpl)o).SampleChanged((int)args.NewValue)));
public int Sample
{
get => (int)GetValue(SampleProperty);
set => SetValue(SampleProperty, value);
}
private void SampleChanged(int newIndex)
{
Sample = Math.Min(Math.Max(0, newIndex), SampleCount - 1);
}
protected override void RenderOverride(SKCanvas canvas, Size area)
{
var minDim = Math.Min(area.Width, area.Height);
// rescale to fit the given area, assuming each drawing is 260x260
canvas.Scale((float)(minDim / 260), (float)(minDim / 260));
switch (Sample)
{
case 0:
SkiaDrawing0(canvas);
break;
case 1:
SkiaDrawing1(canvas);
break;
case 2:
SkiaDrawing2(canvas);
break;
}
}
// https://fiddle.skia.org/c/@shapes
private void SkiaDrawing0(SKCanvas canvas)
{
var paint = new SKPaint();
paint.Style = SKPaintStyle.Fill;
paint.IsAntialias = true;
paint.StrokeWidth = 4;
paint.Color = new SKColor(0xff4285F4);
var rect = SKRect.Create(10, 10, 100, 160);
canvas.DrawRect(rect, paint);
var oval = new SKPath();
oval.AddRoundRect(rect, 20, 20);
oval.Offset(new SKPoint(40, 80));
paint.Color = new SKColor(0xffDB4437);
canvas.DrawPath(oval, paint);
paint.Color = new SKColor(0xff0F9D58);
canvas.DrawCircle(180, 50, 25, paint);
rect.Offset(80, 50);
paint.Color = new SKColor(0xffF4B400);
paint.Style = SKPaintStyle.Stroke;
canvas.DrawRoundRect(rect, 10, 10, paint);
}
// https://fiddle.skia.org/c/@bezier_curves
private void SkiaDrawing1(SKCanvas canvas)
{
var paint = new SKPaint();
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 8;
paint.Color = new SKColor(0xff4285F4);
paint.IsAntialias = true;
paint.StrokeCap = SKStrokeCap.Round;
var path = new SKPath();
path.MoveTo(10, 10);
path.QuadTo(256, 64, 128, 128);
path.QuadTo(10, 192, 250, 250);
canvas.DrawPath(path, paint);
}
// https://fiddle.skia.org/c/@shader
private void SkiaDrawing2(SKCanvas canvas)
{
var paint = new SKPaint();
using var pathEffect = SKPathEffect.CreateDiscrete(10.0f, 4.0f);
paint.PathEffect = pathEffect;
SKPoint[] points =
{
new SKPoint(0.0f, 0.0f),
new SKPoint(256.0f, 256.0f)
};
SKColor[] colors =
{
new SKColor(66, 133, 244),
new SKColor(15, 157, 88)
};
paint.Shader = SKShader.CreateLinearGradient(points[0], points[1], colors, SKShaderTileMode.Clamp);
paint.IsAntialias = true;
var path = Star();
canvas.DrawPath(path, paint);
SKPath Star()
{
const float R = 60.0f, C = 128.0f;
var path = new SKPath();
path.MoveTo(C + R, C);
for (var i = 1; i < 15; ++i)
{
var a = 0.44879895f * i;
var r = R + R * (i % 2);
path.LineTo((float)(C + r * Math.Cos(a)), (float)(C + r * Math.Sin(a)));
}
return path;
}
}
}