File: Platform\iOS\MauiCheckBox.cs
Web Access
Project: src\src\Core\src\Core.csproj (Microsoft.Maui)
using System;
using System.Diagnostics.CodeAnalysis;
using CoreGraphics;
using Microsoft.Maui.Graphics;
using ObjCRuntime;
using UIKit;
 
namespace Microsoft.Maui.Platform
{
	public class MauiCheckBox : UIButton, IUIViewLifeCycleEvents
	{
		// All these values were chosen to just match the android drawables that are used
		const float DefaultSize = 18.0f;
		const float LineWidth = 2.0f;
 
		static UIImage? Checked;
		static UIImage? Unchecked;
		UIImage? CheckedDisabledAndTinted;
		UIImage? UncheckedDisabledAndTinted;
 
		UIAccessibilityTrait _accessibilityTraits;
 
		Color? _tintColor;
		bool _isChecked;
		bool _isEnabled;
		bool _disposed;
 
		readonly WeakEventManager _weakEventManager = new WeakEventManager();
 
		public event EventHandler? CheckedChanged
		{
			add => _weakEventManager.AddEventHandler(value);
			remove => _weakEventManager.RemoveEventHandler(value);
		}
 
		public MauiCheckBox()
		{
			ContentMode = UIViewContentMode.Center;
			ImageView.ContentMode = UIViewContentMode.ScaleAspectFit;
			HorizontalAlignment = UIControlContentHorizontalAlignment.Center;
			VerticalAlignment = UIControlContentVerticalAlignment.Center;
#pragma warning disable CA1416 // TODO: both has [UnsupportedOSPlatform("ios15.0")]
#pragma warning disable CA1422 // Validate platform compatibility
			AdjustsImageWhenDisabled = false;
			AdjustsImageWhenHighlighted = false;
#pragma warning restore CA1422 // Validate platform compatibility
#pragma warning restore CA1416
			TouchUpInside += OnTouchUpInside;
		}
 
		void OnTouchUpInside(object? sender, EventArgs e)
		{
			IsChecked = !IsChecked;
			_weakEventManager.HandleEvent(this, e, nameof(CheckedChanged));
		}
 
		internal float MinimumViewSize { get; set; }
 
		public bool IsChecked
		{
			get => _isChecked;
			set
			{
				if (value == _isChecked)
					return;
 
				_isChecked = value;
				UpdateDisplay();
			}
		}
 
		public bool IsEnabled
		{
			get => _isEnabled;
			set
			{
				if (value == _isEnabled)
					return;
 
				_isEnabled = value;
				UserInteractionEnabled = IsEnabled;
				UpdateDisplay();
			}
		}
 
		public Color? CheckBoxTintColor
		{
			get => _tintColor;
			set
			{
				if (_tintColor == value)
					return;
 
				CheckedDisabledAndTinted = null;
				UncheckedDisabledAndTinted = null;
				_tintColor = value;
				CheckBoxTintUIColor = CheckBoxTintColor?.ToPlatform();
			}
		}
 
		UIColor? _checkBoxTintUIColor;
		UIColor? CheckBoxTintUIColor
		{
			get
			{
				return _checkBoxTintUIColor ?? UIColor.White;
			}
			set
			{
				if (value == _checkBoxTintUIColor)
					return;
 
				_checkBoxTintUIColor = value;
				ImageView.TintColor = value;
				TintColor = value;
 
				if (Enabled)
					SetNeedsDisplay();
				else
					UpdateDisplay();
			}
		}
 
		public override bool Enabled
		{
			get
			{
				return base.Enabled;
			}
 
			set
			{
				bool changed = base.Enabled != value;
				base.Enabled = value;
 
				if (changed)
					UpdateDisplay();
			}
		}
 
		protected virtual UIImage GetCheckBoxImage()
		{
			// Ideally I would use the static images here but when disabled it always tints them grey
			// and I don't know how to make it not tint them gray
			if (!Enabled && CheckBoxTintColor != null)
			{
				if (IsChecked)
				{
					return CheckedDisabledAndTinted ??=
						CreateCheckBox(CreateCheckMark()).ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
				}
 
				return UncheckedDisabledAndTinted ??=
					CreateCheckBox(null).ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
			}
 
			Checked ??= CreateCheckBox(CreateCheckMark()).ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate);
			Unchecked ??= CreateCheckBox(null).ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate);
 
			return IsChecked ? Checked : Unchecked;
		}
 
		static UIBezierPath CreateBoxPath(CGRect backgroundRect) => UIBezierPath.FromOval(backgroundRect);
		static UIBezierPath CreateCheckPath() => new UIBezierPath
		{
			LineWidth = (nfloat)0.077,
			LineCapStyle = CGLineCap.Round,
			LineJoinStyle = CGLineJoin.Round
		};
 
		static void DrawCheckMark(UIBezierPath path)
		{
			path.MoveTo(new CGPoint(0.72f, 0.22f));
			path.AddLineTo(new CGPoint(0.33f, 0.6f));
			path.AddLineTo(new CGPoint(0.15f, 0.42f));
		}
 
		UIImage CreateCheckBox(UIImage? check)
		{
			var renderer = new UIGraphicsImageRenderer(new CGSize(DefaultSize, DefaultSize));
			var image = renderer.CreateImage((UIGraphicsImageRendererContext ctx) =>
			{
				var context = ctx.CGContext;
				RenderCheckMark(context, check);
			});
			return image;
		}
 
		void RenderCheckMark(CGContext context, UIImage? check)
		{
			var checkedColor = CheckBoxTintUIColor;
 
			if (checkedColor != null)
			{
				checkedColor.SetFill();
				checkedColor.SetStroke();
			}
 
			var vPadding = LineWidth / 2;
			var hPadding = LineWidth / 2;
			var diameter = DefaultSize - LineWidth;
 
			var backgroundRect = new CGRect(hPadding, vPadding, diameter, diameter);
			var boxPath = CreateBoxPath(backgroundRect);
			boxPath.LineWidth = LineWidth;
			boxPath.Stroke();
 
			if (check != null)
			{
				boxPath.Fill();
				check.Draw(new CGPoint(0, 0), CGBlendMode.DestinationOut, 1);
			}
		}
 
		static UIImage CreateCheckMark()
		{
			using var renderer = new UIGraphicsImageRenderer(new CGSize(DefaultSize, DefaultSize));
			var image = renderer.CreateImage((UIGraphicsImageRendererContext ctx) =>
			{
				var context = ctx.CGContext;
				RenderCheckMark(context);
			});
			return image;
		}
 
		static void RenderCheckMark(CGContext context)
		{
			context.SaveState();
 
			var vPadding = LineWidth / 2;
			var hPadding = LineWidth / 2;
			var diameter = DefaultSize - LineWidth;
 
			var checkPath = CreateCheckPath();
 
			context.TranslateCTM(hPadding + (nfloat)(0.05 * diameter), vPadding + (nfloat)(0.1 * diameter));
			context.ScaleCTM(diameter, diameter);
			DrawCheckMark(checkPath);
			UIColor.White.SetStroke();
			checkPath.Stroke();
 
			context.RestoreState();
		}
 
		public override CGSize SizeThatFits(CGSize size)
		{
			var result = base.SizeThatFits(size);
			var height = Math.Max(MinimumViewSize, result.Height);
			var width = Math.Max(MinimumViewSize, result.Width);
			var final = Math.Min(width, height);
 
			return new CGSize(final, final);
		}
 
		public override void LayoutSubviews()
		{
			base.LayoutSubviews();
			UpdateDisplay();
		}
 
		protected override void Dispose(bool disposing)
		{
			if (_disposed)
				return;
 
			_disposed = true;
			if (disposing)
				TouchUpInside -= OnTouchUpInside;
 
			base.Dispose(disposing);
		}
 
		void UpdateDisplay()
		{
			SetImage(GetCheckBoxImage(), UIControlState.Normal);
			SetNeedsDisplay();
		}
 
		static UIKit.UIAccessibilityTrait? s_switchAccessibilityTraits;
		UIKit.UIAccessibilityTrait SwitchAccessibilityTraits
		{
			get
			{
				// Accessibility Traits are none if VO is off
				// So we return None until we detect that it's been turned on
				if (base.AccessibilityTraits == UIAccessibilityTrait.None)
					return UIAccessibilityTrait.None;
 
				if (s_switchAccessibilityTraits == null ||
					s_switchAccessibilityTraits == UIKit.UIAccessibilityTrait.None)
				{
					s_switchAccessibilityTraits = new UIKit.UISwitch().AccessibilityTraits;
				}
 
				return s_switchAccessibilityTraits ?? UIKit.UIAccessibilityTrait.None;
			}
		}
 
		public override UIAccessibilityTrait AccessibilityTraits
		{
			get => _accessibilityTraits |= SwitchAccessibilityTraits;
			set => _accessibilityTraits = value | SwitchAccessibilityTraits;
		}
 
		public override string? AccessibilityValue
		{
			get => (IsChecked) ? "1" : "0";
			set { }
		}
 
		[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = IUIViewLifeCycleEvents.UnconditionalSuppressMessage)]
		EventHandler? _movedToWindow;
		event EventHandler IUIViewLifeCycleEvents.MovedToWindow
		{
			add => _movedToWindow += value;
			remove => _movedToWindow -= value;
		}
 
		public override void MovedToWindow()
		{
			base.MovedToWindow();
			_movedToWindow?.Invoke(this, EventArgs.Empty);
		}
	}
}