2025-12-19 09:30:16 +00:00
|
|
|
// Licensed to the .NET Foundation under one or more agreements.
|
|
|
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
using System;
|
|
|
|
|
using Microsoft.Maui.Controls;
|
2026-01-16 05:04:05 +00:00
|
|
|
using Microsoft.Maui.Graphics;
|
2025-12-19 09:30:16 +00:00
|
|
|
using SkiaSharp;
|
|
|
|
|
|
|
|
|
|
namespace Microsoft.Maui.Platform;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-01-16 05:04:05 +00:00
|
|
|
/// Skia-rendered progress bar control with full MAUI compliance.
|
|
|
|
|
/// Implements IProgress interface requirements:
|
|
|
|
|
/// - Progress property (0.0 to 1.0)
|
|
|
|
|
/// - ProgressColor for the filled portion
|
2025-12-19 09:30:16 +00:00
|
|
|
/// </summary>
|
|
|
|
|
public class SkiaProgressBar : SkiaView
|
|
|
|
|
{
|
2026-01-16 05:04:05 +00:00
|
|
|
#region SKColor Helper
|
|
|
|
|
|
|
|
|
|
private static SKColor ToSKColor(Color? color)
|
|
|
|
|
{
|
|
|
|
|
if (color == null) return SKColors.Transparent;
|
2026-01-17 03:36:37 +00:00
|
|
|
return color.ToSKColor();
|
2026-01-16 05:04:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
#region BindableProperties
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
/// <summary>
|
|
|
|
|
/// Bindable property for Progress.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static readonly BindableProperty ProgressProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(Progress),
|
|
|
|
|
typeof(double),
|
|
|
|
|
typeof(SkiaProgressBar),
|
|
|
|
|
0.0,
|
2026-01-16 05:04:05 +00:00
|
|
|
BindingMode.TwoWay,
|
2026-01-01 13:51:12 -05:00
|
|
|
coerceValue: (b, v) => Math.Clamp((double)v, 0.0, 1.0),
|
2026-01-16 05:04:05 +00:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaProgressBar)b).OnProgressChanged((double)o, (double)n));
|
2025-12-21 13:26:56 -05:00
|
|
|
|
|
|
|
|
/// <summary>
|
2026-01-16 05:04:05 +00:00
|
|
|
/// Bindable property for TrackColor (background track).
|
2025-12-21 13:26:56 -05:00
|
|
|
/// </summary>
|
|
|
|
|
public static readonly BindableProperty TrackColorProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(TrackColor),
|
2026-01-16 05:04:05 +00:00
|
|
|
typeof(Color),
|
2025-12-21 13:26:56 -05:00
|
|
|
typeof(SkiaProgressBar),
|
2026-01-16 05:04:05 +00:00
|
|
|
Color.FromRgb(0xE0, 0xE0, 0xE0),
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-01-16 05:04:05 +00:00
|
|
|
/// Bindable property for ProgressColor (filled portion).
|
2025-12-21 13:26:56 -05:00
|
|
|
/// </summary>
|
|
|
|
|
public static readonly BindableProperty ProgressColorProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(ProgressColor),
|
2026-01-16 05:04:05 +00:00
|
|
|
typeof(Color),
|
2025-12-21 13:26:56 -05:00
|
|
|
typeof(SkiaProgressBar),
|
2026-01-16 05:04:05 +00:00
|
|
|
Color.FromRgb(0x21, 0x96, 0xF3), // Material Blue
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Bindable property for DisabledColor.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static readonly BindableProperty DisabledColorProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(DisabledColor),
|
2026-01-16 05:04:05 +00:00
|
|
|
typeof(Color),
|
2025-12-21 13:26:56 -05:00
|
|
|
typeof(SkiaProgressBar),
|
2026-01-16 05:04:05 +00:00
|
|
|
Color.FromRgb(0xBD, 0xBD, 0xBD),
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Bindable property for BarHeight.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static readonly BindableProperty BarHeightProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(BarHeight),
|
2026-01-16 05:04:05 +00:00
|
|
|
typeof(double),
|
2025-12-21 13:26:56 -05:00
|
|
|
typeof(SkiaProgressBar),
|
2026-01-16 05:04:05 +00:00
|
|
|
4.0,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaProgressBar)b).InvalidateMeasure());
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Bindable property for CornerRadius.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static readonly BindableProperty CornerRadiusProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(CornerRadius),
|
2026-01-16 05:04:05 +00:00
|
|
|
typeof(double),
|
2025-12-21 13:26:56 -05:00
|
|
|
typeof(SkiaProgressBar),
|
2026-01-16 05:04:05 +00:00
|
|
|
2.0,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Properties
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the progress value (0.0 to 1.0).
|
|
|
|
|
/// </summary>
|
2025-12-19 09:30:16 +00:00
|
|
|
public double Progress
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
get => (double)GetValue(ProgressProperty);
|
|
|
|
|
set => SetValue(ProgressProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-01-16 05:04:05 +00:00
|
|
|
/// Gets or sets the track color (background).
|
2025-12-21 13:26:56 -05:00
|
|
|
/// </summary>
|
2026-01-16 05:04:05 +00:00
|
|
|
public Color TrackColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:04:05 +00:00
|
|
|
get => (Color)GetValue(TrackColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(TrackColorProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
/// <summary>
|
2026-01-16 05:04:05 +00:00
|
|
|
/// Gets or sets the progress color (filled portion).
|
2025-12-21 13:26:56 -05:00
|
|
|
/// </summary>
|
2026-01-16 05:04:05 +00:00
|
|
|
public Color ProgressColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:04:05 +00:00
|
|
|
get => (Color)GetValue(ProgressColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(ProgressColorProperty, value);
|
|
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the disabled color.
|
|
|
|
|
/// </summary>
|
2026-01-16 05:04:05 +00:00
|
|
|
public Color DisabledColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:04:05 +00:00
|
|
|
get => (Color)GetValue(DisabledColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(DisabledColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the bar height.
|
|
|
|
|
/// </summary>
|
2026-01-16 05:04:05 +00:00
|
|
|
public double BarHeight
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:04:05 +00:00
|
|
|
get => (double)GetValue(BarHeightProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(BarHeightProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the corner radius.
|
|
|
|
|
/// </summary>
|
2026-01-16 05:04:05 +00:00
|
|
|
public double CornerRadius
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:04:05 +00:00
|
|
|
get => (double)GetValue(CornerRadiusProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(CornerRadiusProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
2026-01-16 05:04:05 +00:00
|
|
|
#region Events
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
/// <summary>
|
|
|
|
|
/// Event raised when progress changes.
|
|
|
|
|
/// </summary>
|
2025-12-19 09:30:16 +00:00
|
|
|
public event EventHandler<ProgressChangedEventArgs>? ProgressChanged;
|
|
|
|
|
|
2026-01-16 05:04:05 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Event Handlers
|
|
|
|
|
|
|
|
|
|
private void OnProgressChanged(double oldValue, double newValue)
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 05:04:05 +00:00
|
|
|
ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(oldValue, newValue));
|
2025-12-21 13:26:56 -05:00
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:04:05 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Rendering
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
2026-01-16 05:04:05 +00:00
|
|
|
var barHeight = (float)BarHeight;
|
|
|
|
|
var cornerRadius = (float)CornerRadius;
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
float midY = bounds.MidY;
|
2026-01-16 05:04:05 +00:00
|
|
|
float trackTop = midY - barHeight / 2f;
|
|
|
|
|
float trackBottom = midY + barHeight / 2f;
|
|
|
|
|
|
|
|
|
|
// Get colors
|
|
|
|
|
var trackColorSK = ToSKColor(TrackColor);
|
|
|
|
|
var progressColorSK = ToSKColor(ProgressColor);
|
|
|
|
|
var disabledColorSK = ToSKColor(DisabledColor);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
// Draw track
|
|
|
|
|
using var trackPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-16 05:04:05 +00:00
|
|
|
Color = IsEnabled ? trackColorSK : disabledColorSK,
|
2025-12-19 09:30:16 +00:00
|
|
|
IsAntialias = true,
|
|
|
|
|
Style = SKPaintStyle.Fill
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var trackRect = new SKRoundRect(
|
|
|
|
|
new SKRect(bounds.Left, trackTop, bounds.Right, trackBottom),
|
2026-01-16 05:04:05 +00:00
|
|
|
cornerRadius);
|
2025-12-19 09:30:16 +00:00
|
|
|
canvas.DrawRoundRect(trackRect, trackPaint);
|
|
|
|
|
|
|
|
|
|
// Draw progress
|
2026-01-01 13:51:12 -05:00
|
|
|
if (Progress > 0.0)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
float progressWidth = bounds.Width * (float)Progress;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
using var progressPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-16 05:04:05 +00:00
|
|
|
Color = IsEnabled ? progressColorSK : disabledColorSK,
|
2025-12-19 09:30:16 +00:00
|
|
|
IsAntialias = true,
|
|
|
|
|
Style = SKPaintStyle.Fill
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var progressRect = new SKRoundRect(
|
|
|
|
|
new SKRect(bounds.Left, trackTop, bounds.Left + progressWidth, trackBottom),
|
2026-01-16 05:04:05 +00:00
|
|
|
cornerRadius);
|
2025-12-19 09:30:16 +00:00
|
|
|
canvas.DrawRoundRect(progressRect, progressPaint);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:04:05 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Lifecycle
|
|
|
|
|
|
|
|
|
|
protected override void OnEnabledChanged()
|
|
|
|
|
{
|
|
|
|
|
base.OnEnabledChanged();
|
|
|
|
|
SkiaVisualStateManager.GoToState(this, IsEnabled
|
|
|
|
|
? SkiaVisualStateManager.CommonStates.Normal
|
|
|
|
|
: SkiaVisualStateManager.CommonStates.Disabled);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Layout
|
|
|
|
|
|
2026-01-17 05:22:37 +00:00
|
|
|
protected override Size MeasureOverride(Size availableSize)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-17 05:22:37 +00:00
|
|
|
var barHeight = BarHeight;
|
|
|
|
|
return new Size(200, barHeight + 8);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
2026-01-16 05:04:05 +00:00
|
|
|
|
|
|
|
|
#endregion
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
/// <summary>
|
|
|
|
|
/// Event args for progress changed events.
|
|
|
|
|
/// </summary>
|
2025-12-19 09:30:16 +00:00
|
|
|
public class ProgressChangedEventArgs : EventArgs
|
|
|
|
|
{
|
2026-01-16 05:04:05 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the old progress value.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public double OldProgress { get; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the new progress value.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public double NewProgress { get; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the current progress value (same as NewProgress).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public double Progress => NewProgress;
|
|
|
|
|
|
|
|
|
|
public ProgressChangedEventArgs(double oldProgress, double newProgress)
|
|
|
|
|
{
|
|
|
|
|
OldProgress = oldProgress;
|
|
|
|
|
NewProgress = newProgress;
|
|
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|