Create a Gauge control using SkiaSharp in .NET MAUI

This is my contribution to the MAUI UI July, demonstrating how to easily create a Gauge control using SkiaSharp for your .NET MAUI application.

Sebastian Jensen
8 min readJul 7, 2024

Introduction

In this blog post, I will show you how to easily create your own Gauge control using SkiaSharp for your .NET MAUI application.

SkiaSharp is an open-source 2D graphics library powered by Google’s Skia Graphics Engine. It is designed to provide a comprehensive and efficient API for drawing and manipulating graphics across multiple platforms.

Implement the Gauge control

Open Visual Studio and create a new .NET MAUI application. Open the NuGet Package Manager and update all NuGet packages to ensure your project is using the latest dependencies. Install the SkiaSharp.Views.Maui.Control NuGet package. In my case, I’m not using the preview version, so I’m using the latest stable version, which is 2.88.8 at time of writing this post.

To install the package, you can use the NuGet Package Manager GUI or run the following command in the Package Manager Console:

Install-Package SkiaSharp.Views.Maui.Controls -Version 2.88.8

This will add the necessary SkiaSharp libraries to your project, allowing you to start creating your Gauge control.

Now, open the MauiProgram.cs file and add the .UseSkiaSharp() call to the builder property.

Create a new folder named Controls. In this folder, we will add a new class called GaugeView.

Let’s start by adding the base structure of the file. First, we define some constant values, which will be used later in our class.

public class GaugeView : SKCanvasView
{
// Define the duration of the animation in milliseconds
private const int AnimationDuration = 250;

// Define the sweep angle for each segment of the gauge
private const float SweepAngle = 67.5f;

// Define the number of steps per frame for the animation
private const int StepsPerFrame = AnimationDuration / 16;

// Define the minimum and maximum values for the gauge
private const float MinValue = 0f;
private const float MaxValue = 100f;

// Current animated value of the gauge needle
private float _animatedValue;

// Flag to indicate if an animation is in progress
private bool _isAnimating;

// MORE CODE TO ADD HERE
}

Next, we will add the constructor, which we will use to define a standard width and height for our control.

/// <summary>
/// Initializes a new instance of the <see cref="GaugeView"/> class.
/// </summary>
public GaugeView()
{
WidthRequest = 500;
HeightRequest = 500;
}

The user should be able to update some properties. That is why we specify some bindable properties for our GaugeView class.

/// <summary>
/// Identifies the Value bindable property.
/// </summary>
public static readonly BindableProperty ValueProperty
= BindableProperty.Create(
nameof(Value),
typeof(float),
typeof(GaugeView),
0.0f,
propertyChanged: OnValueChanged);

/// <summary>
/// Gets or sets the value displayed by the gauge.
/// </summary>
public float Value
{
get => (float)GetValue(ValueProperty);
set => SetValue(ValueProperty, Math.Clamp(value, MinValue, MaxValue));
}

/// <summary>
/// Identifies the TextColor bindable property.
/// </summary>
public static readonly BindableProperty TextColorProperty
= BindableProperty.Create(
nameof(TextColor),
typeof(Color),
typeof(GaugeView),
Colors.Black);

/// <summary>
/// Gets or sets the color of the text displayed on the gauge.
/// </summary>
public Color TextColor
{
get => (Color)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value);
}

/// <summary>
/// Identifies the NeedleColor bindable property.
/// </summary>
public static readonly BindableProperty NeedleColorProperty
= BindableProperty.Create(
nameof(NeedleColor),
typeof(Color),
typeof(GaugeView),
Colors.Black);

/// <summary>
/// Gets or sets the color of the gauge needle.
/// </summary>
public Color NeedleColor
{
get => (Color)GetValue(NeedleColorProperty);
set => SetValue(NeedleColorProperty, value);
}

/// <summary>
/// Identifies the NeedleScrewColor bindable property.
/// </summary>
public static readonly BindableProperty NeedleScrewColorProperty
= BindableProperty.Create(
nameof(NeedleScrewColor),
typeof(Color),
typeof(GaugeView),
Colors.DarkGray);

/// <summary>
/// Gets or sets the color of the needle screw.
/// </summary>
public Color NeedleScrewColor
{
get => (Color)GetValue(NeedleScrewColorProperty);
set => SetValue(NeedleScrewColorProperty, value);
}

/// <summary>
/// Identifies the Unit bindable property.
/// </summary>
public static readonly BindableProperty UnitProperty
= BindableProperty.Create(
nameof(Unit),
typeof(string),
typeof(GaugeView),
string.Empty);

/// <summary>
/// Gets or sets the unit of measurement displayed on the gauge.
/// </summary>
public string Unit
{
get => (string)GetValue(UnitProperty);
set => SetValue(UnitProperty, value);
}

/// <summary>
/// Identifies the ValueFontSize bindable property.
/// </summary>
public static readonly BindableProperty ValueFontSizeProperty
= BindableProperty.Create(
nameof(ValueFontSize),
typeof(float),
typeof(GaugeView),
33.0f);

/// <summary>
/// Gets or sets the font size of the value text displayed on the gauge.
/// </summary>
public float ValueFontSize
{
get => (float)GetValue(ValueFontSizeProperty);
set => SetValue(ValueFontSizeProperty, value);
}

Due to the fact that we are using SKCanvasView as our base type, we need to override the OnPaintSurface method. This method will be used to draw our control.

/// <summary>
/// Called when the surface needs to be painted.
/// </summary>
/// <param name="e">The event arguments.</param>
protected override void OnPaintSurface(
SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);

SKCanvas canvas = e.Surface.Canvas;
canvas.Clear();

float width = e.Info.Width;
float height = e.Info.Height;
float size = Math.Min(width, height);

float centerX = width / 2;
float centerY = height / 2;

float scale = size / 210f;

canvas.Translate(centerX, centerY);
canvas.Scale(scale);

DrawBackground(canvas, size);
DrawGauge(canvas);
DrawNeedle(canvas, _animatedValue);
DrawNeedleScrew(canvas);
DrawValueText(canvas);
}

Finally, we need to implement our different draw methods. Let’s start with the DrawBackground method, which is capable of drawing the transparent background of our control.

/// <summary>
/// Draws the background of the gauge.
/// </summary>
/// <param name="canvas">The canvas to draw on.</param>
/// <param name="size">The size of the canvas.</param>
private static void DrawBackground(
SKCanvas canvas,
float size)
{
canvas.DrawRect(new SKRect(-size / 2, -size / 2, size / 2, size / 2),
new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Transparent,
});
}

Next, we will implement the DrawGauge method, which will draw the different segments of the control.

/// <summary>
/// Draws the gauge on the canvas.
/// </summary>
/// <param name="canvas">The canvas to draw on.</param>
private static void DrawGauge(
SKCanvas canvas)
{
SKRect rect = new(-100, -100, 100, 100);
rect.Inflate(-10, -10);

DrawArc(canvas, rect, 135, SweepAngle, SKColors.DarkGray);
DrawArc(canvas, rect, 202.5f, SweepAngle, SKColors.LightGray);
DrawArc(canvas, rect, 270, SweepAngle, SKColors.DarkGray);
DrawArc(canvas, rect, 337.5f, SweepAngle, SKColors.LightGray);
}

/// <summary>
/// Draws an arc on the canvas.
/// </summary>
/// <param name="canvas">The canvas to draw on.</param>
/// <param name="rect">The rectangle bounding the arc.</param>
/// <param name="startAngle">The starting angle of the arc.</param>
/// <param name="sweepAngle">The sweep angle of the arc.</param>
/// <param name="color">The color of the arc.</param>
private static void DrawArc(
SKCanvas canvas,
SKRect rect,
float startAngle,
float sweepAngle,
SKColor color)
{
using SKPath path = new();

path.AddArc(rect, startAngle, sweepAngle);

canvas.DrawPath(path, new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Stroke,
Color = color,
StrokeWidth = 10
});
}

Let’s focus now on drawing the needle and the screw for the gauge. These elements are crucial for representing the current value on the gauge.

/// <summary>
/// Draws the needle of the gauge.
/// </summary>
/// <param name="canvas">The canvas to draw on.</param>
/// <param name="value">The value represented by the needle.</param>
private void DrawNeedle(
SKCanvas canvas,
float value)
{
float angle = -135f + value / 100 * 270f;
canvas.Save();
canvas.RotateDegrees(angle);

SKPaint paint = new()
{
IsAntialias = true,
Color = NeedleColor.ToSKColor()
};

SKPath needlePath = new();
needlePath.MoveTo(0, -76);
needlePath.LineTo(-6, 0);
needlePath.LineTo(6, 0);
needlePath.Close();

canvas.DrawPath(needlePath, paint);
canvas.Restore();
}


/// <summary>
/// Draws the screw at the center of the needle.
/// </summary>
/// <param name="canvas">The canvas to draw on.</param>
private void DrawNeedleScrew(
SKCanvas canvas)
{
canvas.DrawCircle(0, 0, 10, new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
Color = NeedleScrewColor.ToSKColor()
});
}

Now, we’ll implement the methods to display text within the Gauge control. These methods will be responsible for drawing the labels or values on the gauge.

/// <summary>
/// Draws the value text and unit text on the gauge.
/// </summary>
/// <param name="canvas">The canvas to draw on.</param>
private void DrawValueText(
SKCanvas canvas)
{
SKPaint textPaint = new()
{
IsAntialias = true,
Color = TextColor.ToSKColor(),
TextSize = 12f
};

DrawUnitText(canvas, Unit, 95, textPaint);

textPaint.TextSize = ValueFontSize;
DrawUnitText(canvas, _animatedValue.ToString("F2"), 85, textPaint);
}

/// <summary>
/// Draws a unit text on the canvas.
/// </summary>
/// <param name="canvas">The canvas to draw on.</param>
/// <param name="text">The text to draw.</param>
/// <param name="y">The y-coordinate of the text.</param>
/// <param name="paint">The paint to use for drawing the text.</param>
private static void DrawUnitText(
SKCanvas canvas,
string text,
float y,
SKPaint paint)
{
SKRect textBounds = new();
paint.MeasureText(text, ref textBounds);
canvas.DrawText(text, -textBounds.MidX, y - textBounds.Height, paint);
}

Finally, we need to implement the OnValueChanged and AnimateNeedleAsync methods. These methods will be used to animate the needle to the current value.

/// <summary>
/// Called when the value property changes.
/// </summary>
/// <param name="bindable">The bindable object.</param>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
private static async void OnValueChanged(
BindableObject bindable,
object oldValue,
object newValue)
{
if (bindable is GaugeView gaugeView)
{
await gaugeView.AnimateNeedleAsync((float)newValue);
}
}

/// <summary>
/// Animates the needle to a new value.
/// </summary>
/// <param name="toValue">The new value to animate to.</param>
private async Task AnimateNeedleAsync(
float toValue)
{
if (_isAnimating)
{
return;
}

_isAnimating = true;

float stepSize = (toValue - _animatedValue) / StepsPerFrame;
for (int i = 0; i < StepsPerFrame; i++)
{
_animatedValue += stepSize;
InvalidateSurface();
await Task.Delay(16);
}

_animatedValue = toValue;
InvalidateSurface();
_isAnimating = false;
}

Let’s test our control

Now we are ready to use our custom Gauge control. To do this, we need to open the MainPage, remove all existing controls and the code-behind logic, add our controls namespace, and then add buttons to increment and decrement the value of the Gauge control along with the Gauge control itself.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:GaugeControl.Controls"
x:Class="SevenSegmentsMAUI.MainPage">

<VerticalStackLayout Padding="30,0"
Spacing="25">
<Button Text="Decrement"
Clicked="OnDecrementClicked" />

<Button Text="Increment"
Clicked="OnIncrementClicked" />

<controls:GaugeView x:Name="GaugeView"
Unit="My Unit"
NeedleColor="Black"
TextColor="Black" />
</VerticalStackLayout>

</ContentPage>

To handle the increment and decrement actions for our Gauge control, we need to add click event handlers for the buttons in the code-behind file of the MainPage.

private void OnIncrementClicked(object sender, EventArgs e)
{
GaugeView.Value += 10;
}

private void OnDecrementClicked(object sender, EventArgs e)
{
GaugeView.Value -= 10;
}

Here is a screenshot of our Gauge control running on a Windows machine. The control features a needle that moves in response to the value changes triggered by the increment and decrement buttons.

Here is a screenshot of our Gauge control running on an Android phone. The control functions smoothly on mobile platforms, maintaining its visual integrity and responsiveness.

Conclusion

As you have seen, we created a Gauge control using SkiaSharp for our .NET MAUI application. You can find the entire source code in this GitHub repository. Feel free to fork the repository and improve it.

This article is my contribution to #MAUIUIJULY, a series of blog posts where every day in July, a .NET MAUI community member posts something about .NET MAUI and UI. You can find the list of all available blog posts on this website.

For more blog posts on this topic, visit my personal blog at tsjdev-apps.de.

Thank you for following along, and happy coding!

--

--

Sebastian Jensen

Senior Software Developer & Team Lead @ medialesson GmbH