// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
namespace Microsoft.Maui.Platform;
///
/// Skia-rendered radio button control with full XAML styling support.
///
public class SkiaRadioButton : SkiaView
{
#region BindableProperties
public static readonly BindableProperty IsCheckedProperty =
BindableProperty.Create(nameof(IsChecked), typeof(bool), typeof(SkiaRadioButton), false, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).OnIsCheckedChanged());
public static readonly BindableProperty ContentProperty =
BindableProperty.Create(nameof(Content), typeof(string), typeof(SkiaRadioButton), "",
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure());
public static readonly BindableProperty ValueProperty =
BindableProperty.Create(nameof(Value), typeof(object), typeof(SkiaRadioButton), null);
public static readonly BindableProperty GroupNameProperty =
BindableProperty.Create(nameof(GroupName), typeof(string), typeof(SkiaRadioButton), null,
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).OnGroupNameChanged((string?)o, (string?)n));
public static readonly BindableProperty RadioColorProperty =
BindableProperty.Create(nameof(RadioColor), typeof(SKColor), typeof(SkiaRadioButton), new SKColor(0x21, 0x96, 0xF3),
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate());
public static readonly BindableProperty UncheckedColorProperty =
BindableProperty.Create(nameof(UncheckedColor), typeof(SKColor), typeof(SkiaRadioButton), new SKColor(0x75, 0x75, 0x75),
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate());
public static readonly BindableProperty TextColorProperty =
BindableProperty.Create(nameof(TextColor), typeof(SKColor), typeof(SkiaRadioButton), SKColors.Black,
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate());
public static readonly BindableProperty DisabledColorProperty =
BindableProperty.Create(nameof(DisabledColor), typeof(SKColor), typeof(SkiaRadioButton), new SKColor(0xBD, 0xBD, 0xBD),
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate());
public static readonly BindableProperty FontSizeProperty =
BindableProperty.Create(nameof(FontSize), typeof(float), typeof(SkiaRadioButton), 14f,
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure());
public static readonly BindableProperty RadioSizeProperty =
BindableProperty.Create(nameof(RadioSize), typeof(float), typeof(SkiaRadioButton), 20f,
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure());
public static readonly BindableProperty SpacingProperty =
BindableProperty.Create(nameof(Spacing), typeof(float), typeof(SkiaRadioButton), 8f,
propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure());
#endregion
#region Properties
public bool IsChecked
{
get => (bool)GetValue(IsCheckedProperty);
set => SetValue(IsCheckedProperty, value);
}
public string Content
{
get => (string)GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
public object? Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public string? GroupName
{
get => (string?)GetValue(GroupNameProperty);
set => SetValue(GroupNameProperty, value);
}
public SKColor RadioColor
{
get => (SKColor)GetValue(RadioColorProperty);
set => SetValue(RadioColorProperty, value);
}
public SKColor UncheckedColor
{
get => (SKColor)GetValue(UncheckedColorProperty);
set => SetValue(UncheckedColorProperty, value);
}
public SKColor TextColor
{
get => (SKColor)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value);
}
public SKColor DisabledColor
{
get => (SKColor)GetValue(DisabledColorProperty);
set => SetValue(DisabledColorProperty, value);
}
public float FontSize
{
get => (float)GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value);
}
public float RadioSize
{
get => (float)GetValue(RadioSizeProperty);
set => SetValue(RadioSizeProperty, value);
}
public float Spacing
{
get => (float)GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
#endregion
private static readonly Dictionary>> _groups = new();
public event EventHandler? CheckedChanged;
public SkiaRadioButton()
{
IsFocusable = true;
}
private void OnIsCheckedChanged()
{
if (IsChecked && !string.IsNullOrEmpty(GroupName))
{
UncheckOthersInGroup();
}
CheckedChanged?.Invoke(this, EventArgs.Empty);
SkiaVisualStateManager.GoToState(this, IsChecked ? SkiaVisualStateManager.CommonStates.Checked : SkiaVisualStateManager.CommonStates.Unchecked);
Invalidate();
}
private void OnGroupNameChanged(string? oldValue, string? newValue)
{
RemoveFromGroup(oldValue);
AddToGroup(newValue);
}
private void AddToGroup(string? groupName)
{
if (string.IsNullOrEmpty(groupName)) return;
if (!_groups.TryGetValue(groupName, out var group))
{
group = new List>();
_groups[groupName] = group;
}
group.RemoveAll(wr => !wr.TryGetTarget(out _));
group.Add(new WeakReference(this));
}
private void RemoveFromGroup(string? groupName)
{
if (string.IsNullOrEmpty(groupName)) return;
if (_groups.TryGetValue(groupName, out var group))
{
group.RemoveAll(wr => !wr.TryGetTarget(out var target) || target == this);
if (group.Count == 0) _groups.Remove(groupName);
}
}
private void UncheckOthersInGroup()
{
if (string.IsNullOrEmpty(GroupName)) return;
if (_groups.TryGetValue(GroupName, out var group))
{
foreach (var weakRef in group)
{
if (weakRef.TryGetTarget(out var radioButton) && radioButton != this && radioButton.IsChecked)
{
radioButton.SetValue(IsCheckedProperty, false);
}
}
}
}
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
var radioRadius = RadioSize / 2;
var radioCenterX = bounds.Left + radioRadius;
var radioCenterY = bounds.MidY;
using var outerPaint = new SKPaint
{
Color = IsEnabled ? (IsChecked ? RadioColor : UncheckedColor) : DisabledColor,
Style = SKPaintStyle.Stroke,
StrokeWidth = 2,
IsAntialias = true
};
canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius - 1, outerPaint);
if (IsChecked)
{
using var innerPaint = new SKPaint
{
Color = IsEnabled ? RadioColor : DisabledColor,
Style = SKPaintStyle.Fill,
IsAntialias = true
};
canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius - 5, innerPaint);
}
if (IsFocused)
{
using var focusPaint = new SKPaint
{
Color = RadioColor.WithAlpha(80),
Style = SKPaintStyle.Fill,
IsAntialias = true
};
canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius + 4, focusPaint);
}
if (!string.IsNullOrEmpty(Content))
{
using var font = new SKFont(SKTypeface.Default, FontSize);
using var textPaint = new SKPaint(font)
{
Color = IsEnabled ? TextColor : DisabledColor,
IsAntialias = true
};
var textX = bounds.Left + RadioSize + Spacing;
var textBounds = new SKRect();
textPaint.MeasureText(Content, ref textBounds);
canvas.DrawText(Content, textX, bounds.MidY - textBounds.MidY, textPaint);
}
}
public override void OnPointerPressed(PointerEventArgs e)
{
if (!IsEnabled) return;
if (!IsChecked) IsChecked = true;
}
public override void OnKeyDown(KeyEventArgs e)
{
if (!IsEnabled) return;
if (e.Key == Key.Space || e.Key == Key.Enter)
{
if (!IsChecked) IsChecked = true;
e.Handled = true;
}
}
protected override void OnEnabledChanged()
{
base.OnEnabledChanged();
SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled);
}
protected override SKSize MeasureOverride(SKSize availableSize)
{
var textWidth = 0f;
if (!string.IsNullOrEmpty(Content))
{
using var font = new SKFont(SKTypeface.Default, FontSize);
using var paint = new SKPaint(font);
textWidth = paint.MeasureText(Content) + Spacing;
}
return new SKSize(RadioSize + textWidth, Math.Max(RadioSize, FontSize * 1.5f));
}
}