2025-12-19 09:30:16 +00:00
|
|
|
// Licensed to the .NET Foundation under one or more agreements.
|
|
|
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
using System;
|
|
|
|
|
using Microsoft.Maui.Controls;
|
2025-12-19 09:30:16 +00:00
|
|
|
using SkiaSharp;
|
|
|
|
|
|
|
|
|
|
namespace Microsoft.Maui.Platform;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-12-21 13:26:56 -05:00
|
|
|
/// Skia-rendered checkbox control with full XAML styling support.
|
2025-12-19 09:30:16 +00:00
|
|
|
/// </summary>
|
|
|
|
|
public class SkiaCheckBox : SkiaView
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
#region BindableProperties
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
public static readonly BindableProperty IsCheckedProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(IsChecked),
|
|
|
|
|
typeof(bool),
|
|
|
|
|
typeof(SkiaCheckBox),
|
|
|
|
|
false,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.OneWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).OnIsCheckedChanged());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty CheckColorProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(CheckColor),
|
|
|
|
|
typeof(SKColor),
|
|
|
|
|
typeof(SkiaCheckBox),
|
|
|
|
|
SKColors.White,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty BoxColorProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(BoxColor),
|
|
|
|
|
typeof(SKColor),
|
|
|
|
|
typeof(SkiaCheckBox),
|
2026-01-01 13:51:12 -05:00
|
|
|
new SKColor(33, 150, 243),
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty UncheckedBoxColorProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(UncheckedBoxColor),
|
|
|
|
|
typeof(SKColor),
|
|
|
|
|
typeof(SkiaCheckBox),
|
|
|
|
|
SKColors.White,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty BorderColorProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(BorderColor),
|
|
|
|
|
typeof(SKColor),
|
|
|
|
|
typeof(SkiaCheckBox),
|
2026-01-01 13:51:12 -05:00
|
|
|
new SKColor(117, 117, 117),
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty DisabledColorProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(DisabledColor),
|
|
|
|
|
typeof(SKColor),
|
|
|
|
|
typeof(SkiaCheckBox),
|
2026-01-01 13:51:12 -05:00
|
|
|
new SKColor(189, 189, 189),
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty HoveredBorderColorProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(HoveredBorderColor),
|
|
|
|
|
typeof(SKColor),
|
|
|
|
|
typeof(SkiaCheckBox),
|
2026-01-01 13:51:12 -05:00
|
|
|
new SKColor(33, 150, 243),
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty BoxSizeProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(BoxSize),
|
|
|
|
|
typeof(float),
|
|
|
|
|
typeof(SkiaCheckBox),
|
|
|
|
|
20f,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).InvalidateMeasure());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty CornerRadiusProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(CornerRadius),
|
|
|
|
|
typeof(float),
|
|
|
|
|
typeof(SkiaCheckBox),
|
|
|
|
|
3f,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty BorderWidthProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(BorderWidth),
|
|
|
|
|
typeof(float),
|
|
|
|
|
typeof(SkiaCheckBox),
|
|
|
|
|
2f,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty CheckStrokeWidthProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(CheckStrokeWidth),
|
|
|
|
|
typeof(float),
|
|
|
|
|
typeof(SkiaCheckBox),
|
|
|
|
|
2.5f,
|
2026-01-01 13:51:12 -05:00
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Properties
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public bool IsChecked
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
get => (bool)GetValue(IsCheckedProperty);
|
|
|
|
|
set => SetValue(IsCheckedProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
public SKColor CheckColor
|
|
|
|
|
{
|
|
|
|
|
get => (SKColor)GetValue(CheckColorProperty);
|
|
|
|
|
set => SetValue(CheckColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public SKColor BoxColor
|
|
|
|
|
{
|
|
|
|
|
get => (SKColor)GetValue(BoxColorProperty);
|
|
|
|
|
set => SetValue(BoxColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public SKColor UncheckedBoxColor
|
|
|
|
|
{
|
|
|
|
|
get => (SKColor)GetValue(UncheckedBoxColorProperty);
|
|
|
|
|
set => SetValue(UncheckedBoxColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public SKColor BorderColor
|
|
|
|
|
{
|
|
|
|
|
get => (SKColor)GetValue(BorderColorProperty);
|
|
|
|
|
set => SetValue(BorderColorProperty, value);
|
|
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
public SKColor DisabledColor
|
|
|
|
|
{
|
|
|
|
|
get => (SKColor)GetValue(DisabledColorProperty);
|
|
|
|
|
set => SetValue(DisabledColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public SKColor HoveredBorderColor
|
|
|
|
|
{
|
|
|
|
|
get => (SKColor)GetValue(HoveredBorderColorProperty);
|
|
|
|
|
set => SetValue(HoveredBorderColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public float BoxSize
|
|
|
|
|
{
|
|
|
|
|
get => (float)GetValue(BoxSizeProperty);
|
|
|
|
|
set => SetValue(BoxSizeProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public float CornerRadius
|
|
|
|
|
{
|
|
|
|
|
get => (float)GetValue(CornerRadiusProperty);
|
|
|
|
|
set => SetValue(CornerRadiusProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public float BorderWidth
|
|
|
|
|
{
|
|
|
|
|
get => (float)GetValue(BorderWidthProperty);
|
|
|
|
|
set => SetValue(BorderWidthProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public float CheckStrokeWidth
|
|
|
|
|
{
|
|
|
|
|
get => (float)GetValue(CheckStrokeWidthProperty);
|
|
|
|
|
set => SetValue(CheckStrokeWidthProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public bool IsHovered { get; private set; }
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
#endregion
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public event EventHandler<CheckedChangedEventArgs>? CheckedChanged;
|
|
|
|
|
|
|
|
|
|
public SkiaCheckBox()
|
|
|
|
|
{
|
|
|
|
|
IsFocusable = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
private void OnIsCheckedChanged()
|
|
|
|
|
{
|
|
|
|
|
CheckedChanged?.Invoke(this, new CheckedChangedEventArgs(IsChecked));
|
2026-01-01 13:51:12 -05:00
|
|
|
SkiaVisualStateManager.GoToState(this, IsChecked ? "Checked" : "Unchecked");
|
2025-12-21 13:26:56 -05:00
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
|
|
|
|
// Center the checkbox box in bounds
|
|
|
|
|
var boxRect = new SKRect(
|
2026-01-01 13:51:12 -05:00
|
|
|
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);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
var roundRect = new SKRoundRect(boxRect, CornerRadius);
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
// Debug logging when checked
|
|
|
|
|
if (IsChecked)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"[SkiaCheckBox] OnDraw CHECKED - BoxColor=({BoxColor.Red},{BoxColor.Green},{BoxColor.Blue}), UncheckedBoxColor=({UncheckedBoxColor.Red},{UncheckedBoxColor.Green},{UncheckedBoxColor.Blue})");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
// Draw background
|
|
|
|
|
using var bgPaint = new SKPaint
|
|
|
|
|
{
|
|
|
|
|
Color = !IsEnabled ? DisabledColor
|
|
|
|
|
: IsChecked ? BoxColor
|
|
|
|
|
: UncheckedBoxColor,
|
|
|
|
|
IsAntialias = true,
|
|
|
|
|
Style = SKPaintStyle.Fill
|
|
|
|
|
};
|
|
|
|
|
canvas.DrawRoundRect(roundRect, bgPaint);
|
|
|
|
|
|
|
|
|
|
// Draw border
|
|
|
|
|
using var borderPaint = new SKPaint
|
|
|
|
|
{
|
|
|
|
|
Color = !IsEnabled ? DisabledColor
|
|
|
|
|
: IsChecked ? BoxColor
|
|
|
|
|
: IsHovered ? HoveredBorderColor
|
|
|
|
|
: BorderColor,
|
|
|
|
|
IsAntialias = true,
|
|
|
|
|
Style = SKPaintStyle.Stroke,
|
|
|
|
|
StrokeWidth = BorderWidth
|
|
|
|
|
};
|
|
|
|
|
canvas.DrawRoundRect(roundRect, borderPaint);
|
|
|
|
|
|
|
|
|
|
// Draw focus ring
|
|
|
|
|
if (IsFocused)
|
|
|
|
|
{
|
|
|
|
|
using var focusPaint = new SKPaint
|
|
|
|
|
{
|
|
|
|
|
Color = BoxColor.WithAlpha(80),
|
|
|
|
|
IsAntialias = true,
|
|
|
|
|
Style = SKPaintStyle.Stroke,
|
2026-01-01 13:51:12 -05:00
|
|
|
StrokeWidth = 3f
|
2025-12-19 09:30:16 +00:00
|
|
|
};
|
|
|
|
|
var focusRect = new SKRoundRect(boxRect, CornerRadius);
|
2026-01-01 13:51:12 -05:00
|
|
|
focusRect.Inflate(4f, 4f);
|
2025-12-19 09:30:16 +00:00
|
|
|
canvas.DrawRoundRect(focusRect, focusPaint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw checkmark
|
|
|
|
|
if (IsChecked)
|
|
|
|
|
{
|
|
|
|
|
DrawCheckmark(canvas, boxRect);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DrawCheckmark(SKCanvas canvas, SKRect boxRect)
|
|
|
|
|
{
|
|
|
|
|
using var paint = new SKPaint
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
Color = SKColors.White,
|
2025-12-19 09:30:16 +00:00
|
|
|
IsAntialias = true,
|
|
|
|
|
Style = SKPaintStyle.Stroke,
|
|
|
|
|
StrokeWidth = CheckStrokeWidth,
|
|
|
|
|
StrokeCap = SKStrokeCap.Round,
|
|
|
|
|
StrokeJoin = SKStrokeJoin.Round
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
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;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnPointerEntered(PointerEventArgs e)
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
if (IsEnabled)
|
|
|
|
|
{
|
|
|
|
|
IsHovered = true;
|
|
|
|
|
SkiaVisualStateManager.GoToState(this, "PointerOver");
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnPointerExited(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
IsHovered = false;
|
2026-01-01 13:51:12 -05:00
|
|
|
SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled");
|
2025-12-19 09:30:16 +00:00
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnPointerPressed(PointerEventArgs e)
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
if (IsEnabled)
|
|
|
|
|
{
|
|
|
|
|
IsChecked = !IsChecked;
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnPointerReleased(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnKeyDown(KeyEventArgs e)
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
if (IsEnabled && e.Key == Key.Space)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
|
|
|
|
IsChecked = !IsChecked;
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
protected override void OnEnabledChanged()
|
|
|
|
|
{
|
|
|
|
|
base.OnEnabledChanged();
|
2026-01-01 13:51:12 -05:00
|
|
|
SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled");
|
2025-12-21 13:26:56 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
protected override SKSize MeasureOverride(SKSize availableSize)
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
return new SKSize(BoxSize + 8f, BoxSize + 8f);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
}
|