// 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); } }