Files

296 lines
8.0 KiB
C#
Raw Permalink Normal View History

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
2026-01-16 05:03:49 +00:00
using System;
using Microsoft.Maui.Controls;
2026-01-16 05:03:49 +00:00
using Microsoft.Maui.Graphics;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
/// <summary>
2026-01-16 05:03:49 +00:00
/// Skia-rendered activity indicator (spinner) control with full MAUI compliance.
/// Implements IActivityIndicator interface requirements:
/// - IsRunning property to start/stop animation
/// - Color property for the indicator color
/// </summary>
public class SkiaActivityIndicator : SkiaView
{
2026-01-16 05:03:49 +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:03:49 +00:00
}
#endregion
#region BindableProperties
/// <summary>
/// Bindable property for IsRunning.
/// </summary>
public static readonly BindableProperty IsRunningProperty =
BindableProperty.Create(
nameof(IsRunning),
typeof(bool),
typeof(SkiaActivityIndicator),
false,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).OnIsRunningChanged());
/// <summary>
/// Bindable property for Color.
/// </summary>
public static readonly BindableProperty ColorProperty =
BindableProperty.Create(
nameof(Color),
2026-01-16 05:03:49 +00:00
typeof(Color),
typeof(SkiaActivityIndicator),
2026-01-16 05:03:49 +00:00
Color.FromRgb(0x21, 0x96, 0xF3), // Material Blue
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate());
/// <summary>
/// Bindable property for DisabledColor.
/// </summary>
public static readonly BindableProperty DisabledColorProperty =
BindableProperty.Create(
nameof(DisabledColor),
2026-01-16 05:03:49 +00:00
typeof(Color),
typeof(SkiaActivityIndicator),
2026-01-16 05:03:49 +00:00
Color.FromRgb(0xBD, 0xBD, 0xBD),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate());
/// <summary>
/// Bindable property for Size.
/// </summary>
public static readonly BindableProperty SizeProperty =
BindableProperty.Create(
nameof(Size),
2026-01-16 05:03:49 +00:00
typeof(double),
typeof(SkiaActivityIndicator),
2026-01-16 05:03:49 +00:00
32.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).InvalidateMeasure());
/// <summary>
/// Bindable property for StrokeWidth.
/// </summary>
public static readonly BindableProperty StrokeWidthProperty =
BindableProperty.Create(
nameof(StrokeWidth),
2026-01-16 05:03:49 +00:00
typeof(double),
typeof(SkiaActivityIndicator),
2026-01-16 05:03:49 +00:00
3.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).InvalidateMeasure());
/// <summary>
/// Bindable property for RotationSpeed.
/// </summary>
public static readonly BindableProperty RotationSpeedProperty =
BindableProperty.Create(
nameof(RotationSpeed),
2026-01-16 05:03:49 +00:00
typeof(double),
typeof(SkiaActivityIndicator),
2026-01-16 05:03:49 +00:00
360.0,
BindingMode.TwoWay);
/// <summary>
/// Bindable property for ArcCount.
/// </summary>
public static readonly BindableProperty ArcCountProperty =
BindableProperty.Create(
nameof(ArcCount),
typeof(int),
typeof(SkiaActivityIndicator),
12,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate());
#endregion
#region Properties
/// <summary>
/// Gets or sets whether the indicator is running.
/// </summary>
public bool IsRunning
{
get => (bool)GetValue(IsRunningProperty);
set => SetValue(IsRunningProperty, value);
}
/// <summary>
/// Gets or sets the indicator color.
/// </summary>
2026-01-16 05:03:49 +00:00
public Color Color
{
2026-01-16 05:03:49 +00:00
get => (Color)GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
/// <summary>
/// Gets or sets the disabled color.
/// </summary>
2026-01-16 05:03:49 +00:00
public Color DisabledColor
{
2026-01-16 05:03:49 +00:00
get => (Color)GetValue(DisabledColorProperty);
set => SetValue(DisabledColorProperty, value);
}
/// <summary>
/// Gets or sets the indicator size.
/// </summary>
2026-01-16 05:03:49 +00:00
public double Size
{
2026-01-16 05:03:49 +00:00
get => (double)GetValue(SizeProperty);
set => SetValue(SizeProperty, value);
}
/// <summary>
/// Gets or sets the stroke width.
/// </summary>
2026-01-16 05:03:49 +00:00
public double StrokeWidth
{
2026-01-16 05:03:49 +00:00
get => (double)GetValue(StrokeWidthProperty);
set => SetValue(StrokeWidthProperty, value);
}
/// <summary>
/// Gets or sets the rotation speed in degrees per second.
/// </summary>
2026-01-16 05:03:49 +00:00
public double RotationSpeed
{
2026-01-16 05:03:49 +00:00
get => (double)GetValue(RotationSpeedProperty);
set => SetValue(RotationSpeedProperty, value);
}
/// <summary>
/// Gets or sets the number of arcs.
/// </summary>
public int ArcCount
{
get => (int)GetValue(ArcCountProperty);
set => SetValue(ArcCountProperty, value);
}
#endregion
2026-01-16 05:03:49 +00:00
#region Private Fields
private float _rotationAngle;
private DateTime _lastUpdateTime = DateTime.UtcNow;
2026-01-16 05:03:49 +00:00
#endregion
#region Event Handlers
private void OnIsRunningChanged()
{
if (IsRunning)
{
_lastUpdateTime = DateTime.UtcNow;
}
Invalidate();
}
2026-01-16 05:03:49 +00:00
#endregion
#region Rendering
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
if (!IsRunning && !IsEnabled)
{
return;
}
2026-01-16 05:03:49 +00:00
var size = (float)Size;
var strokeWidth = (float)StrokeWidth;
var rotationSpeed = (float)RotationSpeed;
var centerX = bounds.MidX;
var centerY = bounds.MidY;
2026-01-16 05:03:49 +00:00
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;
2026-01-16 05:03:49 +00:00
_rotationAngle = (_rotationAngle + (float)(rotationSpeed * elapsed)) % 360;
}
canvas.Save();
canvas.Translate(centerX, centerY);
canvas.RotateDegrees(_rotationAngle);
2026-01-16 05:03:49 +00:00
var colorSK = ToSKColor(IsEnabled ? Color : DisabledColor);
// Draw arcs with varying opacity
for (int i = 0; i < ArcCount; i++)
{
var alpha = (byte)(255 * (1 - (float)i / ArcCount));
2026-01-16 05:03:49 +00:00
var arcColor = colorSK.WithAlpha(alpha);
using var paint = new SKPaint
{
Color = arcColor,
IsAntialias = true,
Style = SKPaintStyle.Stroke,
2026-01-16 05:03:49 +00:00
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();
}
}
2026-01-16 05:03:49 +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)
{
2026-01-16 05:03:49 +00:00
var size = (float)Size;
var strokeWidth = (float)StrokeWidth;
2026-01-17 05:22:37 +00:00
return new Size(size + strokeWidth * 2, size + strokeWidth * 2);
}
2026-01-16 05:03:49 +00:00
#endregion
}