Slider completed

This commit is contained in:
2026-01-16 04:57:40 +00:00
parent f263ee96b3
commit 083f110cf4
2 changed files with 170 additions and 112 deletions

View File

@@ -68,7 +68,7 @@ public partial class SliderHandler : ViewHandler<ISlider, SkiaSlider>
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<ISlider, SkiaSlider>
{
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<ISlider, SkiaSlider>
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)

View File

@@ -3,20 +3,36 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
/// <summary>
/// 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
/// </summary>
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
/// <summary>
/// Bindable property for Minimum.
/// </summary>
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());
/// <summary>
/// Bindable property for Maximum.
/// </summary>
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());
/// <summary>
/// Bindable property for Value.
/// </summary>
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));
/// <summary>
/// Bindable property for TrackColor.
/// </summary>
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());
/// <summary>
/// Bindable property for ActiveTrackColor.
/// </summary>
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());
/// <summary>
/// Bindable property for ThumbColor.
/// </summary>
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());
/// <summary>
/// Bindable property for DisabledColor.
/// </summary>
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());
/// <summary>
/// Bindable property for TrackHeight.
/// </summary>
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());
/// <summary>
/// Bindable property for ThumbRadius.
/// </summary>
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
}
/// <summary>
/// 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.
/// </summary>
public SKColor TrackColor
public Color MinimumTrackColor
{
get => (SKColor)GetValue(TrackColorProperty);
set => SetValue(TrackColorProperty, value);
get => (Color)GetValue(MinimumTrackColorProperty);
set => SetValue(MinimumTrackColorProperty, value);
}
/// <summary>
/// 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.
/// </summary>
public SKColor ActiveTrackColor
public Color MaximumTrackColor
{
get => (SKColor)GetValue(ActiveTrackColorProperty);
set => SetValue(ActiveTrackColorProperty, value);
get => (Color)GetValue(MaximumTrackColorProperty);
set => SetValue(MaximumTrackColorProperty, value);
}
/// <summary>
/// Gets or sets the thumb color.
/// </summary>
public SKColor ThumbColor
public Color ThumbColor
{
get => (SKColor)GetValue(ThumbColorProperty);
get => (Color)GetValue(ThumbColorProperty);
set => SetValue(ThumbColorProperty, value);
}
/// <summary>
/// Gets or sets the disabled color.
/// Gets or sets the color used when disabled.
/// </summary>
public SKColor DisabledColor
public Color DisabledColor
{
get => (SKColor)GetValue(DisabledColorProperty);
get => (Color)GetValue(DisabledColorProperty);
set => SetValue(DisabledColorProperty, value);
}
/// <summary>
/// Gets or sets the track height.
/// Gets or sets the track height in device-independent units.
/// </summary>
public float TrackHeight
public double TrackHeight
{
get => (float)GetValue(TrackHeightProperty);
get => (double)GetValue(TrackHeightProperty);
set => SetValue(TrackHeightProperty, value);
}
/// <summary>
/// Gets or sets the thumb radius.
/// Gets or sets the thumb radius in device-independent units.
/// </summary>
public float ThumbRadius
public double ThumbRadius
{
get => (float)GetValue(ThumbRadiusProperty);
get => (double)GetValue(ThumbRadiusProperty);
set => SetValue(ThumbRadiusProperty, value);
}
/// <summary>
/// Gets whether the slider is currently being dragged.
/// </summary>
public bool IsDragging { get; private set; }
#endregion
private bool _isDragging;
#region Events
/// <summary>
/// Event raised when the value changes.
/// </summary>
public event EventHandler<SliderValueChangedEventArgs>? ValueChanged;
public event EventHandler<ValueChangedEventArgs>? ValueChanged;
/// <summary>
/// Event raised when drag starts.
@@ -226,16 +225,24 @@ public class SkiaSlider : SkiaView
/// </summary>
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);
}
}
/// <summary>
/// Event args for slider value changed events.
/// </summary>
public class SliderValueChangedEventArgs : EventArgs
{
public double NewValue { get; }
public SliderValueChangedEventArgs(double newValue) => NewValue = newValue;
#endregion
}