Stepper completed

This commit is contained in:
2026-01-16 05:01:27 +00:00
parent 083f110cf4
commit 436c9d60cb
2 changed files with 350 additions and 61 deletions

View File

@@ -5,12 +5,12 @@ using Microsoft.Maui.Handlers;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Handlers;
/// <summary>
/// Handler for Stepper on Linux using Skia rendering.
/// Maps IStepper interface to SkiaStepper platform view.
/// </summary>
public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
{
@@ -52,12 +52,21 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
// Apply dark theme colors if needed
if (Application.Current?.UserAppTheme == AppTheme.Dark)
{
platformView.ButtonBackgroundColor = new SKColor(66, 66, 66);
platformView.ButtonPressedColor = new SKColor(97, 97, 97);
platformView.ButtonDisabledColor = new SKColor(48, 48, 48);
platformView.SymbolColor = new SKColor(224, 224, 224);
platformView.SymbolDisabledColor = new SKColor(97, 97, 97);
platformView.BorderColor = new SKColor(97, 97, 97);
platformView.ButtonBackgroundColor = Color.FromRgb(66, 66, 66);
platformView.ButtonPressedColor = Color.FromRgb(97, 97, 97);
platformView.ButtonDisabledColor = Color.FromRgb(48, 48, 48);
platformView.SymbolColor = Color.FromRgb(224, 224, 224);
platformView.SymbolDisabledColor = Color.FromRgb(97, 97, 97);
platformView.BorderColor = Color.FromRgb(97, 97, 97);
}
// Sync properties
if (VirtualView != null)
{
MapValue(this, VirtualView);
MapMinimum(this, VirtualView);
MapMaximum(this, VirtualView);
MapIsEnabled(this, VirtualView);
}
}
@@ -67,16 +76,22 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
base.DisconnectHandler(platformView);
}
private void OnValueChanged(object? sender, EventArgs e)
private void OnValueChanged(object? sender, ValueChangedEventArgs e)
{
if (VirtualView is null || PlatformView is null) return;
VirtualView.Value = PlatformView.Value;
if (Math.Abs(VirtualView.Value - e.NewValue) > 0.0001)
{
VirtualView.Value = e.NewValue;
}
}
public static void MapValue(StepperHandler handler, IStepper stepper)
{
if (handler.PlatformView is null) return;
handler.PlatformView.Value = stepper.Value;
if (Math.Abs(handler.PlatformView.Value - stepper.Value) > 0.0001)
handler.PlatformView.Value = stepper.Value;
}
public static void MapMinimum(StepperHandler handler, IStepper stepper)
@@ -115,5 +130,6 @@ public partial class StepperHandler : ViewHandler<IStepper, SkiaStepper>
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsEnabled = stepper.IsEnabled;
handler.PlatformView.Invalidate();
}
}

View File

@@ -1,205 +1,404 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Skia-rendered stepper control with increment/decrement buttons.
/// Implements IStepper interface requirements:
/// - Minimum, Maximum, Value, Increment properties
/// - ValueChanged event with old/new values
/// </summary>
public class SkiaStepper : 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
public static readonly BindableProperty ValueProperty =
BindableProperty.Create(nameof(Value), typeof(double), typeof(SkiaStepper), 0.0, BindingMode.OneWay,
BindableProperty.Create(
nameof(Value),
typeof(double),
typeof(SkiaStepper),
0.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStepper)b).OnValuePropertyChanged((double)o, (double)n));
public static readonly BindableProperty MinimumProperty =
BindableProperty.Create(nameof(Minimum), typeof(double), typeof(SkiaStepper), 0.0, BindingMode.TwoWay,
BindableProperty.Create(
nameof(Minimum),
typeof(double),
typeof(SkiaStepper),
0.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStepper)b).OnRangeChanged());
public static readonly BindableProperty MaximumProperty =
BindableProperty.Create(nameof(Maximum), typeof(double), typeof(SkiaStepper), 100.0, BindingMode.TwoWay,
BindableProperty.Create(
nameof(Maximum),
typeof(double),
typeof(SkiaStepper),
100.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStepper)b).OnRangeChanged());
public static readonly BindableProperty IncrementProperty =
BindableProperty.Create(nameof(Increment), typeof(double), typeof(SkiaStepper), 1.0, BindingMode.TwoWay);
BindableProperty.Create(
nameof(Increment),
typeof(double),
typeof(SkiaStepper),
1.0,
BindingMode.TwoWay);
public static readonly BindableProperty ButtonBackgroundColorProperty =
BindableProperty.Create(nameof(ButtonBackgroundColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xE0, 0xE0, 0xE0), BindingMode.TwoWay,
BindableProperty.Create(
nameof(ButtonBackgroundColor),
typeof(Color),
typeof(SkiaStepper),
Color.FromRgb(0xE0, 0xE0, 0xE0),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate());
public static readonly BindableProperty ButtonPressedColorProperty =
BindableProperty.Create(nameof(ButtonPressedColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xBD, 0xBD, 0xBD), BindingMode.TwoWay,
BindableProperty.Create(
nameof(ButtonPressedColor),
typeof(Color),
typeof(SkiaStepper),
Color.FromRgb(0xBD, 0xBD, 0xBD),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate());
public static readonly BindableProperty ButtonDisabledColorProperty =
BindableProperty.Create(nameof(ButtonDisabledColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xF5, 0xF5, 0xF5), BindingMode.TwoWay,
BindableProperty.Create(
nameof(ButtonDisabledColor),
typeof(Color),
typeof(SkiaStepper),
Color.FromRgb(0xF5, 0xF5, 0xF5),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate());
public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create(nameof(BorderColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xBD, 0xBD, 0xBD), BindingMode.TwoWay,
BindableProperty.Create(
nameof(BorderColor),
typeof(Color),
typeof(SkiaStepper),
Color.FromRgb(0xBD, 0xBD, 0xBD),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate());
public static readonly BindableProperty SymbolColorProperty =
BindableProperty.Create(nameof(SymbolColor), typeof(SKColor), typeof(SkiaStepper), SKColors.Black, BindingMode.TwoWay,
BindableProperty.Create(
nameof(SymbolColor),
typeof(Color),
typeof(SkiaStepper),
Colors.Black,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate());
public static readonly BindableProperty SymbolDisabledColorProperty =
BindableProperty.Create(nameof(SymbolDisabledColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xBD, 0xBD, 0xBD), BindingMode.TwoWay,
BindableProperty.Create(
nameof(SymbolDisabledColor),
typeof(Color),
typeof(SkiaStepper),
Color.FromRgb(0xBD, 0xBD, 0xBD),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate());
public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaStepper), 4f, BindingMode.TwoWay,
BindableProperty.Create(
nameof(CornerRadius),
typeof(double),
typeof(SkiaStepper),
4.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate());
public static readonly BindableProperty ButtonWidthProperty =
BindableProperty.Create(nameof(ButtonWidth), typeof(float), typeof(SkiaStepper), 40f, BindingMode.TwoWay,
BindableProperty.Create(
nameof(ButtonWidth),
typeof(double),
typeof(SkiaStepper),
40.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaStepper)b).InvalidateMeasure());
#endregion
#region Properties
/// <summary>
/// Gets or sets the current value.
/// </summary>
public double Value
{
get => (double)GetValue(ValueProperty);
set => SetValue(ValueProperty, Math.Clamp(value, Minimum, Maximum));
}
/// <summary>
/// Gets or sets the minimum value.
/// </summary>
public double Minimum
{
get => (double)GetValue(MinimumProperty);
set => SetValue(MinimumProperty, value);
}
/// <summary>
/// Gets or sets the maximum value.
/// </summary>
public double Maximum
{
get => (double)GetValue(MaximumProperty);
set => SetValue(MaximumProperty, value);
}
/// <summary>
/// Gets or sets the increment amount.
/// </summary>
public double Increment
{
get => (double)GetValue(IncrementProperty);
set => SetValue(IncrementProperty, Math.Max(0.001, value));
}
public SKColor ButtonBackgroundColor
/// <summary>
/// Gets or sets the button background color.
/// </summary>
public Color ButtonBackgroundColor
{
get => (SKColor)GetValue(ButtonBackgroundColorProperty);
get => (Color)GetValue(ButtonBackgroundColorProperty);
set => SetValue(ButtonBackgroundColorProperty, value);
}
public SKColor ButtonPressedColor
/// <summary>
/// Gets or sets the button pressed color.
/// </summary>
public Color ButtonPressedColor
{
get => (SKColor)GetValue(ButtonPressedColorProperty);
get => (Color)GetValue(ButtonPressedColorProperty);
set => SetValue(ButtonPressedColorProperty, value);
}
public SKColor ButtonDisabledColor
/// <summary>
/// Gets or sets the button disabled color.
/// </summary>
public Color ButtonDisabledColor
{
get => (SKColor)GetValue(ButtonDisabledColorProperty);
get => (Color)GetValue(ButtonDisabledColorProperty);
set => SetValue(ButtonDisabledColorProperty, value);
}
public SKColor BorderColor
/// <summary>
/// Gets or sets the border color.
/// </summary>
public Color BorderColor
{
get => (SKColor)GetValue(BorderColorProperty);
get => (Color)GetValue(BorderColorProperty);
set => SetValue(BorderColorProperty, value);
}
public SKColor SymbolColor
/// <summary>
/// Gets or sets the symbol color.
/// </summary>
public Color SymbolColor
{
get => (SKColor)GetValue(SymbolColorProperty);
get => (Color)GetValue(SymbolColorProperty);
set => SetValue(SymbolColorProperty, value);
}
public SKColor SymbolDisabledColor
/// <summary>
/// Gets or sets the symbol disabled color.
/// </summary>
public Color SymbolDisabledColor
{
get => (SKColor)GetValue(SymbolDisabledColorProperty);
get => (Color)GetValue(SymbolDisabledColorProperty);
set => SetValue(SymbolDisabledColorProperty, value);
}
public float CornerRadius
/// <summary>
/// Gets or sets the corner radius.
/// </summary>
public double CornerRadius
{
get => (float)GetValue(CornerRadiusProperty);
get => (double)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public float ButtonWidth
/// <summary>
/// Gets or sets the button width.
/// </summary>
public double ButtonWidth
{
get => (float)GetValue(ButtonWidthProperty);
get => (double)GetValue(ButtonWidthProperty);
set => SetValue(ButtonWidthProperty, value);
}
/// <summary>
/// Gets whether the minus button is currently pressed.
/// </summary>
public bool IsMinusPressed { get; private set; }
/// <summary>
/// Gets whether the plus button is currently pressed.
/// </summary>
public bool IsPlusPressed { get; private set; }
#endregion
private bool _isMinusPressed;
private bool _isPlusPressed;
#region Events
public event EventHandler? ValueChanged;
/// <summary>
/// Event raised when the value changes.
/// </summary>
public event EventHandler<ValueChangedEventArgs>? ValueChanged;
#endregion
#region Constructor
public SkiaStepper()
{
IsFocusable = true;
}
#endregion
#region Event Handlers
private void OnValuePropertyChanged(double oldValue, double newValue)
{
ValueChanged?.Invoke(this, EventArgs.Empty);
ValueChanged?.Invoke(this, new ValueChangedEventArgs(oldValue, newValue));
Invalidate();
}
private void OnRangeChanged()
{
var clamped = Math.Clamp(Value, Minimum, Maximum);
if (Value != clamped)
if (Math.Abs(Value - clamped) > double.Epsilon)
{
Value = clamped;
}
Invalidate();
}
#endregion
#region Rendering
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
var minusRect = new SKRect(bounds.Left, bounds.Top, bounds.Left + ButtonWidth, bounds.Bottom);
var plusRect = new SKRect(bounds.Right - ButtonWidth, bounds.Top, bounds.Right, bounds.Bottom);
var buttonWidth = (float)ButtonWidth;
var cornerRadius = (float)CornerRadius;
DrawButton(canvas, minusRect, "-", _isMinusPressed, !CanDecrement());
DrawButton(canvas, plusRect, "+", _isPlusPressed, !CanIncrement());
var minusRect = new SKRect(bounds.Left, bounds.Top, bounds.Left + buttonWidth, bounds.Bottom);
var plusRect = new SKRect(bounds.Right - buttonWidth, bounds.Top, bounds.Right, bounds.Bottom);
// Get colors
var buttonBgColorSK = ToSKColor(ButtonBackgroundColor);
var buttonPressedColorSK = ToSKColor(ButtonPressedColor);
var buttonDisabledColorSK = ToSKColor(ButtonDisabledColor);
var borderColorSK = ToSKColor(BorderColor);
var symbolColorSK = ToSKColor(SymbolColor);
var symbolDisabledColorSK = ToSKColor(SymbolDisabledColor);
// Draw focus ring
if (IsFocused)
{
using var focusPaint = new SKPaint
{
Color = ToSKColor(Color.FromRgb(0x21, 0x96, 0xF3)).WithAlpha(60),
Style = SKPaintStyle.Stroke,
StrokeWidth = 3,
IsAntialias = true
};
var focusRect = new SKRect(bounds.Left - 2, bounds.Top - 2, bounds.Right + 2, bounds.Bottom + 2);
canvas.DrawRoundRect(new SKRoundRect(focusRect, cornerRadius + 2), focusPaint);
}
DrawButton(canvas, minusRect, "-", IsMinusPressed, !CanDecrement(),
buttonBgColorSK, buttonPressedColorSK, buttonDisabledColorSK,
symbolColorSK, symbolDisabledColorSK, cornerRadius, true);
DrawButton(canvas, plusRect, "+", IsPlusPressed, !CanIncrement(),
buttonBgColorSK, buttonPressedColorSK, buttonDisabledColorSK,
symbolColorSK, symbolDisabledColorSK, cornerRadius, false);
// Draw border
using var borderPaint = new SKPaint
{
Color = BorderColor,
Color = borderColorSK,
Style = SKPaintStyle.Stroke,
StrokeWidth = 1,
IsAntialias = true
};
var totalRect = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom);
canvas.DrawRoundRect(new SKRoundRect(totalRect, CornerRadius), borderPaint);
canvas.DrawRoundRect(new SKRoundRect(totalRect, cornerRadius), borderPaint);
// Draw center divider
var centerX = bounds.MidX;
canvas.DrawLine(centerX, bounds.Top, centerX, bounds.Bottom, borderPaint);
}
private void DrawButton(SKCanvas canvas, SKRect rect, string symbol, bool isPressed, bool isDisabled)
private void DrawButton(SKCanvas canvas, SKRect rect, string symbol, bool isPressed, bool isDisabled,
SKColor bgColor, SKColor pressedColor, SKColor disabledColor,
SKColor symbolColor, SKColor symbolDisabledColor, float cornerRadius, bool isLeft)
{
// Draw background with rounded corners on the appropriate side
using var bgPaint = new SKPaint
{
Color = isDisabled ? ButtonDisabledColor : (isPressed ? ButtonPressedColor : ButtonBackgroundColor),
Color = isDisabled ? disabledColor : (isPressed ? pressedColor : bgColor),
Style = SKPaintStyle.Fill,
IsAntialias = true
};
canvas.DrawRect(rect, bgPaint);
// Create path for rounded corners on one side only
using var path = new SKPath();
if (isLeft)
{
path.MoveTo(rect.Left + cornerRadius, rect.Top);
path.LineTo(rect.Right, rect.Top);
path.LineTo(rect.Right, rect.Bottom);
path.LineTo(rect.Left + cornerRadius, rect.Bottom);
path.ArcTo(new SKRect(rect.Left, rect.Bottom - cornerRadius * 2, rect.Left + cornerRadius * 2, rect.Bottom), 90, 90, false);
path.LineTo(rect.Left, rect.Top + cornerRadius);
path.ArcTo(new SKRect(rect.Left, rect.Top, rect.Left + cornerRadius * 2, rect.Top + cornerRadius * 2), 180, 90, false);
}
else
{
path.MoveTo(rect.Left, rect.Top);
path.LineTo(rect.Right - cornerRadius, rect.Top);
path.ArcTo(new SKRect(rect.Right - cornerRadius * 2, rect.Top, rect.Right, rect.Top + cornerRadius * 2), 270, 90, false);
path.LineTo(rect.Right, rect.Bottom - cornerRadius);
path.ArcTo(new SKRect(rect.Right - cornerRadius * 2, rect.Bottom - cornerRadius * 2, rect.Right, rect.Bottom), 0, 90, false);
path.LineTo(rect.Left, rect.Bottom);
}
path.Close();
canvas.DrawPath(path, bgPaint);
// Draw symbol
using var font = new SKFont(SKTypeface.Default, 20);
using var textPaint = new SKPaint(font)
{
Color = isDisabled ? SymbolDisabledColor : SymbolColor,
Color = isDisabled ? symbolDisabledColor : symbolColor,
IsAntialias = true
};
@@ -208,33 +407,72 @@ public class SkiaStepper : SkiaView
canvas.DrawText(symbol, rect.MidX - textBounds.MidX, rect.MidY - textBounds.MidY, textPaint);
}
#endregion
#region Helper Methods
private bool CanIncrement() => IsEnabled && Value < Maximum;
private bool CanDecrement() => IsEnabled && Value > Minimum;
#endregion
#region Pointer Events
public override void OnPointerPressed(PointerEventArgs e)
{
if (!IsEnabled) return;
if (e.X < ButtonWidth)
var buttonWidth = (float)ButtonWidth;
if (e.X < buttonWidth)
{
_isMinusPressed = true;
IsMinusPressed = true;
if (CanDecrement()) Value -= Increment;
SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed);
}
else if (e.X > Bounds.Width - ButtonWidth)
else if (e.X > Bounds.Width - buttonWidth)
{
_isPlusPressed = true;
IsPlusPressed = true;
if (CanIncrement()) Value += Increment;
SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed);
}
e.Handled = true;
Invalidate();
}
public override void OnPointerReleased(PointerEventArgs e)
{
_isMinusPressed = false;
_isPlusPressed = false;
Invalidate();
if (IsMinusPressed || IsPlusPressed)
{
IsMinusPressed = false;
IsPlusPressed = false;
SkiaVisualStateManager.GoToState(this, IsEnabled
? SkiaVisualStateManager.CommonStates.Normal
: SkiaVisualStateManager.CommonStates.Disabled);
Invalidate();
}
}
public override void OnPointerEntered(PointerEventArgs e)
{
if (IsEnabled)
{
SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver);
}
}
public override void OnPointerExited(PointerEventArgs e)
{
SkiaVisualStateManager.GoToState(this, IsEnabled
? SkiaVisualStateManager.CommonStates.Normal
: SkiaVisualStateManager.CommonStates.Disabled);
}
#endregion
#region Keyboard Events
public override void OnKeyDown(KeyEventArgs e)
{
if (!IsEnabled) return;
@@ -251,11 +489,46 @@ public class SkiaStepper : SkiaView
if (CanDecrement()) Value -= Increment;
e.Handled = true;
break;
case Key.PageUp:
if (CanIncrement()) Value += Increment * 10;
e.Handled = true;
break;
case Key.PageDown:
if (CanDecrement()) Value -= Increment * 10;
e.Handled = true;
break;
case Key.Home:
Value = Minimum;
e.Handled = true;
break;
case Key.End:
Value = Maximum;
e.Handled = true;
break;
}
}
#endregion
#region Lifecycle
protected override void OnEnabledChanged()
{
base.OnEnabledChanged();
SkiaVisualStateManager.GoToState(this, IsEnabled
? SkiaVisualStateManager.CommonStates.Normal
: SkiaVisualStateManager.CommonStates.Disabled);
}
#endregion
#region Layout
protected override SKSize MeasureOverride(SKSize availableSize)
{
return new SKSize(ButtonWidth * 2 + 1, 32);
var buttonWidth = (float)ButtonWidth;
return new SKSize(buttonWidth * 2 + 1, 32);
}
#endregion
}