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); 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; 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; if (handler.PlatformView is null) return;
// MinimumTrackColor maps to ActiveTrackColor (the filled portion)
if (slider.MinimumTrackColor is not null) 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) public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider)
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
// MaximumTrackColor maps to TrackColor (the unfilled portion)
if (slider.MaximumTrackColor is not null) 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) 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 (handler.PlatformView is null) return;
if (slider.ThumbColor is not null) 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) public static void MapBackground(SliderHandler handler, ISlider slider)

View File

@@ -3,20 +3,36 @@
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 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> /// </summary>
public class SkiaSlider : SkiaView 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 #region BindableProperties
/// <summary>
/// Bindable property for Minimum.
/// </summary>
public static readonly BindableProperty MinimumProperty = public static readonly BindableProperty MinimumProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(Minimum), nameof(Minimum),
@@ -26,9 +42,6 @@ public class SkiaSlider : SkiaView
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged());
/// <summary>
/// Bindable property for Maximum.
/// </summary>
public static readonly BindableProperty MaximumProperty = public static readonly BindableProperty MaximumProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(Maximum), nameof(Maximum),
@@ -38,87 +51,66 @@ public class SkiaSlider : SkiaView
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged());
/// <summary>
/// Bindable property for Value.
/// </summary>
public static readonly BindableProperty ValueProperty = public static readonly BindableProperty ValueProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(Value), nameof(Value),
typeof(double), typeof(double),
typeof(SkiaSlider), typeof(SkiaSlider),
0.0, 0.0,
BindingMode.OneWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).OnValuePropertyChanged((double)o, (double)n)); propertyChanged: (b, o, n) => ((SkiaSlider)b).OnValuePropertyChanged((double)o, (double)n));
/// <summary> public static readonly BindableProperty MinimumTrackColorProperty =
/// Bindable property for TrackColor.
/// </summary>
public static readonly BindableProperty TrackColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(TrackColor), nameof(MinimumTrackColor),
typeof(SKColor), typeof(Color),
typeof(SkiaSlider), typeof(SkiaSlider),
new SKColor(0xE0, 0xE0, 0xE0), Color.FromRgb(0x21, 0x96, 0xF3), // Material Blue - active track
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate());
/// <summary> public static readonly BindableProperty MaximumTrackColorProperty =
/// Bindable property for ActiveTrackColor.
/// </summary>
public static readonly BindableProperty ActiveTrackColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(ActiveTrackColor), nameof(MaximumTrackColor),
typeof(SKColor), typeof(Color),
typeof(SkiaSlider), typeof(SkiaSlider),
new SKColor(0x21, 0x96, 0xF3), Color.FromRgb(0xE0, 0xE0, 0xE0), // Gray - inactive track
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSlider)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(SkiaSlider), typeof(SkiaSlider),
new SKColor(0x21, 0x96, 0xF3), Color.FromRgb(0x21, 0x96, 0xF3), // Material Blue
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSlider)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(SkiaSlider), typeof(SkiaSlider),
new SKColor(0xBD, 0xBD, 0xBD), Color.FromRgb(0xBD, 0xBD, 0xBD),
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate());
/// <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(SkiaSlider), typeof(SkiaSlider),
4f, 4.0,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate());
/// <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(SkiaSlider), typeof(SkiaSlider),
10f, 10.0,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).InvalidateMeasure()); propertyChanged: (b, o, n) => ((SkiaSlider)b).InvalidateMeasure());
@@ -154,67 +146,74 @@ public class SkiaSlider : SkiaView
} }
/// <summary> /// <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> /// </summary>
public SKColor TrackColor public Color MinimumTrackColor
{ {
get => (SKColor)GetValue(TrackColorProperty); get => (Color)GetValue(MinimumTrackColorProperty);
set => SetValue(TrackColorProperty, value); set => SetValue(MinimumTrackColorProperty, value);
} }
/// <summary> /// <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> /// </summary>
public SKColor ActiveTrackColor public Color MaximumTrackColor
{ {
get => (SKColor)GetValue(ActiveTrackColorProperty); get => (Color)GetValue(MaximumTrackColorProperty);
set => SetValue(ActiveTrackColorProperty, value); set => SetValue(MaximumTrackColorProperty, 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 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 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>
/// Gets whether the slider is currently being dragged.
/// </summary>
public bool IsDragging { get; private set; }
#endregion #endregion
private bool _isDragging; #region Events
/// <summary> /// <summary>
/// Event raised when the value changes. /// Event raised when the value changes.
/// </summary> /// </summary>
public event EventHandler<SliderValueChangedEventArgs>? ValueChanged; public event EventHandler<ValueChangedEventArgs>? ValueChanged;
/// <summary> /// <summary>
/// Event raised when drag starts. /// Event raised when drag starts.
@@ -226,16 +225,24 @@ public class SkiaSlider : SkiaView
/// </summary> /// </summary>
public event EventHandler? DragCompleted; public event EventHandler? DragCompleted;
#endregion
#region Constructor
public SkiaSlider() public SkiaSlider()
{ {
IsFocusable = true; IsFocusable = true;
} }
#endregion
#region Event Handlers
private void OnRangeChanged() private void OnRangeChanged()
{ {
// Clamp value to new range // Clamp value to new range
var clamped = Math.Clamp(Value, Minimum, Maximum); var clamped = Math.Clamp(Value, Minimum, Maximum);
if (Value != clamped) if (Math.Abs(Value - clamped) > double.Epsilon)
{ {
Value = clamped; Value = clamped;
} }
@@ -244,49 +251,74 @@ public class SkiaSlider : SkiaView
private void OnValuePropertyChanged(double oldValue, double newValue) private void OnValuePropertyChanged(double oldValue, double newValue)
{ {
ValueChanged?.Invoke(this, new SliderValueChangedEventArgs(newValue)); ValueChanged?.Invoke(this, new ValueChangedEventArgs(oldValue, newValue));
Invalidate(); Invalidate();
} }
#endregion
#region Rendering
protected override void OnDraw(SKCanvas canvas, SKRect bounds) protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{ {
var trackHeight = (float)TrackHeight;
var thumbRadius = (float)ThumbRadius;
var trackY = bounds.MidY; var trackY = bounds.MidY;
var trackLeft = bounds.Left + ThumbRadius; var trackLeft = bounds.Left + thumbRadius;
var trackRight = bounds.Right - ThumbRadius; var trackRight = bounds.Right - thumbRadius;
var trackWidth = trackRight - trackLeft; var trackWidth = trackRight - trackLeft;
var percentage = Maximum > Minimum ? (Value - Minimum) / (Maximum - Minimum) : 0; var percentage = Maximum > Minimum ? (Value - Minimum) / (Maximum - Minimum) : 0;
var thumbX = trackLeft + (float)(percentage * trackWidth); 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 using var inactiveTrackPaint = new SKPaint
{ {
Color = IsEnabled ? TrackColor : DisabledColor, Color = IsEnabled ? maxTrackColorSK : disabledColorSK,
IsAntialias = true, IsAntialias = true,
Style = SKPaintStyle.Fill Style = SKPaintStyle.Fill
}; };
var inactiveRect = new SKRoundRect( var inactiveRect = new SKRoundRect(
new SKRect(trackLeft, trackY - TrackHeight / 2, trackRight, trackY + TrackHeight / 2), new SKRect(trackLeft, trackY - trackHeight / 2, trackRight, trackY + trackHeight / 2),
TrackHeight / 2); trackHeight / 2);
canvas.DrawRoundRect(inactiveRect, inactiveTrackPaint); canvas.DrawRoundRect(inactiveRect, inactiveTrackPaint);
// Draw active track // Draw active (minimum) track
if (percentage > 0) if (percentage > 0)
{ {
using var activeTrackPaint = new SKPaint using var activeTrackPaint = new SKPaint
{ {
Color = IsEnabled ? ActiveTrackColor : DisabledColor, Color = IsEnabled ? minTrackColorSK : disabledColorSK,
IsAntialias = true, IsAntialias = true,
Style = SKPaintStyle.Fill Style = SKPaintStyle.Fill
}; };
var activeRect = new SKRoundRect( var activeRect = new SKRoundRect(
new SKRect(trackLeft, trackY - TrackHeight / 2, thumbX, trackY + TrackHeight / 2), new SKRect(trackLeft, trackY - trackHeight / 2, thumbX, trackY + trackHeight / 2),
TrackHeight / 2); trackHeight / 2);
canvas.DrawRoundRect(activeRect, activeTrackPaint); 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 // Draw thumb shadow
if (IsEnabled) if (IsEnabled)
{ {
@@ -296,83 +328,99 @@ public class SkiaSlider : SkiaView
IsAntialias = true, IsAntialias = true,
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 3) MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 3)
}; };
canvas.DrawCircle(thumbX + 1, trackY + 2, ThumbRadius, shadowPaint); canvas.DrawCircle(thumbX + 1, trackY + 2, thumbRadius, shadowPaint);
} }
// Draw thumb // Draw thumb
using var thumbPaint = new SKPaint using var thumbPaint = new SKPaint
{ {
Color = IsEnabled ? ThumbColor : DisabledColor, Color = IsEnabled ? thumbColorSK : disabledColorSK,
IsAntialias = true, IsAntialias = true,
Style = SKPaintStyle.Fill Style = SKPaintStyle.Fill
}; };
canvas.DrawCircle(thumbX, trackY, ThumbRadius, thumbPaint); canvas.DrawCircle(thumbX, trackY, thumbRadius, thumbPaint);
// Draw focus ring // Draw pressed state (larger thumb when dragging)
if (IsFocused) if (IsDragging)
{ {
using var focusPaint = new SKPaint using var pressedPaint = new SKPaint
{ {
Color = ThumbColor.WithAlpha(60), Color = thumbColorSK.WithAlpha(40),
IsAntialias = true, IsAntialias = true,
Style = SKPaintStyle.Fill 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) public override void OnPointerPressed(PointerEventArgs e)
{ {
if (!IsEnabled) return; if (!IsEnabled) return;
_isDragging = true; IsDragging = true;
UpdateValueFromPosition(e.X); UpdateValueFromPosition(e.X);
DragStarted?.Invoke(this, EventArgs.Empty); DragStarted?.Invoke(this, EventArgs.Empty);
SkiaVisualStateManager.GoToState(this, "Pressed"); SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed);
e.Handled = true;
} }
public override void OnPointerMoved(PointerEventArgs e) public override void OnPointerMoved(PointerEventArgs e)
{ {
if (!IsEnabled || !_isDragging) return; if (!IsEnabled || !IsDragging) return;
UpdateValueFromPosition(e.X); UpdateValueFromPosition(e.X);
} }
public override void OnPointerReleased(PointerEventArgs e) public override void OnPointerReleased(PointerEventArgs e)
{ {
if (_isDragging) if (IsDragging)
{ {
_isDragging = false; IsDragging = false;
DragCompleted?.Invoke(this, EventArgs.Empty); 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) private void UpdateValueFromPosition(float x)
{ {
var trackLeft = Bounds.Left + ThumbRadius; var thumbRadius = (float)ThumbRadius;
var trackRight = Bounds.Right - ThumbRadius; var trackLeft = Bounds.Left + thumbRadius;
var trackRight = Bounds.Right - thumbRadius;
var trackWidth = trackRight - trackLeft; var trackWidth = trackRight - trackLeft;
if (trackWidth <= 0) return;
var percentage = Math.Clamp((x - trackLeft) / trackWidth, 0, 1); var percentage = Math.Clamp((x - trackLeft) / trackWidth, 0, 1);
Value = Minimum + percentage * (Maximum - Minimum); Value = Minimum + percentage * (Maximum - Minimum);
} }
#endregion
#region Keyboard Events
public override void OnKeyDown(KeyEventArgs e) public override void OnKeyDown(KeyEventArgs e)
{ {
if (!IsEnabled) return; if (!IsEnabled) return;
var step = (Maximum - Minimum) / 100; // 1% steps var step = (Maximum - Minimum) / 100; // 1% steps
var largeStep = step * 10; // 10% for arrow keys
switch (e.Key) switch (e.Key)
{ {
case Key.Left: case Key.Left:
case Key.Down: case Key.Down:
Value -= step * 10; Value -= largeStep;
e.Handled = true; e.Handled = true;
break; break;
case Key.Right: case Key.Right:
case Key.Up: case Key.Up:
Value += step * 10; Value += largeStep;
e.Handled = true; e.Handled = true;
break; break;
case Key.Home: case Key.Home:
@@ -383,26 +431,38 @@ public class SkiaSlider : SkiaView
Value = Maximum; Value = Maximum;
e.Handled = true; e.Handled = true;
break; 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() protected override void OnEnabledChanged()
{ {
base.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) 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> #endregion
/// Event args for slider value changed events.
/// </summary>
public class SliderValueChangedEventArgs : EventArgs
{
public double NewValue { get; }
public SliderValueChangedEventArgs(double newValue) => NewValue = newValue;
} }