Files

445 lines
13 KiB
C#

// 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 checkbox control with full MAUI compliance.
/// Implements ICheckBox interface requirements:
/// - IsChecked property with CheckedChanged event
/// - Color property (maps to BoxColor when checked)
/// - Foreground property (maps to CheckColor - the checkmark color)
/// </summary>
public class SkiaCheckBox : SkiaView
{
#region SKColor Helper
private static SKColor ToSKColor(Color? color)
{
if (color == null) return SKColors.Transparent;
return color.ToSKColor();
}
#endregion
#region BindableProperties
public static readonly BindableProperty IsCheckedProperty =
BindableProperty.Create(
nameof(IsChecked),
typeof(bool),
typeof(SkiaCheckBox),
false,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).OnIsCheckedChanged());
public static readonly BindableProperty ColorProperty =
BindableProperty.Create(
nameof(Color),
typeof(Color),
typeof(SkiaCheckBox),
Color.FromRgb(33, 150, 243), // Material Blue
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
public static readonly BindableProperty CheckColorProperty =
BindableProperty.Create(
nameof(CheckColor),
typeof(Color),
typeof(SkiaCheckBox),
Colors.White,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
public static readonly BindableProperty UncheckedBoxColorProperty =
BindableProperty.Create(
nameof(UncheckedBoxColor),
typeof(Color),
typeof(SkiaCheckBox),
Colors.White,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create(
nameof(BorderColor),
typeof(Color),
typeof(SkiaCheckBox),
Color.FromRgb(117, 117, 117),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
public static readonly BindableProperty DisabledColorProperty =
BindableProperty.Create(
nameof(DisabledColor),
typeof(Color),
typeof(SkiaCheckBox),
Color.FromRgb(189, 189, 189),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
public static readonly BindableProperty HoveredBorderColorProperty =
BindableProperty.Create(
nameof(HoveredBorderColor),
typeof(Color),
typeof(SkiaCheckBox),
Color.FromRgb(33, 150, 243),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
public static readonly BindableProperty BoxSizeProperty =
BindableProperty.Create(
nameof(BoxSize),
typeof(double),
typeof(SkiaCheckBox),
20.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).InvalidateMeasure());
public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.Create(
nameof(CornerRadius),
typeof(double),
typeof(SkiaCheckBox),
3.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
public static readonly BindableProperty BorderWidthProperty =
BindableProperty.Create(
nameof(BorderWidth),
typeof(double),
typeof(SkiaCheckBox),
2.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
public static readonly BindableProperty CheckStrokeWidthProperty =
BindableProperty.Create(
nameof(CheckStrokeWidth),
typeof(double),
typeof(SkiaCheckBox),
2.5,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
#endregion
#region Properties
/// <summary>
/// Gets or sets whether the checkbox is checked.
/// </summary>
public bool IsChecked
{
get => (bool)GetValue(IsCheckedProperty);
set => SetValue(IsCheckedProperty, value);
}
/// <summary>
/// Gets or sets the color of the checkbox box when checked.
/// This is the primary MAUI CheckBox.Color property.
/// </summary>
public Color Color
{
get => (Color)GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
/// <summary>
/// Gets or sets the color of the checkmark itself.
/// Maps to ICheckBox.Foreground in MAUI.
/// </summary>
public Color CheckColor
{
get => (Color)GetValue(CheckColorProperty);
set => SetValue(CheckColorProperty, value);
}
/// <summary>
/// Gets or sets the color of the checkbox box when unchecked.
/// </summary>
public Color UncheckedBoxColor
{
get => (Color)GetValue(UncheckedBoxColorProperty);
set => SetValue(UncheckedBoxColorProperty, value);
}
/// <summary>
/// Gets or sets the border color when unchecked.
/// </summary>
public Color BorderColor
{
get => (Color)GetValue(BorderColorProperty);
set => SetValue(BorderColorProperty, value);
}
/// <summary>
/// Gets or sets the color used when the control is disabled.
/// </summary>
public Color DisabledColor
{
get => (Color)GetValue(DisabledColorProperty);
set => SetValue(DisabledColorProperty, value);
}
/// <summary>
/// Gets or sets the border color when hovered.
/// </summary>
public Color HoveredBorderColor
{
get => (Color)GetValue(HoveredBorderColorProperty);
set => SetValue(HoveredBorderColorProperty, value);
}
/// <summary>
/// Gets or sets the size of the checkbox box in device-independent units.
/// </summary>
public double BoxSize
{
get => (double)GetValue(BoxSizeProperty);
set => SetValue(BoxSizeProperty, value);
}
/// <summary>
/// Gets or sets the corner radius of the checkbox box.
/// </summary>
public double CornerRadius
{
get => (double)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
/// <summary>
/// Gets or sets the border width.
/// </summary>
public double BorderWidth
{
get => (double)GetValue(BorderWidthProperty);
set => SetValue(BorderWidthProperty, value);
}
/// <summary>
/// Gets or sets the stroke width of the checkmark.
/// </summary>
public double CheckStrokeWidth
{
get => (double)GetValue(CheckStrokeWidthProperty);
set => SetValue(CheckStrokeWidthProperty, value);
}
/// <summary>
/// Gets whether the control is currently hovered.
/// </summary>
public bool IsHovered { get; private set; }
#endregion
#region Events
/// <summary>
/// Occurs when the IsChecked property changes.
/// </summary>
public event EventHandler<CheckedChangedEventArgs>? CheckedChanged;
#endregion
#region Constructor
public SkiaCheckBox()
{
IsFocusable = true;
}
#endregion
#region Event Handlers
private void OnIsCheckedChanged()
{
CheckedChanged?.Invoke(this, new CheckedChangedEventArgs(IsChecked));
SkiaVisualStateManager.GoToState(this, IsChecked ? "Checked" : "Unchecked");
Invalidate();
}
#endregion
#region Rendering
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
var boxSize = (float)BoxSize;
var cornerRadius = (float)CornerRadius;
var borderWidth = (float)BorderWidth;
// Center the checkbox box in bounds
var boxRect = new SKRect(
bounds.Left + (bounds.Width - boxSize) / 2f,
bounds.Top + (bounds.Height - boxSize) / 2f,
bounds.Left + (bounds.Width - boxSize) / 2f + boxSize,
bounds.Top + (bounds.Height - boxSize) / 2f + boxSize);
var roundRect = new SKRoundRect(boxRect, cornerRadius);
// Get colors as SKColor
var colorSK = ToSKColor(Color);
var checkColorSK = ToSKColor(CheckColor);
// Use theme-aware color for unchecked box if default white
var uncheckedBoxColorSK = UncheckedBoxColor == Colors.White
? SkiaTheme.CurrentSurfaceSK
: ToSKColor(UncheckedBoxColor);
var borderColorSK = ToSKColor(BorderColor);
var disabledColorSK = ToSKColor(DisabledColor);
var hoveredBorderColorSK = ToSKColor(HoveredBorderColor);
// Draw background
using var bgPaint = new SKPaint
{
Color = !IsEnabled ? disabledColorSK
: IsChecked ? colorSK
: uncheckedBoxColorSK,
IsAntialias = true,
Style = SKPaintStyle.Fill
};
canvas.DrawRoundRect(roundRect, bgPaint);
// Draw border
using var borderPaint = new SKPaint
{
Color = !IsEnabled ? disabledColorSK
: IsChecked ? colorSK
: IsHovered ? hoveredBorderColorSK
: borderColorSK,
IsAntialias = true,
Style = SKPaintStyle.Stroke,
StrokeWidth = borderWidth
};
canvas.DrawRoundRect(roundRect, borderPaint);
// Draw focus ring
if (IsFocused)
{
using var focusPaint = new SKPaint
{
Color = colorSK.WithAlpha(80),
IsAntialias = true,
Style = SKPaintStyle.Stroke,
StrokeWidth = 3f
};
var focusRect = new SKRoundRect(boxRect, cornerRadius);
focusRect.Inflate(4f, 4f);
canvas.DrawRoundRect(focusRect, focusPaint);
}
// Draw checkmark
if (IsChecked)
{
DrawCheckmark(canvas, boxRect, checkColorSK);
}
}
private void DrawCheckmark(SKCanvas canvas, SKRect boxRect, SKColor checkColor)
{
var checkStrokeWidth = (float)CheckStrokeWidth;
var boxSize = (float)BoxSize;
using var paint = new SKPaint
{
Color = checkColor,
IsAntialias = true,
Style = SKPaintStyle.Stroke,
StrokeWidth = checkStrokeWidth,
StrokeCap = SKStrokeCap.Round,
StrokeJoin = SKStrokeJoin.Round
};
float padding = boxSize * 0.2f;
float left = boxRect.Left + padding;
float right = boxRect.Right - padding;
float top = boxRect.Top + padding;
float bottom = boxRect.Bottom - padding;
using var path = new SKPath();
path.MoveTo(left, boxRect.MidY);
path.LineTo(boxRect.MidX - padding * 0.3f, bottom - padding * 0.5f);
path.LineTo(right, top + padding * 0.3f);
canvas.DrawPath(path, paint);
}
#endregion
#region Pointer Events
public override void OnPointerEntered(PointerEventArgs e)
{
if (IsEnabled)
{
IsHovered = true;
SkiaVisualStateManager.GoToState(this, "PointerOver");
Invalidate();
}
}
public override void OnPointerExited(PointerEventArgs e)
{
IsHovered = false;
SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled");
Invalidate();
}
public override void OnPointerPressed(PointerEventArgs e)
{
if (IsEnabled)
{
IsChecked = !IsChecked;
e.Handled = true;
}
}
public override void OnPointerReleased(PointerEventArgs e)
{
// No action needed
}
#endregion
#region Keyboard Events
public override void OnKeyDown(KeyEventArgs e)
{
if (IsEnabled && e.Key == Key.Space)
{
IsChecked = !IsChecked;
e.Handled = true;
}
}
#endregion
#region Lifecycle
protected override void OnEnabledChanged()
{
base.OnEnabledChanged();
SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled");
}
#endregion
#region Layout
protected override Size MeasureOverride(Size availableSize)
{
var boxSize = BoxSize;
return new Size(boxSize + 8.0, boxSize + 8.0);
}
#endregion
}