// 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 toggle switch control. /// public class SkiaSwitch : SkiaView { private bool _isOn; private float _animationProgress; // 0 = off, 1 = on public bool IsOn { get => _isOn; set { if (_isOn != value) { _isOn = value; _animationProgress = value ? 1f : 0f; Toggled?.Invoke(this, new ToggledEventArgs(value)); Invalidate(); } } } public SKColor OnTrackColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); public SKColor OffTrackColor { get; set; } = new SKColor(0x9E, 0x9E, 0x9E); public SKColor ThumbColor { get; set; } = SKColors.White; public SKColor DisabledColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); public float TrackWidth { get; set; } = 52; public float TrackHeight { get; set; } = 32; public float ThumbRadius { get; set; } = 12; public float ThumbPadding { get; set; } = 4; public event EventHandler? Toggled; public SkiaSwitch() { IsFocusable = true; } protected override void OnDraw(SKCanvas canvas, SKRect bounds) { var centerY = bounds.MidY; var trackLeft = bounds.MidX - TrackWidth / 2; var trackRight = trackLeft + TrackWidth; // Calculate thumb position var thumbMinX = trackLeft + ThumbPadding + ThumbRadius; var thumbMaxX = trackRight - ThumbPadding - ThumbRadius; var thumbX = thumbMinX + _animationProgress * (thumbMaxX - thumbMinX); // Interpolate track color var trackColor = IsEnabled ? InterpolateColor(OffTrackColor, OnTrackColor, _animationProgress) : DisabledColor; // Draw track using var trackPaint = new SKPaint { Color = trackColor, IsAntialias = true, Style = SKPaintStyle.Fill }; var trackRect = new SKRoundRect( new SKRect(trackLeft, centerY - TrackHeight / 2, trackRight, centerY + TrackHeight / 2), TrackHeight / 2); canvas.DrawRoundRect(trackRect, trackPaint); // Draw thumb shadow if (IsEnabled) { using var shadowPaint = new SKPaint { Color = new SKColor(0, 0, 0, 40), IsAntialias = true, MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 2) }; canvas.DrawCircle(thumbX + 1, centerY + 1, ThumbRadius, shadowPaint); } // Draw thumb using var thumbPaint = new SKPaint { Color = IsEnabled ? ThumbColor : new SKColor(0xF5, 0xF5, 0xF5), IsAntialias = true, Style = SKPaintStyle.Fill }; canvas.DrawCircle(thumbX, centerY, ThumbRadius, thumbPaint); // Draw focus ring if (IsFocused) { using var focusPaint = new SKPaint { Color = OnTrackColor.WithAlpha(60), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 3 }; var focusRect = new SKRoundRect(trackRect.Rect, TrackHeight / 2); focusRect.Inflate(3, 3); canvas.DrawRoundRect(focusRect, focusPaint); } } private static SKColor InterpolateColor(SKColor from, SKColor to, float t) { return new SKColor( (byte)(from.Red + (to.Red - from.Red) * t), (byte)(from.Green + (to.Green - from.Green) * t), (byte)(from.Blue + (to.Blue - from.Blue) * t), (byte)(from.Alpha + (to.Alpha - from.Alpha) * t)); } public override void OnPointerPressed(PointerEventArgs e) { if (!IsEnabled) return; IsOn = !IsOn; e.Handled = true; } public override void OnPointerReleased(PointerEventArgs e) { // Toggle handled in OnPointerPressed } public override void OnKeyDown(KeyEventArgs e) { if (!IsEnabled) return; if (e.Key == Key.Space || e.Key == Key.Enter) { IsOn = !IsOn; e.Handled = true; } } protected override SKSize MeasureOverride(SKSize availableSize) { return new SKSize(TrackWidth + 8, TrackHeight + 8); } } public class ToggledEventArgs : EventArgs { public bool Value { get; } public ToggledEventArgs(bool value) => Value = value; }