Top

Use SkiaSharp and Xamarin Forms to create your own bindable controls

Use SkiaSharp and Xamarin Forms to create your own bindable controls

Working with Xamarin Forms is great, as it’s easy to create user interfaces that work in any of the supported platforms. It also provides mappings for most common controls such as labels, images, buttons, etc. keeping each platform look and feel. We can create complex controls in our applications combining these elements, that also could be reusable along our application or even be reused in many projects.

 

In this article, we are going to present a way to create controls using SkiaSharp, as sometimes our projects require some non-standard controls as plots, elements with gradients, etc. As you may know, SkiaSharp is a 2D drawing library over Skia graphics, works on .NET and we can use it from C# 😊. To use SkiaSharp from Xamarin Forms, just add SkiaSharp.Views.Forms package via NuGet.

 

In our sample, we just created a very simple bar to represent a progress of a given value, having the following requirements:

  • Should draw the progress value using a gradient.
  • The user can show or hide a label indicating the current value.
  • User can change the way line is drawn, making it rounded or not.
  • As this control should be reused, we want to have, for example, the possibility to change the colors using Bindable properties.

 

You can take a look to the sample right here: https://github.com/sescalada/SkiaSharpSample

 

It’s easy, just consider the following idea:

  • You need to extend the class SKCanvasView and override OnPaintSurface to create all your canvas drawing stuff.
public class GradientBar : SKCanvasView
  • Create as many Bindable properties as you need. In this case, we created Progress, StrokeWidth, BackColor, FrontColor, DrawLabel and UserRoundedBorders. In this case, all properties work only in one way, but of course you could create some logic to make it two-way bindable, for example, if user could interact within your control.
public static readonly BindableProperty ProgressProperty =
    BindableProperty.Create(nameof(Progress), typeof(float), typeof(GradientBar), 0f, propertyChanged: OnPropertyChanged);

public static readonly BindableProperty StrokeWidthProperty =
    BindableProperty.Create(nameof(StrokeWidth), typeof(float), typeof(GradientBar), 24f, propertyChanged: OnPropertyChanged);

public static readonly BindableProperty BackColorProperty =
    BindableProperty.Create(nameof(BackColor), typeof(Color), typeof(GradientBar), Color.Black, propertyChanged: OnPropertyChanged);

public static readonly BindableProperty FrontColorFromProperty =
    BindableProperty.Create(nameof(FrontColorFrom), typeof(Color), typeof(GradientBar), Color.Yellow, propertyChanged: OnPropertyChanged);

public static readonly BindableProperty FrontColorToProperty =
   BindableProperty.Create(nameof(FrontColorTo), typeof(Color), typeof(GradientBar), Color.Orange, propertyChanged: OnPropertyChanged);

public static readonly BindableProperty DrawLabelProperty =
    BindableProperty.Create(nameof(DrawLabel), typeof(bool), typeof(GradientBar), false, propertyChanged: OnPropertyChanged);

public static readonly BindableProperty UseRoundedBordersProperty =
    BindableProperty.Create(nameof(UseRoundedBorders), typeof(bool), typeof(GradientBar), true, propertyChanged: OnPropertyChanged);

public float Progress
{
    get => (float)GetValue(ProgressProperty);
    set => SetValue(ProgressProperty, value);
}

public float StrokeWidth
{
    get => (float)GetValue(StrokeWidthProperty);
    set => SetValue(StrokeWidthProperty, value);
}

public Color BackColor
{
    get => (Color)GetValue(BackColorProperty);
    set => SetValue(BackColorProperty, value);
}

public Color FrontColorFrom
{
    get => (Color)GetValue(FrontColorFromProperty);
    set => SetValue(FrontColorFromProperty, value);
}

public Color FrontColorTo
{
    get => (Color)GetValue(FrontColorToProperty);
    set => SetValue(FrontColorToProperty, value);
}

public bool DrawLabel
{
    get => (bool)GetValue(DrawLabelProperty);
    set => SetValue(DrawLabelProperty, value);
}

public bool UseRoundedBorders
{
    get => (bool)GetValue(UseRoundedBordersProperty);
    set => SetValue(UseRoundedBordersProperty, value);
}
  • Each bindable property, in this case, makes use of a callback when its value changes. The callback is shared as we just invoke InvalidateSurface to force a canvas repaint. This will provoke OnPaintSource code to be executed.
private static void OnPropertyChanged(BindableObject bindable, object oldVal, object newVal)
{
    var bar = bindable as GradientBar;
    bar?.InvalidateSurface();
}
  • If you take a look to OnPaintSurface code, one of the first things we do is to clear the canvas, and then start drawing. In this case, we draw the silver back line, then the gradient line over it and finally, if user requires it, we draw a label indicating the value of our progress.
protected override void OnPaintSurface(SKPaintSurfaceEventArgs args)
{
    base.OnPaintSurface(args);

    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();
    canvas.Save();

    DrawBackLine(info, canvas);
    DrawFrontLine(info, canvas);
    DrawLabelIfRequired(info, canvas);

    canvas.Restore();
}

 

Of course, this is a simple scenario, but playing with this idea and Skia API, you could create beautiful controls such as plots, circular progress bars, etc. that will work and render the same in any platform, a good thing if our applications need to cover Android, iOS and UWP platform, for example 😊.

Sergio Escalada
No Comments

Post a Comment