I suck at titles, feel free to change it.
I found a tutorial for making custom controls in Maui using SkiaSharp. I modified it a little by using C# Markup instead of XAML. Except for having to instantiate the progress bar with ProgressBar.Maui.ProgressBar progressBar
instead of ProgressBar progressBar
, everything worked great.
I wanted to modify it a little by limiting the redraws to only happen after a certain progress change threshold was met. (i.e. only redraw every x% progress). I added a button that increments the progress by less then the threshold, so I should have to click the buttons a few times to see the progress bar update. However, the progress bar updates every time I click the button.
If I change the progress manually, like adding progressBar.Progress = 0.01f;
to the end of my page constructor, the progress bar behaves as expected.
Here is all the relevant code [I think]
My library project only has 2 files
ProgressBar.cs
using SkiaSharp;
using SkiaSharp.Views.Maui;
using SkiaSharp.Views.Maui.Controls;
using System.Diagnostics;
namespace ProgressBar.Maui;
// All the code in this file is included in all platforms.
public class ProgressBar : SKCanvasView
{
public static readonly BindableProperty ProgressProperty =
BindableProperty.Create(nameof(Progress), typeof(float), typeof(ProgressBar), 0f, propertyChanged: OnBindablePropertyChanged);
public static readonly BindableProperty ProgressColorProperty =
BindableProperty.Create(nameof(ProgressColor), typeof(Color), typeof(ProgressBar), Colors.CornflowerBlue, propertyChanged: OnBindablePropertyChanged);
public static readonly BindableProperty BaseColorProperty =
BindableProperty.Create(nameof(BaseColor), typeof(Color), typeof(ProgressBar), Colors.LightGray, propertyChanged: OnBindablePropertyChanged);
public float Progress
{
get => (float)GetValue(ProgressProperty);
set
{
if (Math.Abs(value - _previousProgress) >= _RedrawThreshold)
{
SetValue(ProgressProperty, value);
_previousProgress = value;
Debug.WriteLine("Progress updated and surface invalidated.");
}
else
{
Debug.WriteLine("Progress change below threshold. No redraw.");
}
}
}
public Color ProgressColor
{
get => (Color)GetValue(ProgressColorProperty);
set => SetValue(ProgressColorProperty, value);
}
public Color BaseColor
{
get => (Color)GetValue(BaseColorProperty);
set => SetValue(BaseColorProperty, value);
}
// actual canvas instance to draw on
private SKCanvas _canvas;
// rectangle which will be used to draw the Progress Bar
private SKRect _drawRect;
// holds information about the dimensions, etc.
private SKImageInfo _info;
private const float _RedrawThreshold = 0.5f; // 1% change
private float _previousProgress = 0f;
private SKPaint _basePaint = new()
{
Style = SKPaintStyle.Fill,
IsAntialias = true,
};
private SKPaint _progressPaint = new()
{
Style = SKPaintStyle.Fill,
IsAntialias = true,
};
protected override void OnPaintSurface (SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
_canvas = e.Surface.Canvas;
_canvas.Clear();
_info = e.Info;
_drawRect = new SKRect(0, 0, _info.Width, _info.Height);
DrawBase();
DrawProgress();
}
private void DrawBase ()
{
using SKPath basePath = new();
basePath.AddRect(_drawRect);
_basePaint.Color = BaseColor.ToSKColor();
_canvas.DrawPath(basePath, _basePaint);
}
private void DrawProgress ()
{
using SKPath progressPath = new();
var progressRect = new SKRect(0, 0, _info.Width * Progress, _info.Height);
progressPath.AddRect(progressRect);
_progressPaint.Color = ProgressColor.ToSKColor();
_canvas.DrawPath(progressPath, _progressPaint);
}
private static void OnBindablePropertyChanged (BindableObject bindable, object oldValue, object newValue)
{
if (oldValue != newValue)
((ProgressBar)bindable).InvalidateSurface();
}
}
and
Registration.cs
using SkiaSharp.Views.Maui.Handlers;
namespace ProgressBar.Maui;
public static class Registration
{
public static MauiAppBuilder UseProgressBar(this MauiAppBuilder builder)
{
builder.ConfigureMauiHandlers(h =>
{
h.AddHandler<ProgressBar, SKCanvasViewHandler>();
});
return builder;
}
}
My sample project contains
SamplePage.cs
namespace ProgressBarSample.Views;
public partial class SamplePage : ContentPage
{
public SamplePage (SampleViewModel viewModel)
{
BindingContext = viewModel;
Content = new VerticalStackLayout
{
Spacing = 30,
Padding = new Thickness(30, 0),
VerticalOptions = LayoutOptions.Center,
Children =
{
new ProgressBar.Maui.ProgressBar()
{
WidthRequest = 300,
HeightRequest = 5,
ProgressColor = Colors.DeepSkyBlue,
}
.Assign(out ProgressBar.Maui.ProgressBar progressBar)
.Bind(ProgressBar.Maui.ProgressBar.ProgressProperty, nameof(SampleViewModel.Progress), BindingMode.OneWay),
new Button
{
Text = "Increment",
}
.BindCommand(nameof(SampleViewModel.IncrementProgressCommand))
}
};
}
}
and
SampleViewModel.cs
namespace ProgressBarSample.ViewModels;
public partial class SampleViewModel : BaseViewModel
{
[ObservableProperty]
private float progress = 0.01f;
[RelayCommand]
private void IncrementProgress()
{
Debug.WriteLine("IncrementProgress()");
Progress +=0.01f;
}
}
BaseViewModel.cs is an empty class that inherits ObservableObject
I also added builder.UseProgressBar()
to MauiProgram.cs
I think I figured it out. In C# Markup, the .Bind
extensions binds to the ProgressProperty
directly and not the public property Progress
. This means, I think, that the threshold logic is never run when I click the button.
I just reworked the logic a bit
ProgressBar.cs
using SkiaSharp;
using SkiaSharp.Views.Maui;
using SkiaSharp.Views.Maui.Controls;
using System.Diagnostics;
namespace ProgressBar.Maui;
// All the code in this file is included in all platforms.
public class ProgressBar : SKCanvasView
{
public static readonly BindableProperty ProgressProperty =
BindableProperty.Create(nameof(Progress), typeof(float), typeof(ProgressBar), 0f, propertyChanged: OnProgressPropertyChanged, coerceValue: ClampProgressValue);
public static readonly BindableProperty ProgressColorProperty =
BindableProperty.Create(nameof(ProgressColor), typeof(Color), typeof(ProgressBar), Colors.CornflowerBlue, propertyChanged: OnColorPropertyChanged);
public static readonly BindableProperty BaseColorProperty =
BindableProperty.Create(nameof(BaseColor), typeof(Color), typeof(ProgressBar), Colors.LightGray, propertyChanged: OnColorPropertyChanged);
public float Progress
{
get => (float)GetValue(ProgressProperty);
set => SetValue(ProgressProperty, value);
}
public Color ProgressColor
{
get => (Color)GetValue(ProgressColorProperty);
set => SetValue(ProgressColorProperty, value);
}
public Color BaseColor
{
get => (Color)GetValue(BaseColorProperty);
set => SetValue(BaseColorProperty, value);
}
// actual canvas instance to draw on
private SKCanvas _canvas;
// rectangle which will be used to draw the Progress Bar
private SKRect _drawRect;
// holds information about the dimensions, etc.
private SKImageInfo _info;
private const float _RedrawThreshold = 0.1f; // 1% change
private float _displayedProgress = 0f;
private SKPaint _basePaint = new()
{
Style = SKPaintStyle.Fill,
IsAntialias = true,
};
private SKPaint _progressPaint = new()
{
Style = SKPaintStyle.Fill,
IsAntialias = true,
};
protected override void OnPaintSurface (SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
_canvas = e.Surface.Canvas;
_canvas.Clear();
_info = e.Info;
_drawRect = new SKRect(0, 0, _info.Width, _info.Height);
DrawBase();
DrawProgress();
}
private void DrawBase ()
{
using SKPath basePath = new();
basePath.AddRect(_drawRect);
_basePaint.Color = BaseColor.ToSKColor();
_canvas.DrawPath(basePath, _basePaint);
}
private void DrawProgress ()
{
using SKPath progressPath = new();
var progressRect = new SKRect(0, 0, _info.Width * Progress, _info.Height);
progressPath.AddRect(progressRect);
_progressPaint.Color = ProgressColor.ToSKColor();
_canvas.DrawPath(progressPath, _progressPaint);
}
private static void OnProgressPropertyChanged (BindableObject bindable, object oldValue, object newValue)
{
if (oldValue == newValue)
return;
if ((float)newValue - ((ProgressBar)bindable)._displayedProgress >= _RedrawThreshold)
((ProgressBar)bindable).Update();
}
private static void OnColorPropertyChanged (BindableObject bindable, object oldValue, object newValue)
{
if (oldValue != newValue)
((ProgressBar)bindable).InvalidateSurface();
}
public void Update ()
{
if (_displayedProgress != Progress)
{
_displayedProgress = Progress;
InvalidateSurface();
}
}
private static object ClampProgressValue (BindableObject bindable, object value)
{
return (float)value switch
{
< 0 => 0,
> 1 => 1,
_ => value,
};
}
}