This commit is contained in:
2026-01-16 04:54:18 +00:00
parent a8c8939a3f
commit 271f8d7fa9
2 changed files with 212 additions and 97 deletions

View File

@@ -70,13 +70,12 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
// TrackColor sets both On and Off track colors // TrackColor sets the On track color (MAUI's OnColor)
if (@switch.TrackColor is not null) if (@switch.TrackColor is not null)
{ {
var color = @switch.TrackColor.ToSKColor(); handler.PlatformView.OnTrackColor = @switch.TrackColor;
handler.PlatformView.OnTrackColor = color; // Off track is a lighter/desaturated version
// Off track could be a lighter version handler.PlatformView.OffTrackColor = @switch.TrackColor.WithAlpha(0.5f);
handler.PlatformView.OffTrackColor = color.WithAlpha(128);
} }
} }
@@ -85,7 +84,7 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (@switch.ThumbColor is not null) if (@switch.ThumbColor is not null)
handler.PlatformView.ThumbColor = @switch.ThumbColor.ToSKColor(); handler.PlatformView.ThumbColor = @switch.ThumbColor;
} }
public static void MapBackground(SwitchHandler handler, ISwitch @switch) public static void MapBackground(SwitchHandler handler, ISwitch @switch)
@@ -94,6 +93,7 @@ public partial class SwitchHandler : ViewHandler<ISwitch, SkiaSwitch>
if (@switch.Background is SolidPaint solidPaint && solidPaint.Color is not null) if (@switch.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{ {
// Background color for the switch container (not the track)
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
} }
} }

View File

@@ -3,122 +3,115 @@
using System; using System;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using SkiaSharp; using SkiaSharp;
namespace Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform;
/// <summary> /// <summary>
/// Skia-rendered toggle switch control with full XAML styling support. /// Skia-rendered toggle switch control with full MAUI compliance.
/// Implements ISwitch interface requirements:
/// - IsOn property with Toggled event
/// - OnColor (TrackColor when on)
/// - ThumbColor
/// - Smooth animation on toggle
/// </summary> /// </summary>
public class SkiaSwitch : SkiaView public class SkiaSwitch : SkiaView
{ {
#region SKColor Helper
private static SKColor ToSKColor(Color? color)
{
if (color == null) return SKColors.Transparent;
return new SKColor(
(byte)(color.Red * 255),
(byte)(color.Green * 255),
(byte)(color.Blue * 255),
(byte)(color.Alpha * 255));
}
#endregion
#region BindableProperties #region BindableProperties
/// <summary>
/// Bindable property for IsOn.
/// </summary>
public static readonly BindableProperty IsOnProperty = public static readonly BindableProperty IsOnProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(IsOn), nameof(IsOn),
typeof(bool), typeof(bool),
typeof(SkiaSwitch), typeof(SkiaSwitch),
false, false,
BindingMode.OneWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSwitch)b).OnIsOnChanged()); propertyChanged: (b, o, n) => ((SkiaSwitch)b).OnIsOnChanged((bool)o, (bool)n));
/// <summary>
/// Bindable property for OnTrackColor.
/// </summary>
public static readonly BindableProperty OnTrackColorProperty = public static readonly BindableProperty OnTrackColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(OnTrackColor), nameof(OnTrackColor),
typeof(SKColor), typeof(Color),
typeof(SkiaSwitch), typeof(SkiaSwitch),
new SKColor(33, 150, 243), Color.FromRgb(33, 150, 243), // Material Blue
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate());
/// <summary>
/// Bindable property for OffTrackColor.
/// </summary>
public static readonly BindableProperty OffTrackColorProperty = public static readonly BindableProperty OffTrackColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(OffTrackColor), nameof(OffTrackColor),
typeof(SKColor), typeof(Color),
typeof(SkiaSwitch), typeof(SkiaSwitch),
new SKColor(158, 158, 158), Color.FromRgb(158, 158, 158),
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate());
/// <summary>
/// Bindable property for ThumbColor.
/// </summary>
public static readonly BindableProperty ThumbColorProperty = public static readonly BindableProperty ThumbColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(ThumbColor), nameof(ThumbColor),
typeof(SKColor), typeof(Color),
typeof(SkiaSwitch), typeof(SkiaSwitch),
SKColors.White, Colors.White,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate());
/// <summary>
/// Bindable property for DisabledColor.
/// </summary>
public static readonly BindableProperty DisabledColorProperty = public static readonly BindableProperty DisabledColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(DisabledColor), nameof(DisabledColor),
typeof(SKColor), typeof(Color),
typeof(SkiaSwitch), typeof(SkiaSwitch),
new SKColor(189, 189, 189), Color.FromRgb(189, 189, 189),
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate());
/// <summary>
/// Bindable property for TrackWidth.
/// </summary>
public static readonly BindableProperty TrackWidthProperty = public static readonly BindableProperty TrackWidthProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(TrackWidth), nameof(TrackWidth),
typeof(float), typeof(double),
typeof(SkiaSwitch), typeof(SkiaSwitch),
52f, 52.0,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSwitch)b).InvalidateMeasure()); propertyChanged: (b, o, n) => ((SkiaSwitch)b).InvalidateMeasure());
/// <summary>
/// Bindable property for TrackHeight.
/// </summary>
public static readonly BindableProperty TrackHeightProperty = public static readonly BindableProperty TrackHeightProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(TrackHeight), nameof(TrackHeight),
typeof(float), typeof(double),
typeof(SkiaSwitch), typeof(SkiaSwitch),
32f, 32.0,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSwitch)b).InvalidateMeasure()); propertyChanged: (b, o, n) => ((SkiaSwitch)b).InvalidateMeasure());
/// <summary>
/// Bindable property for ThumbRadius.
/// </summary>
public static readonly BindableProperty ThumbRadiusProperty = public static readonly BindableProperty ThumbRadiusProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(ThumbRadius), nameof(ThumbRadius),
typeof(float), typeof(double),
typeof(SkiaSwitch), typeof(SkiaSwitch),
12f, 12.0,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate());
/// <summary>
/// Bindable property for ThumbPadding.
/// </summary>
public static readonly BindableProperty ThumbPaddingProperty = public static readonly BindableProperty ThumbPaddingProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(ThumbPadding), nameof(ThumbPadding),
typeof(float), typeof(double),
typeof(SkiaSwitch), typeof(SkiaSwitch),
4f, 4.0,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate());
@@ -136,114 +129,203 @@ public class SkiaSwitch : SkiaView
} }
/// <summary> /// <summary>
/// Gets or sets the on track color. /// Gets or sets the track color when the switch is on.
/// This is the primary MAUI Switch.OnColor property.
/// </summary> /// </summary>
public SKColor OnTrackColor public Color OnTrackColor
{ {
get => (SKColor)GetValue(OnTrackColorProperty); get => (Color)GetValue(OnTrackColorProperty);
set => SetValue(OnTrackColorProperty, value); set => SetValue(OnTrackColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the off track color. /// Gets or sets the track color when the switch is off.
/// </summary> /// </summary>
public SKColor OffTrackColor public Color OffTrackColor
{ {
get => (SKColor)GetValue(OffTrackColorProperty); get => (Color)GetValue(OffTrackColorProperty);
set => SetValue(OffTrackColorProperty, value); set => SetValue(OffTrackColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the thumb color. /// Gets or sets the thumb color.
/// </summary> /// </summary>
public SKColor ThumbColor public Color ThumbColor
{ {
get => (SKColor)GetValue(ThumbColorProperty); get => (Color)GetValue(ThumbColorProperty);
set => SetValue(ThumbColorProperty, value); set => SetValue(ThumbColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the disabled color. /// Gets or sets the color used when the control is disabled.
/// </summary> /// </summary>
public SKColor DisabledColor public Color DisabledColor
{ {
get => (SKColor)GetValue(DisabledColorProperty); get => (Color)GetValue(DisabledColorProperty);
set => SetValue(DisabledColorProperty, value); set => SetValue(DisabledColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the track width. /// Gets or sets the track width in device-independent units.
/// </summary> /// </summary>
public float TrackWidth public double TrackWidth
{ {
get => (float)GetValue(TrackWidthProperty); get => (double)GetValue(TrackWidthProperty);
set => SetValue(TrackWidthProperty, value); set => SetValue(TrackWidthProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the track height. /// Gets or sets the track height in device-independent units.
/// </summary> /// </summary>
public float TrackHeight public double TrackHeight
{ {
get => (float)GetValue(TrackHeightProperty); get => (double)GetValue(TrackHeightProperty);
set => SetValue(TrackHeightProperty, value); set => SetValue(TrackHeightProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the thumb radius. /// Gets or sets the thumb radius in device-independent units.
/// </summary> /// </summary>
public float ThumbRadius public double ThumbRadius
{ {
get => (float)GetValue(ThumbRadiusProperty); get => (double)GetValue(ThumbRadiusProperty);
set => SetValue(ThumbRadiusProperty, value); set => SetValue(ThumbRadiusProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the thumb padding. /// Gets or sets the thumb padding in device-independent units.
/// </summary> /// </summary>
public float ThumbPadding public double ThumbPadding
{ {
get => (float)GetValue(ThumbPaddingProperty); get => (double)GetValue(ThumbPaddingProperty);
set => SetValue(ThumbPaddingProperty, value); set => SetValue(ThumbPaddingProperty, value);
} }
#endregion #endregion
#region Animation Fields
private float _animationProgress; private float _animationProgress;
private System.Timers.Timer? _animationTimer;
private bool _animatingToOn;
private const int AnimationDurationMs = 200;
private const int AnimationFrameMs = 16; // ~60fps
#endregion
#region Events
/// <summary> /// <summary>
/// Event raised when the switch is toggled. /// Event raised when the switch is toggled.
/// </summary> /// </summary>
public event EventHandler<ToggledEventArgs>? Toggled; public event EventHandler<ToggledEventArgs>? Toggled;
#endregion
#region Constructor
public SkiaSwitch() public SkiaSwitch()
{ {
IsFocusable = true; IsFocusable = true;
_animationProgress = 0f;
} }
private void OnIsOnChanged() #endregion
#region Event Handlers
private void OnIsOnChanged(bool oldValue, bool newValue)
{ {
_animationProgress = IsOn ? 1f : 0f; // Start animation
Toggled?.Invoke(this, new ToggledEventArgs(IsOn)); StartAnimation(newValue);
SkiaVisualStateManager.GoToState(this, IsOn ? "On" : "Off");
Toggled?.Invoke(this, new ToggledEventArgs(newValue));
SkiaVisualStateManager.GoToState(this, newValue ? "On" : "Off");
}
#endregion
#region Animation
private void StartAnimation(bool toOn)
{
_animatingToOn = toOn;
// Stop existing animation
_animationTimer?.Stop();
_animationTimer?.Dispose();
// Create new animation timer
_animationTimer = new System.Timers.Timer(AnimationFrameMs);
_animationTimer.Elapsed += OnAnimationFrame;
_animationTimer.AutoReset = true;
_animationTimer.Start();
}
private void OnAnimationFrame(object? sender, System.Timers.ElapsedEventArgs e)
{
float step = AnimationFrameMs / (float)AnimationDurationMs;
if (_animatingToOn)
{
_animationProgress += step;
if (_animationProgress >= 1f)
{
_animationProgress = 1f;
StopAnimation();
}
}
else
{
_animationProgress -= step;
if (_animationProgress <= 0f)
{
_animationProgress = 0f;
StopAnimation();
}
}
// Request redraw on UI thread
Invalidate(); Invalidate();
} }
private void StopAnimation()
{
_animationTimer?.Stop();
_animationTimer?.Dispose();
_animationTimer = null;
}
#endregion
#region Rendering
protected override void OnDraw(SKCanvas canvas, SKRect bounds) protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{ {
var centerY = bounds.MidY; var trackWidth = (float)TrackWidth;
var trackLeft = bounds.MidX - TrackWidth / 2f; var trackHeight = (float)TrackHeight;
var trackRight = trackLeft + TrackWidth; var thumbRadius = (float)ThumbRadius;
var thumbPadding = (float)ThumbPadding;
// Calculate thumb position var centerY = bounds.MidY;
var thumbMinX = trackLeft + ThumbPadding + ThumbRadius; var trackLeft = bounds.MidX - trackWidth / 2f;
var thumbMaxX = trackRight - ThumbPadding - ThumbRadius; var trackRight = trackLeft + trackWidth;
// Calculate thumb position based on animation progress
var thumbMinX = trackLeft + thumbPadding + thumbRadius;
var thumbMaxX = trackRight - thumbPadding - thumbRadius;
var thumbX = thumbMinX + _animationProgress * (thumbMaxX - thumbMinX); var thumbX = thumbMinX + _animationProgress * (thumbMaxX - thumbMinX);
// Interpolate track color // Get colors
var onColorSK = ToSKColor(OnTrackColor);
var offColorSK = ToSKColor(OffTrackColor);
var thumbColorSK = ToSKColor(ThumbColor);
var disabledColorSK = ToSKColor(DisabledColor);
// Interpolate track color based on animation progress
var trackColor = IsEnabled var trackColor = IsEnabled
? InterpolateColor(OffTrackColor, OnTrackColor, _animationProgress) ? InterpolateColor(offColorSK, onColorSK, _animationProgress)
: DisabledColor; : disabledColorSK;
// Draw track // Draw track
using var trackPaint = new SKPaint using var trackPaint = new SKPaint
@@ -254,11 +336,11 @@ public class SkiaSwitch : SkiaView
}; };
var trackRect = new SKRoundRect( var trackRect = new SKRoundRect(
new SKRect(trackLeft, centerY - TrackHeight / 2f, trackRight, centerY + TrackHeight / 2f), new SKRect(trackLeft, centerY - trackHeight / 2f, trackRight, centerY + trackHeight / 2f),
TrackHeight / 2f); trackHeight / 2f);
canvas.DrawRoundRect(trackRect, trackPaint); canvas.DrawRoundRect(trackRect, trackPaint);
// Draw thumb shadow // Draw thumb shadow (only when enabled)
if (IsEnabled) if (IsEnabled)
{ {
using var shadowPaint = new SKPaint using var shadowPaint = new SKPaint
@@ -267,29 +349,29 @@ public class SkiaSwitch : SkiaView
IsAntialias = true, IsAntialias = true,
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 2f) MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 2f)
}; };
canvas.DrawCircle(thumbX + 1f, centerY + 1f, ThumbRadius, shadowPaint); canvas.DrawCircle(thumbX + 1f, centerY + 1f, thumbRadius, shadowPaint);
} }
// Draw thumb // Draw thumb
using var thumbPaint = new SKPaint using var thumbPaint = new SKPaint
{ {
Color = IsEnabled ? ThumbColor : new SKColor(245, 245, 245), Color = IsEnabled ? thumbColorSK : new SKColor(245, 245, 245),
IsAntialias = true, IsAntialias = true,
Style = SKPaintStyle.Fill Style = SKPaintStyle.Fill
}; };
canvas.DrawCircle(thumbX, centerY, ThumbRadius, thumbPaint); canvas.DrawCircle(thumbX, centerY, thumbRadius, thumbPaint);
// Draw focus ring // Draw focus ring
if (IsFocused) if (IsFocused)
{ {
using var focusPaint = new SKPaint using var focusPaint = new SKPaint
{ {
Color = OnTrackColor.WithAlpha(60), Color = onColorSK.WithAlpha(60),
IsAntialias = true, IsAntialias = true,
Style = SKPaintStyle.Stroke, Style = SKPaintStyle.Stroke,
StrokeWidth = 3f StrokeWidth = 3f
}; };
var focusRect = new SKRoundRect(trackRect.Rect, TrackHeight / 2f); var focusRect = new SKRoundRect(trackRect.Rect, trackHeight / 2f);
focusRect.Inflate(3f, 3f); focusRect.Inflate(3f, 3f);
canvas.DrawRoundRect(focusRect, focusPaint); canvas.DrawRoundRect(focusRect, focusPaint);
} }
@@ -297,6 +379,9 @@ public class SkiaSwitch : SkiaView
private static SKColor InterpolateColor(SKColor from, SKColor to, float t) private static SKColor InterpolateColor(SKColor from, SKColor to, float t)
{ {
// Clamp t to [0, 1]
t = Math.Max(0f, Math.Min(1f, t));
return new SKColor( return new SKColor(
(byte)(from.Red + (to.Red - from.Red) * t), (byte)(from.Red + (to.Red - from.Red) * t),
(byte)(from.Green + (to.Green - from.Green) * t), (byte)(from.Green + (to.Green - from.Green) * t),
@@ -304,6 +389,10 @@ public class SkiaSwitch : SkiaView
(byte)(from.Alpha + (to.Alpha - from.Alpha) * t)); (byte)(from.Alpha + (to.Alpha - from.Alpha) * t));
} }
#endregion
#region Pointer Events
public override void OnPointerPressed(PointerEventArgs e) public override void OnPointerPressed(PointerEventArgs e)
{ {
if (IsEnabled) if (IsEnabled)
@@ -315,8 +404,13 @@ public class SkiaSwitch : SkiaView
public override void OnPointerReleased(PointerEventArgs e) public override void OnPointerReleased(PointerEventArgs e)
{ {
// No action needed
} }
#endregion
#region Keyboard Events
public override void OnKeyDown(KeyEventArgs e) public override void OnKeyDown(KeyEventArgs e)
{ {
if (IsEnabled && (e.Key == Key.Space || e.Key == Key.Enter)) if (IsEnabled && (e.Key == Key.Space || e.Key == Key.Enter))
@@ -326,14 +420,35 @@ public class SkiaSwitch : SkiaView
} }
} }
#endregion
#region Lifecycle
protected override void OnEnabledChanged() protected override void OnEnabledChanged()
{ {
base.OnEnabledChanged(); base.OnEnabledChanged();
SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled"); SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled");
} }
protected override void Dispose(bool disposing)
{
if (disposing)
{
StopAnimation();
}
base.Dispose(disposing);
}
#endregion
#region Layout
protected override SKSize MeasureOverride(SKSize availableSize) protected override SKSize MeasureOverride(SKSize availableSize)
{ {
return new SKSize(TrackWidth + 8f, TrackHeight + 8f); var trackWidth = (float)TrackWidth;
var trackHeight = (float)TrackHeight;
return new SKSize(trackWidth + 8f, trackHeight + 8f);
} }
#endregion
} }