// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
namespace Microsoft.Maui.Platform;
///
/// Skia-rendered activity indicator (spinner) control with full XAML styling support.
///
public class SkiaActivityIndicator : SkiaView
{
#region BindableProperties
///
/// Bindable property for IsRunning.
///
public static readonly BindableProperty IsRunningProperty =
BindableProperty.Create(
nameof(IsRunning),
typeof(bool),
typeof(SkiaActivityIndicator),
false,
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).OnIsRunningChanged());
///
/// Bindable property for Color.
///
public static readonly BindableProperty ColorProperty =
BindableProperty.Create(
nameof(Color),
typeof(SKColor),
typeof(SkiaActivityIndicator),
new SKColor(0x21, 0x96, 0xF3),
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate());
///
/// Bindable property for DisabledColor.
///
public static readonly BindableProperty DisabledColorProperty =
BindableProperty.Create(
nameof(DisabledColor),
typeof(SKColor),
typeof(SkiaActivityIndicator),
new SKColor(0xBD, 0xBD, 0xBD),
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate());
///
/// Bindable property for Size.
///
public static readonly BindableProperty SizeProperty =
BindableProperty.Create(
nameof(Size),
typeof(float),
typeof(SkiaActivityIndicator),
32f,
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).InvalidateMeasure());
///
/// Bindable property for StrokeWidth.
///
public static readonly BindableProperty StrokeWidthProperty =
BindableProperty.Create(
nameof(StrokeWidth),
typeof(float),
typeof(SkiaActivityIndicator),
3f,
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).InvalidateMeasure());
///
/// Bindable property for RotationSpeed.
///
public static readonly BindableProperty RotationSpeedProperty =
BindableProperty.Create(
nameof(RotationSpeed),
typeof(float),
typeof(SkiaActivityIndicator),
360f);
///
/// Bindable property for ArcCount.
///
public static readonly BindableProperty ArcCountProperty =
BindableProperty.Create(
nameof(ArcCount),
typeof(int),
typeof(SkiaActivityIndicator),
12,
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate());
#endregion
#region Properties
///
/// Gets or sets whether the indicator is running.
///
public bool IsRunning
{
get => (bool)GetValue(IsRunningProperty);
set => SetValue(IsRunningProperty, value);
}
///
/// Gets or sets the indicator color.
///
public SKColor Color
{
get => (SKColor)GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
///
/// Gets or sets the disabled color.
///
public SKColor DisabledColor
{
get => (SKColor)GetValue(DisabledColorProperty);
set => SetValue(DisabledColorProperty, value);
}
///
/// Gets or sets the indicator size.
///
public float Size
{
get => (float)GetValue(SizeProperty);
set => SetValue(SizeProperty, value);
}
///
/// Gets or sets the stroke width.
///
public float StrokeWidth
{
get => (float)GetValue(StrokeWidthProperty);
set => SetValue(StrokeWidthProperty, value);
}
///
/// Gets or sets the rotation speed in degrees per second.
///
public float RotationSpeed
{
get => (float)GetValue(RotationSpeedProperty);
set => SetValue(RotationSpeedProperty, value);
}
///
/// Gets or sets the number of arcs.
///
public int ArcCount
{
get => (int)GetValue(ArcCountProperty);
set => SetValue(ArcCountProperty, value);
}
#endregion
private float _rotationAngle;
private DateTime _lastUpdateTime = DateTime.UtcNow;
private void OnIsRunningChanged()
{
if (IsRunning)
{
_lastUpdateTime = DateTime.UtcNow;
}
Invalidate();
}
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
if (!IsRunning && !IsEnabled)
{
return;
}
var centerX = bounds.MidX;
var centerY = bounds.MidY;
var radius = Math.Min(Size / 2, Math.Min(bounds.Width, bounds.Height) / 2) - StrokeWidth;
// Update rotation
if (IsRunning)
{
var now = DateTime.UtcNow;
var elapsed = (now - _lastUpdateTime).TotalSeconds;
_lastUpdateTime = now;
_rotationAngle = (_rotationAngle + (float)(RotationSpeed * elapsed)) % 360;
}
canvas.Save();
canvas.Translate(centerX, centerY);
canvas.RotateDegrees(_rotationAngle);
var color = IsEnabled ? Color : DisabledColor;
// Draw arcs with varying opacity
for (int i = 0; i < ArcCount; i++)
{
var alpha = (byte)(255 * (1 - (float)i / ArcCount));
var arcColor = color.WithAlpha(alpha);
using var paint = new SKPaint
{
Color = arcColor,
IsAntialias = true,
Style = SKPaintStyle.Stroke,
StrokeWidth = StrokeWidth,
StrokeCap = SKStrokeCap.Round
};
var startAngle = (360f / ArcCount) * i;
var sweepAngle = 360f / ArcCount / 2;
using var path = new SKPath();
path.AddArc(
new SKRect(-radius, -radius, radius, radius),
startAngle,
sweepAngle);
canvas.DrawPath(path, paint);
}
canvas.Restore();
// Request redraw for animation
if (IsRunning)
{
Invalidate();
}
}
protected override SKSize MeasureOverride(SKSize availableSize)
{
return new SKSize(Size + StrokeWidth * 2, Size + StrokeWidth * 2);
}
}