diff --git a/Handlers/SliderHandler.cs b/Handlers/SliderHandler.cs index 2eb4583..0ea7281 100644 --- a/Handlers/SliderHandler.cs +++ b/Handlers/SliderHandler.cs @@ -68,7 +68,7 @@ public partial class SliderHandler : ViewHandler base.DisconnectHandler(platformView); } - private void OnValueChanged(object? sender, SliderValueChangedEventArgs e) + private void OnValueChanged(object? sender, ValueChangedEventArgs e) { if (VirtualView is null || PlatformView is null) return; @@ -112,18 +112,16 @@ public partial class SliderHandler : ViewHandler { if (handler.PlatformView is null) return; - // MinimumTrackColor maps to ActiveTrackColor (the filled portion) if (slider.MinimumTrackColor is not null) - handler.PlatformView.ActiveTrackColor = slider.MinimumTrackColor.ToSKColor(); + handler.PlatformView.MinimumTrackColor = slider.MinimumTrackColor; } public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider) { if (handler.PlatformView is null) return; - // MaximumTrackColor maps to TrackColor (the unfilled portion) if (slider.MaximumTrackColor is not null) - handler.PlatformView.TrackColor = slider.MaximumTrackColor.ToSKColor(); + handler.PlatformView.MaximumTrackColor = slider.MaximumTrackColor; } public static void MapThumbColor(SliderHandler handler, ISlider slider) @@ -131,7 +129,7 @@ public partial class SliderHandler : ViewHandler if (handler.PlatformView is null) return; if (slider.ThumbColor is not null) - handler.PlatformView.ThumbColor = slider.ThumbColor.ToSKColor(); + handler.PlatformView.ThumbColor = slider.ThumbColor; } public static void MapBackground(SliderHandler handler, ISlider slider) diff --git a/Views/SkiaSlider.cs b/Views/SkiaSlider.cs index 6ed6124..23ce616 100644 --- a/Views/SkiaSlider.cs +++ b/Views/SkiaSlider.cs @@ -3,20 +3,36 @@ using System; using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; using SkiaSharp; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered slider control with full XAML styling support. +/// Skia-rendered slider control with full MAUI compliance. +/// Implements ISlider interface requirements: +/// - Minimum, Maximum, Value properties +/// - MinimumTrackColor, MaximumTrackColor, ThumbColor +/// - ValueChanged, DragStarted, DragCompleted events /// public class SkiaSlider : 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 - /// - /// Bindable property for Minimum. - /// public static readonly BindableProperty MinimumProperty = BindableProperty.Create( nameof(Minimum), @@ -26,9 +42,6 @@ public class SkiaSlider : SkiaView BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); - /// - /// Bindable property for Maximum. - /// public static readonly BindableProperty MaximumProperty = BindableProperty.Create( nameof(Maximum), @@ -38,87 +51,66 @@ public class SkiaSlider : SkiaView BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); - /// - /// Bindable property for Value. - /// public static readonly BindableProperty ValueProperty = BindableProperty.Create( nameof(Value), typeof(double), typeof(SkiaSlider), 0.0, - BindingMode.OneWay, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).OnValuePropertyChanged((double)o, (double)n)); - /// - /// Bindable property for TrackColor. - /// - public static readonly BindableProperty TrackColorProperty = + public static readonly BindableProperty MinimumTrackColorProperty = BindableProperty.Create( - nameof(TrackColor), - typeof(SKColor), + nameof(MinimumTrackColor), + typeof(Color), typeof(SkiaSlider), - new SKColor(0xE0, 0xE0, 0xE0), + Color.FromRgb(0x21, 0x96, 0xF3), // Material Blue - active track BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); - /// - /// Bindable property for ActiveTrackColor. - /// - public static readonly BindableProperty ActiveTrackColorProperty = + public static readonly BindableProperty MaximumTrackColorProperty = BindableProperty.Create( - nameof(ActiveTrackColor), - typeof(SKColor), + nameof(MaximumTrackColor), + typeof(Color), typeof(SkiaSlider), - new SKColor(0x21, 0x96, 0xF3), + Color.FromRgb(0xE0, 0xE0, 0xE0), // Gray - inactive track BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); - /// - /// Bindable property for ThumbColor. - /// public static readonly BindableProperty ThumbColorProperty = BindableProperty.Create( nameof(ThumbColor), - typeof(SKColor), + typeof(Color), typeof(SkiaSlider), - new SKColor(0x21, 0x96, 0xF3), + Color.FromRgb(0x21, 0x96, 0xF3), // Material Blue BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); - /// - /// Bindable property for DisabledColor. - /// public static readonly BindableProperty DisabledColorProperty = BindableProperty.Create( nameof(DisabledColor), - typeof(SKColor), + typeof(Color), typeof(SkiaSlider), - new SKColor(0xBD, 0xBD, 0xBD), + Color.FromRgb(0xBD, 0xBD, 0xBD), BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); - /// - /// Bindable property for TrackHeight. - /// public static readonly BindableProperty TrackHeightProperty = BindableProperty.Create( nameof(TrackHeight), - typeof(float), + typeof(double), typeof(SkiaSlider), - 4f, + 4.0, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); - /// - /// Bindable property for ThumbRadius. - /// public static readonly BindableProperty ThumbRadiusProperty = BindableProperty.Create( nameof(ThumbRadius), - typeof(float), + typeof(double), typeof(SkiaSlider), - 10f, + 10.0, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).InvalidateMeasure()); @@ -154,67 +146,74 @@ public class SkiaSlider : SkiaView } /// - /// Gets or sets the track color. + /// Gets or sets the color of the track from minimum to current value. + /// This is the "active" or "filled" portion of the track. /// - public SKColor TrackColor + public Color MinimumTrackColor { - get => (SKColor)GetValue(TrackColorProperty); - set => SetValue(TrackColorProperty, value); + get => (Color)GetValue(MinimumTrackColorProperty); + set => SetValue(MinimumTrackColorProperty, value); } /// - /// Gets or sets the active track color. + /// Gets or sets the color of the track from current value to maximum. + /// This is the "inactive" or "unfilled" portion of the track. /// - public SKColor ActiveTrackColor + public Color MaximumTrackColor { - get => (SKColor)GetValue(ActiveTrackColorProperty); - set => SetValue(ActiveTrackColorProperty, value); + get => (Color)GetValue(MaximumTrackColorProperty); + set => SetValue(MaximumTrackColorProperty, value); } /// /// Gets or sets the thumb color. /// - public SKColor ThumbColor + public Color ThumbColor { - get => (SKColor)GetValue(ThumbColorProperty); + get => (Color)GetValue(ThumbColorProperty); set => SetValue(ThumbColorProperty, value); } /// - /// Gets or sets the disabled color. + /// Gets or sets the color used when disabled. /// - public SKColor DisabledColor + public Color DisabledColor { - get => (SKColor)GetValue(DisabledColorProperty); + get => (Color)GetValue(DisabledColorProperty); set => SetValue(DisabledColorProperty, value); } /// - /// Gets or sets the track height. + /// Gets or sets the track height in device-independent units. /// - public float TrackHeight + public double TrackHeight { - get => (float)GetValue(TrackHeightProperty); + get => (double)GetValue(TrackHeightProperty); set => SetValue(TrackHeightProperty, value); } /// - /// Gets or sets the thumb radius. + /// Gets or sets the thumb radius in device-independent units. /// - public float ThumbRadius + public double ThumbRadius { - get => (float)GetValue(ThumbRadiusProperty); + get => (double)GetValue(ThumbRadiusProperty); set => SetValue(ThumbRadiusProperty, value); } + /// + /// Gets whether the slider is currently being dragged. + /// + public bool IsDragging { get; private set; } + #endregion - private bool _isDragging; + #region Events /// /// Event raised when the value changes. /// - public event EventHandler? ValueChanged; + public event EventHandler? ValueChanged; /// /// Event raised when drag starts. @@ -226,16 +225,24 @@ public class SkiaSlider : SkiaView /// public event EventHandler? DragCompleted; + #endregion + + #region Constructor + public SkiaSlider() { IsFocusable = true; } + #endregion + + #region Event Handlers + private void OnRangeChanged() { // Clamp value to new range var clamped = Math.Clamp(Value, Minimum, Maximum); - if (Value != clamped) + if (Math.Abs(Value - clamped) > double.Epsilon) { Value = clamped; } @@ -244,49 +251,74 @@ public class SkiaSlider : SkiaView private void OnValuePropertyChanged(double oldValue, double newValue) { - ValueChanged?.Invoke(this, new SliderValueChangedEventArgs(newValue)); + ValueChanged?.Invoke(this, new ValueChangedEventArgs(oldValue, newValue)); Invalidate(); } + #endregion + + #region Rendering + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { + var trackHeight = (float)TrackHeight; + var thumbRadius = (float)ThumbRadius; + var trackY = bounds.MidY; - var trackLeft = bounds.Left + ThumbRadius; - var trackRight = bounds.Right - ThumbRadius; + var trackLeft = bounds.Left + thumbRadius; + var trackRight = bounds.Right - thumbRadius; var trackWidth = trackRight - trackLeft; var percentage = Maximum > Minimum ? (Value - Minimum) / (Maximum - Minimum) : 0; var thumbX = trackLeft + (float)(percentage * trackWidth); - // Draw inactive track + // Get colors + var minTrackColorSK = ToSKColor(MinimumTrackColor); + var maxTrackColorSK = ToSKColor(MaximumTrackColor); + var thumbColorSK = ToSKColor(ThumbColor); + var disabledColorSK = ToSKColor(DisabledColor); + + // Draw inactive (maximum) track using var inactiveTrackPaint = new SKPaint { - Color = IsEnabled ? TrackColor : DisabledColor, + Color = IsEnabled ? maxTrackColorSK : disabledColorSK, IsAntialias = true, Style = SKPaintStyle.Fill }; var inactiveRect = new SKRoundRect( - new SKRect(trackLeft, trackY - TrackHeight / 2, trackRight, trackY + TrackHeight / 2), - TrackHeight / 2); + new SKRect(trackLeft, trackY - trackHeight / 2, trackRight, trackY + trackHeight / 2), + trackHeight / 2); canvas.DrawRoundRect(inactiveRect, inactiveTrackPaint); - // Draw active track + // Draw active (minimum) track if (percentage > 0) { using var activeTrackPaint = new SKPaint { - Color = IsEnabled ? ActiveTrackColor : DisabledColor, + Color = IsEnabled ? minTrackColorSK : disabledColorSK, IsAntialias = true, Style = SKPaintStyle.Fill }; var activeRect = new SKRoundRect( - new SKRect(trackLeft, trackY - TrackHeight / 2, thumbX, trackY + TrackHeight / 2), - TrackHeight / 2); + new SKRect(trackLeft, trackY - trackHeight / 2, thumbX, trackY + trackHeight / 2), + trackHeight / 2); canvas.DrawRoundRect(activeRect, activeTrackPaint); } + // Draw focus ring behind thumb + if (IsFocused) + { + using var focusPaint = new SKPaint + { + Color = thumbColorSK.WithAlpha(60), + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + canvas.DrawCircle(thumbX, trackY, thumbRadius + 8, focusPaint); + } + // Draw thumb shadow if (IsEnabled) { @@ -296,83 +328,99 @@ public class SkiaSlider : SkiaView IsAntialias = true, MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 3) }; - canvas.DrawCircle(thumbX + 1, trackY + 2, ThumbRadius, shadowPaint); + canvas.DrawCircle(thumbX + 1, trackY + 2, thumbRadius, shadowPaint); } // Draw thumb using var thumbPaint = new SKPaint { - Color = IsEnabled ? ThumbColor : DisabledColor, + Color = IsEnabled ? thumbColorSK : disabledColorSK, IsAntialias = true, Style = SKPaintStyle.Fill }; - canvas.DrawCircle(thumbX, trackY, ThumbRadius, thumbPaint); + canvas.DrawCircle(thumbX, trackY, thumbRadius, thumbPaint); - // Draw focus ring - if (IsFocused) + // Draw pressed state (larger thumb when dragging) + if (IsDragging) { - using var focusPaint = new SKPaint + using var pressedPaint = new SKPaint { - Color = ThumbColor.WithAlpha(60), + Color = thumbColorSK.WithAlpha(40), IsAntialias = true, Style = SKPaintStyle.Fill }; - canvas.DrawCircle(thumbX, trackY, ThumbRadius + 8, focusPaint); + canvas.DrawCircle(thumbX, trackY, thumbRadius + 4, pressedPaint); } } + #endregion + + #region Pointer Events + public override void OnPointerPressed(PointerEventArgs e) { if (!IsEnabled) return; - _isDragging = true; + IsDragging = true; UpdateValueFromPosition(e.X); DragStarted?.Invoke(this, EventArgs.Empty); - SkiaVisualStateManager.GoToState(this, "Pressed"); + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed); + e.Handled = true; } public override void OnPointerMoved(PointerEventArgs e) { - if (!IsEnabled || !_isDragging) return; + if (!IsEnabled || !IsDragging) return; UpdateValueFromPosition(e.X); } public override void OnPointerReleased(PointerEventArgs e) { - if (_isDragging) + if (IsDragging) { - _isDragging = false; + IsDragging = false; DragCompleted?.Invoke(this, EventArgs.Empty); - SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled"); + SkiaVisualStateManager.GoToState(this, IsEnabled + ? SkiaVisualStateManager.CommonStates.Normal + : SkiaVisualStateManager.CommonStates.Disabled); + Invalidate(); } } private void UpdateValueFromPosition(float x) { - var trackLeft = Bounds.Left + ThumbRadius; - var trackRight = Bounds.Right - ThumbRadius; + var thumbRadius = (float)ThumbRadius; + var trackLeft = Bounds.Left + thumbRadius; + var trackRight = Bounds.Right - thumbRadius; var trackWidth = trackRight - trackLeft; + if (trackWidth <= 0) return; + var percentage = Math.Clamp((x - trackLeft) / trackWidth, 0, 1); Value = Minimum + percentage * (Maximum - Minimum); } + #endregion + + #region Keyboard Events + public override void OnKeyDown(KeyEventArgs e) { if (!IsEnabled) return; var step = (Maximum - Minimum) / 100; // 1% steps + var largeStep = step * 10; // 10% for arrow keys switch (e.Key) { case Key.Left: case Key.Down: - Value -= step * 10; + Value -= largeStep; e.Handled = true; break; case Key.Right: case Key.Up: - Value += step * 10; + Value += largeStep; e.Handled = true; break; case Key.Home: @@ -383,26 +431,38 @@ public class SkiaSlider : SkiaView Value = Maximum; e.Handled = true; break; + case Key.PageDown: + Value -= (Maximum - Minimum) * 0.1; // 10% + e.Handled = true; + break; + case Key.PageUp: + Value += (Maximum - Minimum) * 0.1; // 10% + e.Handled = true; + break; } } + #endregion + + #region Lifecycle + protected override void OnEnabledChanged() { base.OnEnabledChanged(); - SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled"); + SkiaVisualStateManager.GoToState(this, IsEnabled + ? SkiaVisualStateManager.CommonStates.Normal + : SkiaVisualStateManager.CommonStates.Disabled); } + #endregion + + #region Layout + protected override SKSize MeasureOverride(SKSize availableSize) { - return new SKSize(200, ThumbRadius * 2 + 16); + var thumbRadius = (float)ThumbRadius; + return new SKSize(200, thumbRadius * 2 + 16); } -} -/// -/// Event args for slider value changed events. -/// -public class SliderValueChangedEventArgs : EventArgs -{ - public double NewValue { get; } - public SliderValueChangedEventArgs(double newValue) => NewValue = newValue; + #endregion }