File: Handlers\Items\iOS\TemplatedCell.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.ComponentModel;
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using UIKit;
 
namespace Microsoft.Maui.Controls.Handlers.Items
{
	public abstract class TemplatedCell : ItemsViewCell
	{
		readonly WeakEventManager _weakEventManager = new();
 
		public event EventHandler<EventArgs> ContentSizeChanged
		{
			add => _weakEventManager.AddEventHandler(value);
			remove => _weakEventManager.RemoveEventHandler(value);
		}
 
		public event EventHandler<LayoutAttributesChangedEventArgs> LayoutAttributesChanged
		{
			add => _weakEventManager.AddEventHandler(value);
			remove => _weakEventManager.RemoveEventHandler(value);
		}
 
		protected CGSize ConstrainedSize;
 
		protected nfloat ConstrainedDimension;
 
		WeakReference<DataTemplate> _currentTemplate;
 
		public DataTemplate CurrentTemplate
		{
			get => _currentTemplate is not null && _currentTemplate.TryGetTarget(out var target) ? target : null;
			private set => _currentTemplate = value is null ? null : new(value);
		}
 
		// Keep track of the cell size so we can verify whether a measure invalidation 
		// actually changed the size of the cell
		Size _size;
 
		internal CGSize CurrentSize => _size.ToCGSize();
 
		[Export("initWithFrame:")]
		[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
		protected TemplatedCell(CGRect frame) : base(frame)
		{
		}
 
		WeakReference<IPlatformViewHandler> _handler;
 
		internal IPlatformViewHandler PlatformHandler
		{
			get => _handler is not null && _handler.TryGetTarget(out var h) ? h : null;
			set => _handler = value == null ? null : new(value);
		}
 
		public override void ConstrainTo(CGSize constraint)
		{
			ClearConstraints();
			ConstrainedSize = constraint;
		}
 
		public override void ConstrainTo(nfloat constant)
		{
			ClearConstraints();
			ConstrainedDimension = constant;
		}
 
		protected void ClearConstraints()
		{
			ConstrainedSize = default;
			ConstrainedDimension = default;
		}
 
		internal void Unbind()
		{
			if (PlatformHandler?.VirtualView is View view)
			{
				view.MeasureInvalidated -= MeasureInvalidated;
				view.BindingContext = null;
			}
		}
 
		public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittingAttributes(
			UICollectionViewLayoutAttributes layoutAttributes)
		{
			var preferredAttributes = base.PreferredLayoutAttributesFittingAttributes(layoutAttributes);
 
			var preferredSize = preferredAttributes.Frame.Size;
 
			if (preferredSize.IsCloseTo(_size)
				&& AttributesConsistentWithConstrainedDimension(preferredAttributes))
			{
				return preferredAttributes;
			}
 
			var size = UpdateCellSize();
 
			// Adjust the preferred attributes to include space for the Forms element
			preferredAttributes.Frame = new CGRect(preferredAttributes.Frame.Location, size);
 
			OnLayoutAttributesChanged(preferredAttributes);
 
			return preferredAttributes;
		}
 
		CGSize UpdateCellSize()
		{
			// Measure this cell (including the Forms element) if there is no constrained size
			var size = ConstrainedSize == default ? Measure() : ConstrainedSize;
 
			// Update the size of the root view to accommodate the Forms element
			var platformView = PlatformHandler.ToPlatform();
			platformView.Frame = new CGRect(CGPoint.Empty, size);
 
			// Layout the Maui element 
			var nativeBounds = platformView.Frame.ToRectangle();
			PlatformHandler.VirtualView.Arrange(nativeBounds);
			_size = nativeBounds.Size;
 
			return size;
		}
 
		[Obsolete]
		[EditorBrowsable(EditorBrowsableState.Never)]
		protected void Layout(CGSize constraints)
		{
			var platformView = PlatformHandler.ToPlatform();
 
			var width = constraints.Width;
			var height = constraints.Height;
 
			PlatformHandler.VirtualView.Measure(width, height);
 
			platformView.Frame = new CGRect(0, 0, width, height);
 
			var rectangle = platformView.Frame.ToRectangle();
			PlatformHandler.VirtualView.Arrange(rectangle);
			_size = rectangle.Size;
		}
 
		public override void PrepareForReuse()
		{
			Unbind();
			base.PrepareForReuse();
		}
 
		public void Bind(DataTemplate template, object bindingContext, ItemsView itemsView)
		{
			var oldElement = PlatformHandler?.VirtualView as View;
 
			// Run this through the extension method in case it's really a DataTemplateSelector
			var itemTemplate = template.SelectDataTemplate(bindingContext, itemsView);
 
			if (itemTemplate != CurrentTemplate)
			{
				// Remove the old view, if it exists
				if (oldElement != null)
				{
					oldElement.MeasureInvalidated -= MeasureInvalidated;
					oldElement.BindingContext = null;
					itemsView.RemoveLogicalChild(oldElement);
					ClearSubviews();
					_size = Size.Zero;
				}
 
				// Create the content and renderer for the view 
				var content = itemTemplate.CreateContent();
 
				if (content is not View view)
				{
					throw new InvalidOperationException($"{itemTemplate} could not be created from {content}");
				}
 
				// Set the binding context _before_ we create the renderer; that way, it's available during OnElementChanged
				view.BindingContext = bindingContext;
 
				var renderer = TemplateHelpers.GetHandler(view, itemsView.FindMauiContext());
				SetRenderer(renderer);
 
				// And make the new Element a "child" of the ItemsView
				// We deliberately do this _after_ setting the binding context for the new element;
				// if we do it before, the element briefly inherits the ItemsView's bindingcontext and we 
				// emit a bunch of needless binding errors
				itemsView.AddLogicalChild(view);
 
				UpdateSelectionColor(view);
			}
			else
			{
				// Same template
				if (oldElement != null)
				{
					oldElement.BindingContext = bindingContext;
					oldElement.MeasureInvalidated += MeasureInvalidated;
 
					UpdateCellSize();
				}
			}
 
			CurrentTemplate = itemTemplate;
		}
 
		void SetRenderer(IPlatformViewHandler renderer)
		{
			PlatformHandler = renderer;
 
			var platformView = PlatformHandler.ToPlatform();
 
			// Clear out any old views if this cell is being reused
			ClearSubviews();
 
			InitializeContentConstraints(platformView);
 
			UpdateVisualStates();
 
			(renderer.VirtualView as View).MeasureInvalidated += MeasureInvalidated;
		}
 
		void ClearSubviews()
		{
			for (int n = ContentView.Subviews.Length - 1; n >= 0; n--)
			{
				ContentView.Subviews[n].RemoveFromSuperview();
			}
		}
 
		internal void UseContent(TemplatedCell measurementCell)
		{
			// Copy all the content and values from the measurement cell 
			ConstrainedDimension = measurementCell.ConstrainedDimension;
			ConstrainedSize = measurementCell.ConstrainedSize;
			CurrentTemplate = measurementCell.CurrentTemplate;
			_size = measurementCell._size;
			SetRenderer(measurementCell.PlatformHandler);
		}
 
		bool IsUsingVSMForSelectionColor(View view)
		{
			var groups = VisualStateManager.GetVisualStateGroups(view);
			for (var groupIndex = 0; groupIndex < groups.Count; groupIndex++)
			{
				var group = groups[groupIndex];
				for (var stateIndex = 0; stateIndex < group.States.Count; stateIndex++)
				{
					var state = group.States[stateIndex];
					if (state.Name != VisualStateManager.CommonStates.Selected)
					{
						continue;
					}
 
					for (var setterIndex = 0; setterIndex < state.Setters.Count; setterIndex++)
					{
						var setter = state.Setters[setterIndex];
						if (setter.Property.PropertyName == VisualElement.BackgroundColorProperty.PropertyName)
						{
							return true;
						}
					}
				}
			}
 
			return false;
		}
 
		public override bool Selected
		{
			get => base.Selected;
			set
			{
				base.Selected = value;
 
				UpdateVisualStates();
 
				if (base.Selected)
				{
					// This must be called here otherwise the first item will have a gray background
					UpdateSelectionColor();
				}
			}
		}
 
		protected abstract (bool, Size) NeedsContentSizeUpdate(Size currentSize);
 
		void MeasureInvalidated(object sender, EventArgs args)
		{
			var (needsUpdate, toSize) = NeedsContentSizeUpdate(_size);
 
			if (!needsUpdate)
			{
				return;
			}
 
			// Cache the size for next time
			_size = toSize;
 
			// Let the controller know that things need to be arranged again
			OnContentSizeChanged();
		}
 
		protected void OnContentSizeChanged()
		{
			_weakEventManager.HandleEvent(this, EventArgs.Empty, nameof(ContentSizeChanged));
		}
 
		protected void OnLayoutAttributesChanged(UICollectionViewLayoutAttributes newAttributes)
		{
			_weakEventManager.HandleEvent(this, new LayoutAttributesChangedEventArgs(newAttributes), nameof(LayoutAttributesChanged));
		}
 
		protected abstract bool AttributesConsistentWithConstrainedDimension(UICollectionViewLayoutAttributes attributes);
 
		void UpdateVisualStates()
		{
			if (PlatformHandler?.VirtualView is VisualElement element)
			{
				VisualStateManager.GoToState(element, Selected
					? VisualStateManager.CommonStates.Selected
					: VisualStateManager.CommonStates.Normal);
			}
		}
 
		void UpdateSelectionColor()
		{
			if (PlatformHandler?.VirtualView is not View view)
			{
				return;
			}
 
			UpdateSelectionColor(view);
		}
 
		void UpdateSelectionColor(View view)
		{
			if (SelectedBackgroundView is null)
			{
				return;
			}
 
			// Prevents the use of default color when there are VisualStateManager with Selected state setting the background color
			// First we check whether the cell has the default selected background color; if it does, then we should check
			// to see if the cell content is the VSM to set a selected color
 
			if (ColorExtensions.AreEqual(SelectedBackgroundView.BackgroundColor, ColorExtensions.Gray) && IsUsingVSMForSelectionColor(view))
			{
				SelectedBackgroundView.BackgroundColor = UIColor.Clear;
			}
		}
	}
}