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-16 04:56:23 +00:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using Microsoft.Maui.Controls;
|
|
|
|
|
using Microsoft.Maui.Graphics;
|
2025-12-19 09:30:16 +00:00
|
|
|
using SkiaSharp;
|
|
|
|
|
|
|
|
|
|
namespace Microsoft.Maui.Platform;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-01-16 04:56:23 +00:00
|
|
|
/// Skia-rendered radio button control with full MAUI compliance.
|
|
|
|
|
/// Implements IRadioButton interface requirements:
|
|
|
|
|
/// - IsChecked property with CheckedChanged event
|
|
|
|
|
/// - GroupName for mutual exclusion
|
|
|
|
|
/// - Value property for binding
|
|
|
|
|
/// - Content property for label text
|
2025-12-19 09:30:16 +00:00
|
|
|
/// </summary>
|
|
|
|
|
public class SkiaRadioButton : SkiaView
|
|
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
#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
|
|
|
|
|
|
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 =
|
2026-01-16 04:56:23 +00:00
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(IsChecked),
|
|
|
|
|
typeof(bool),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
false,
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).OnIsCheckedChanged());
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
public static readonly BindableProperty ContentProperty =
|
2026-01-16 04:56:23 +00:00
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(Content),
|
|
|
|
|
typeof(string),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
"",
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure());
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
public static readonly BindableProperty ValueProperty =
|
2026-01-16 04:56:23 +00:00
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(Value),
|
|
|
|
|
typeof(object),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
null,
|
|
|
|
|
BindingMode.TwoWay);
|
2025-12-21 13:26:56 -05:00
|
|
|
|
|
|
|
|
public static readonly BindableProperty GroupNameProperty =
|
2026-01-16 04:56:23 +00:00
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(GroupName),
|
|
|
|
|
typeof(string),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
null,
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).OnGroupNameChanged((string?)o, (string?)n));
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty RadioColorProperty =
|
2026-01-16 04:56:23 +00:00
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(RadioColor),
|
|
|
|
|
typeof(Color),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
Color.FromRgb(0x21, 0x96, 0xF3), // Material Blue
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty UncheckedColorProperty =
|
2026-01-16 04:56:23 +00:00
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(UncheckedColor),
|
|
|
|
|
typeof(Color),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
Color.FromRgb(0x75, 0x75, 0x75),
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty TextColorProperty =
|
2026-01-16 04:56:23 +00:00
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(TextColor),
|
|
|
|
|
typeof(Color),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
Colors.Black,
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty DisabledColorProperty =
|
2026-01-16 04:56:23 +00:00
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(DisabledColor),
|
|
|
|
|
typeof(Color),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
Color.FromRgb(0xBD, 0xBD, 0xBD),
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty FontSizeProperty =
|
2026-01-16 04:56:23 +00:00
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(FontSize),
|
|
|
|
|
typeof(double),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
14.0,
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty RadioSizeProperty =
|
2026-01-16 04:56:23 +00:00
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(RadioSize),
|
|
|
|
|
typeof(double),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
20.0,
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty SpacingProperty =
|
2026-01-16 04:56:23 +00:00
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(Spacing),
|
|
|
|
|
typeof(double),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
8.0,
|
|
|
|
|
BindingMode.TwoWay,
|
2025-12-21 13:26:56 -05:00
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure());
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
public static readonly BindableProperty BorderColorProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(BorderColor),
|
|
|
|
|
typeof(Color),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
Color.FromRgb(0x75, 0x75, 0x75),
|
|
|
|
|
BindingMode.TwoWay,
|
|
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate());
|
|
|
|
|
|
|
|
|
|
public static readonly BindableProperty BorderWidthProperty =
|
|
|
|
|
BindableProperty.Create(
|
|
|
|
|
nameof(BorderWidth),
|
|
|
|
|
typeof(double),
|
|
|
|
|
typeof(SkiaRadioButton),
|
|
|
|
|
2.0,
|
|
|
|
|
BindingMode.TwoWay,
|
|
|
|
|
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate());
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Properties
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets whether the radio button is checked.
|
|
|
|
|
/// </summary>
|
2025-12-21 13:26:56 -05:00
|
|
|
public bool IsChecked
|
|
|
|
|
{
|
|
|
|
|
get => (bool)GetValue(IsCheckedProperty);
|
|
|
|
|
set => SetValue(IsCheckedProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the text content displayed next to the radio button.
|
|
|
|
|
/// </summary>
|
2025-12-19 09:30:16 +00:00
|
|
|
public string Content
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
get => (string)GetValue(ContentProperty);
|
|
|
|
|
set => SetValue(ContentProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the value associated with this radio button.
|
|
|
|
|
/// </summary>
|
2025-12-19 09:30:16 +00:00
|
|
|
public object? Value
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
get => GetValue(ValueProperty);
|
|
|
|
|
set => SetValue(ValueProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the group name for mutual exclusion.
|
|
|
|
|
/// </summary>
|
2025-12-19 09:30:16 +00:00
|
|
|
public string? GroupName
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
get => (string?)GetValue(GroupNameProperty);
|
|
|
|
|
set => SetValue(GroupNameProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the color of the radio circle when checked.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Color RadioColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
get => (Color)GetValue(RadioColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(RadioColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the color of the radio circle when unchecked.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Color UncheckedColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
get => (Color)GetValue(UncheckedColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(UncheckedColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the text color.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Color TextColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
get => (Color)GetValue(TextColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(TextColorProperty, value);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the color used when disabled.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Color DisabledColor
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
get => (Color)GetValue(DisabledColorProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(DisabledColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the font size for the content text.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public double FontSize
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
get => (double)GetValue(FontSizeProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(FontSizeProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the size of the radio circle.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public double RadioSize
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
get => (double)GetValue(RadioSizeProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(RadioSizeProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the spacing between the radio circle and content.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public double Spacing
|
2025-12-21 13:26:56 -05:00
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
get => (double)GetValue(SpacingProperty);
|
2025-12-21 13:26:56 -05:00
|
|
|
set => SetValue(SpacingProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the border color.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Color BorderColor
|
|
|
|
|
{
|
|
|
|
|
get => (Color)GetValue(BorderColorProperty);
|
|
|
|
|
set => SetValue(BorderColorProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the border width.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public double BorderWidth
|
|
|
|
|
{
|
|
|
|
|
get => (double)GetValue(BorderWidthProperty);
|
|
|
|
|
set => SetValue(BorderWidthProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets whether the control is currently hovered.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool IsHovered { get; private set; }
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
#endregion
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
#region Group Management
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
private static readonly Dictionary<string, List<WeakReference<SkiaRadioButton>>> _groups = new();
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Events
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Occurs when IsChecked changes.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public event EventHandler<CheckedChangedEventArgs>? CheckedChanged;
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Constructor
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
public SkiaRadioButton()
|
|
|
|
|
{
|
|
|
|
|
IsFocusable = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Event Handlers
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
private void OnIsCheckedChanged()
|
|
|
|
|
{
|
|
|
|
|
if (IsChecked && !string.IsNullOrEmpty(GroupName))
|
|
|
|
|
{
|
|
|
|
|
UncheckOthersInGroup();
|
|
|
|
|
}
|
2026-01-16 04:56:23 +00:00
|
|
|
CheckedChanged?.Invoke(this, new CheckedChangedEventArgs(IsChecked));
|
|
|
|
|
SkiaVisualStateManager.GoToState(this, IsChecked
|
|
|
|
|
? SkiaVisualStateManager.CommonStates.Checked
|
|
|
|
|
: SkiaVisualStateManager.CommonStates.Unchecked);
|
2025-12-21 13:26:56 -05:00
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnGroupNameChanged(string? oldValue, string? newValue)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
RemoveFromGroup(oldValue);
|
|
|
|
|
AddToGroup(newValue);
|
|
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Group Management Methods
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
private void AddToGroup(string? groupName)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(groupName)) return;
|
|
|
|
|
|
|
|
|
|
if (!_groups.TryGetValue(groupName, out var group))
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
|
|
|
|
group = new List<WeakReference<SkiaRadioButton>>();
|
2025-12-21 13:26:56 -05:00
|
|
|
_groups[groupName] = group;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
// Clean up dead references
|
2025-12-19 09:30:16 +00:00
|
|
|
group.RemoveAll(wr => !wr.TryGetTarget(out _));
|
|
|
|
|
group.Add(new WeakReference<SkiaRadioButton>(this));
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
private void RemoveFromGroup(string? groupName)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
if (string.IsNullOrEmpty(groupName)) return;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
if (_groups.TryGetValue(groupName, out var group))
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
|
|
|
|
group.RemoveAll(wr => !wr.TryGetTarget(out var target) || target == this);
|
2025-12-21 13:26:56 -05:00
|
|
|
if (group.Count == 0) _groups.Remove(groupName);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UncheckOthersInGroup()
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
if (string.IsNullOrEmpty(GroupName)) return;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
if (_groups.TryGetValue(GroupName, out var group))
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
|
|
|
|
foreach (var weakRef in group)
|
|
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
if (weakRef.TryGetTarget(out var radioButton) && radioButton != this && radioButton.IsChecked)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
radioButton.SetValue(IsCheckedProperty, false);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Rendering
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
var radioSize = (float)RadioSize;
|
|
|
|
|
var fontSize = (float)FontSize;
|
|
|
|
|
var spacing = (float)Spacing;
|
|
|
|
|
var borderWidth = (float)BorderWidth;
|
|
|
|
|
|
|
|
|
|
var radioRadius = radioSize / 2;
|
2025-12-19 09:30:16 +00:00
|
|
|
var radioCenterX = bounds.Left + radioRadius;
|
|
|
|
|
var radioCenterY = bounds.MidY;
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
// Get colors
|
|
|
|
|
var radioColorSK = ToSKColor(RadioColor);
|
|
|
|
|
var uncheckedColorSK = ToSKColor(UncheckedColor);
|
|
|
|
|
var textColorSK = ToSKColor(TextColor);
|
|
|
|
|
var disabledColorSK = ToSKColor(DisabledColor);
|
|
|
|
|
var borderColorSK = ToSKColor(BorderColor);
|
|
|
|
|
|
|
|
|
|
// Draw focus ring behind radio circle
|
|
|
|
|
if (IsFocused)
|
|
|
|
|
{
|
|
|
|
|
using var focusPaint = new SKPaint
|
|
|
|
|
{
|
|
|
|
|
Color = radioColorSK.WithAlpha(80),
|
|
|
|
|
Style = SKPaintStyle.Fill,
|
|
|
|
|
IsAntialias = true
|
|
|
|
|
};
|
|
|
|
|
canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius + 4, focusPaint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw outer circle (border)
|
2025-12-19 09:30:16 +00:00
|
|
|
using var outerPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
Color = IsEnabled
|
|
|
|
|
? (IsChecked ? radioColorSK : (IsHovered ? radioColorSK : uncheckedColorSK))
|
|
|
|
|
: disabledColorSK,
|
2025-12-19 09:30:16 +00:00
|
|
|
Style = SKPaintStyle.Stroke,
|
2026-01-16 04:56:23 +00:00
|
|
|
StrokeWidth = borderWidth,
|
2025-12-19 09:30:16 +00:00
|
|
|
IsAntialias = true
|
|
|
|
|
};
|
|
|
|
|
canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius - 1, outerPaint);
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
// Draw inner filled circle when checked
|
2025-12-21 13:26:56 -05:00
|
|
|
if (IsChecked)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
|
|
|
|
using var innerPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
Color = IsEnabled ? radioColorSK : disabledColorSK,
|
2025-12-19 09:30:16 +00:00
|
|
|
Style = SKPaintStyle.Fill,
|
|
|
|
|
IsAntialias = true
|
|
|
|
|
};
|
|
|
|
|
canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius - 5, innerPaint);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
// Draw content text
|
2025-12-21 13:26:56 -05:00
|
|
|
if (!string.IsNullOrEmpty(Content))
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
using var font = new SKFont(SKTypeface.Default, fontSize);
|
2025-12-19 09:30:16 +00:00
|
|
|
using var textPaint = new SKPaint(font)
|
|
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
Color = IsEnabled ? textColorSK : disabledColorSK,
|
2025-12-19 09:30:16 +00:00
|
|
|
IsAntialias = true
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
var textX = bounds.Left + radioSize + spacing;
|
2025-12-19 09:30:16 +00:00
|
|
|
var textBounds = new SKRect();
|
2025-12-21 13:26:56 -05:00
|
|
|
textPaint.MeasureText(Content, ref textBounds);
|
|
|
|
|
canvas.DrawText(Content, textX, bounds.MidY - textBounds.MidY, textPaint);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Pointer Events
|
|
|
|
|
|
|
|
|
|
public override void OnPointerEntered(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (IsEnabled)
|
|
|
|
|
{
|
|
|
|
|
IsHovered = true;
|
|
|
|
|
SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver);
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnPointerExited(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
IsHovered = false;
|
|
|
|
|
SkiaVisualStateManager.GoToState(this, IsEnabled
|
|
|
|
|
? SkiaVisualStateManager.CommonStates.Normal
|
|
|
|
|
: SkiaVisualStateManager.CommonStates.Disabled);
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public override void OnPointerPressed(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (!IsEnabled) return;
|
2025-12-21 13:26:56 -05:00
|
|
|
if (!IsChecked) IsChecked = true;
|
2026-01-16 04:56:23 +00:00
|
|
|
e.Handled = true;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
public override void OnPointerReleased(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
// No action needed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Keyboard Events
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public override void OnKeyDown(KeyEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (!IsEnabled) return;
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
if (e.Key == Key.Space || e.Key == Key.Enter)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2025-12-21 13:26:56 -05:00
|
|
|
if (!IsChecked) IsChecked = true;
|
|
|
|
|
e.Handled = true;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Lifecycle
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
protected override void OnEnabledChanged()
|
|
|
|
|
{
|
|
|
|
|
base.OnEnabledChanged();
|
2026-01-16 04:56:23 +00:00
|
|
|
SkiaVisualStateManager.GoToState(this, IsEnabled
|
|
|
|
|
? SkiaVisualStateManager.CommonStates.Normal
|
|
|
|
|
: SkiaVisualStateManager.CommonStates.Disabled);
|
2025-12-21 13:26:56 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:56:23 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Layout
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
protected override SKSize MeasureOverride(SKSize availableSize)
|
|
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
var radioSize = (float)RadioSize;
|
|
|
|
|
var fontSize = (float)FontSize;
|
|
|
|
|
var spacing = (float)Spacing;
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
var textWidth = 0f;
|
2025-12-21 13:26:56 -05:00
|
|
|
if (!string.IsNullOrEmpty(Content))
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-16 04:56:23 +00:00
|
|
|
using var font = new SKFont(SKTypeface.Default, fontSize);
|
2025-12-19 09:30:16 +00:00
|
|
|
using var paint = new SKPaint(font);
|
2026-01-16 04:56:23 +00:00
|
|
|
textWidth = paint.MeasureText(Content) + spacing;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
2026-01-16 04:56:23 +00:00
|
|
|
return new SKSize(radioSize + textWidth, Math.Max(radioSize, fontSize * 1.5f));
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
2026-01-16 04:56:23 +00:00
|
|
|
|
|
|
|
|
#endregion
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|