The basics to create custom Xamarin.Forms controls using SkiaSharp
While Xamarin.Forms provides a wide range of native controls, many apps require special controls that are not part of the native kit. SkiaSharp is a cross platform library to directly draw on the UI canvas and makes it possible to create complete custom controls. This post will explain basic techniques you will propably need when creating your first control.
- What is SkiaSharp
- SkiaSharp basics
- User input
- Bindable properties, commands and events
All samples of this post can be found in this repository.
This article is part of the Xamarin UI July 2019 where a new Xamarin UI related article is published each day of July. You can see the complete line-up here.
What is SkiaSharp?
First things first, what even is this SkiaSharp thing? Simply said, SkiaSharp is a 2D graphics library that provides a rich API to basically draw fancy things on the UI canvas. The name comes from the Skia Graphics Library which is a project by Google and serves as the graphic engine for products like Chrome, Android, Firefox or Flutter. Similiar to Xamarin.Forms, Skia has different backends where a common API is exposed that we can use. The most important backends for us are the ones powered by the CPU and GPU, namely OpenGL and recently Vulcan. SkiaSharp is a library which provides Xamarin bindings to the Skia API which is mostly written in C++. In order to provide the bindings, Mono maintains a fork of Skia which contains some Xamarin specific logic (you can find the fork here). The actual bindings can be found in the SkiaSharp repository. Additionaly, SkiaSharp provides views which abstract some of the Skia logic and takes care of all required lifecycle operations. SkiaSharp is available for all Xamarin platforms and beyond, the repository currently lists the following platforms:
- .NET Standard 1.3
- .NET Core
- Windows Classic Desktop (Windows.Forms / WPF)
- Windows UWP (Desktop / Mobile / Xbox / HoloLens)
Most importantly for this tutorial, SkiaSharp provides Views for Xamarin.Forms we can utilize to create custom controls, so we dont need to create custom renderers ourselfs.
To use SkiaSharp in Xamarin.Forms, you need to install the SkiaSharp.Views.Forms NuGet package in the core project. If you dont use Xamarin.Forms, you just have to install the SkiaSharp.Views package in your platform project.
The basic SkiaSharp.Views.Forms provides two views you can use as a base for your controls: SKCanvasView and SKGLView. The CanvasView uses the CPU accelerated backend while the GLView uses OpenGL and therefore the GPU. You might intuitively think that using the GPU for graphics operations is always the better choice but in fact, OpenGL has a high overhead when creating the GL context. The CanvasView simply allocates the memory it needs and as long as enough CPU power is available, it can render without problems. In theory, the CanvasView should be better suited for less demanding renderings and GlView better for complex renderings but the power of modern smartphones makes these differences mostly unnoticable. I would recommend to simply stick to the CanvasView and switch to the GlView if the rendering gets to complex and you notice performance problems. I will use the CanvasView for the following samples, but all operations work quite similar using the GLView.
To start drawing, you need to hook into the OnPaintSurface method. You can do so by either listening to the PaintSurface event or directly overriding the OnPaintSurface method.
This method is called everytime the surface is invalidated, for example when the view size changes. You can call InvalidateSurface yourself but be sure to clear the old canvas first by using the canvas Clear method.
To draw something you can directly add a point, line, rectangle, circle, rounded rectangle, text or image to the canvas. The position of each shape is defined by SKPoints or SKRects, more about the coordinate system in SkiaSharp later.
This will result in this view.
To create more complex shapes you can create a path which consists of multiple forms and apply the path to the canvas. This path will create a rounded rectangle for example.
Creating paths in code can get quite complex, specially when working with rounded elements. An easy way to create paths is to create the shape in an SVG tool like Incscape or Adobe Illustrator and parse the created path data using the SKPath.ParseSvgPathData method. This makes it really easy to create the Xamarin logo for example.
Using ClipPath, you can mask the area of the canvas where a drawing is applied. This way the X in the Xamarin logo stays empty.
All operations that add a drawing to the canvas require a paint for the form that gets drawn.
The basic paint properties are:
- Color: … The color of the paint
- StrokeWidth: The size of the stroke.
- Style: You can either only draw the path (Stroke), fill the form created by the path (Fill) or fill the form and draw the path (StrokeAndFill) which will add the StrokeWidth to the form.
A very usefull property is IsAntialias which will enable anti-aliasing to smooth the drawing and is specially helpfull when working with rounded forms.
Apart from these basic properties you can create more advanced effects using paints, like maskings, blurs or gradients. One last effect I would like to show is the BlendMode. BlendMode lets you define how the new drawing (source) will interact with the existing drawings (destintion). Let’s say we want to draw the Xamarin logo on a background.
In this case we can:
- Draw the outer form
- Remove the inner X using SKBlendMode.Src (as an alternative to ClipPath)
- Draw the background beneath the logo using SKBlendMode.SrcOut (to be more precise, we fill everything else with the background)
The coordinate system
The coordinates of where something is drawn is mostly represented by SKPoints or SKRects. In Skia, the upper left position in the view has the coordinates x=0; y=0, the lower right position has the coordinates x=Width;y=Height. Another thing to note is that the zero angle when drawing an arc is positioned right to the center. I would advice to draw help points when creating a control to visualize where a position is.
Let’s have a look at the code:
It is important to note that the sizes used in SkiaSharp are defined in device pixels, while Xamarin.Forms uses device independent sizes. Therefore when passing sizes between Xamarin.Forms and your control, you have to scale these sizes. You can do that by calculating the scale factor from the Xamarin.Forms view size and SkiaSharp CanvasSize.
Manipulating the canvas
Instead of transforming every shape it can get very handy to tranform the canvas itself. One example was the ClipPath method which masks the drawable area. Other very usefull methods are Transform (move in x,y direction), Scale or Rotate. When applying a transformation, it will get applied to all drawings after that. To restore the original canvas transformation you can use Save and Restore or put the code in a using block of an SKAutoCanvasRestore instance.
After applying this rotation the arc starts at the top. Another use case for this is to transform an SVG to stretch across the complete canvas by scaling the canvas accordingly. Another thing you can do to draw everything in Xamarin.Forms device independent size and scale the complete canvas up to the device pixel size.
Controls usually recieve some user input, like a button that gets pressed. To make our control interactive, we can use the Xamarin.Forms gestures, native gestures or the SkiaSharp touch event. When working with the Xamarin.Forms touch input you have to account for the different sizes Xamarin.Forms and SkiaSharp use.
One great thing about SkiaSharp is that it provides its own touch event. This is particularly usefull since Xamarin.Forms does not have a gesture for ongoing touch inputs. To enable the event, you have to set the EnableTouchEvent property to true and hook into the OnTouch method.
To continously recieve touch events, you have to set the Handled property of the event parameter to true. The event parameter contains the location of the input and a type which indicates if the touch started, continued, ended or is just a single tap. To adopt the rendering to the input, you can just call InvalidateSurface which will trigger the OnPaintSurface method we override. In the example Canvas.Clear is not called hence a new circle will be drawn on the canvas for each input and the old ones will not get removed (This does not work for SKGLView).
Bindable properties, commands and events
Now this is more of a Xamarin.Forms basic in general, but since it is important when creating a control I would like to shortly explain how to create bindable properties, commands and how to use events to propagate changes in the control. They can get used to bind when using MVVM or when you want to create a style because style setters require bindable properties. Adding an event is useful not only because not everyone works with bindings but also because you can use it in the code behind to adapt the UI in addition to execute some logic when the command is triggered.
A bindable property consists of the property and an accessor field.
This is a sample property for the border color of a button. Lets examine the parameters:
- propertyName: Name of the property accessor.
- returnType: The return type of the property.
- declaringType: Type of the class where the property is declared.
- defaultValue: The default value of the property. Setting this property is important if the value cant be null because you will otherwise get an exception!
- validateValue: A validation expression for the value that gets set. If the expression fails, an exception will be thrown. In the sample, we need a color so we check that the color is not set to null.
- propertyChanged: A delegate that is called whenever the property changes. We can use this action to invalidate the surface to represent the new value.
Since the property has a static context, the action we register is static and we cant directly reference to the control instance. Luckily, the control instance is directly passed in the method as a BindableObject we can cast to our control and call InvalidateSurface on it.
To delegate user input you can use a bindable command and a simple event. It is important to do a null-check before invoking the command or event to avoid exceptions when nobody subscribed.
If you want to dive deeper into SkiaSharp, I would encourage you to check out these links:
- Official SkiaSharp documentation: https://docs.microsoft.com/de-de/xamarin/graphics-games/skiasharp/introduction
- SkiaSharp sample gallery: https://github.com/mono/SkiaSharp/tree/master/samples/Gallery
- MicroCharts, a chart library written with SkiaSharp by Aloïs Deniel: https://github.com/aloisdeniel/Microcharts
- Create Awesome Cross-Platform Animations in Your Mobile App by Andrei Nitescu: https://www.telerik.com/blogs/xamarinforms-skiasharp-create-awesome-cross-platform-animations-in-your-mobile-app
- Kym Phillpotts using SkiaSharp in his post “Xamarin.Forms UI Challenges — Day vs Night”: https://kymphillpotts.com/xamarin-forms-ui-challenge-dayvsnight.html
I hope this post helped you to understand the basics of SkiaSharp and you are now able to create your own controls! You can find all samples and a clickable app here. If there is anything you think is missing in this blog, please let me know and I will make sure to add it.
Finally, I want to give a huge thank you to Matthew Leibowitz who maintains the SkiaSharp repository and was kind enough to answer some questions about SkiaSharp and Skia in general.
Thank you for reading! :)