File: iOS\CollectionView\TemplatedCell.cs
Web Access
Project: src\src\Compatibility\Core\src\Compatibility.csproj (Microsoft.Maui.Controls.Compatibility)
using System;
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using ObjCRuntime;
using UIKit;
 
namespace Microsoft.Maui.Controls.Compatibility.Platform.iOS
{
	[System.Obsolete]
	public abstract class TemplatedCell : ItemsViewCell
	{
		public event EventHandler<EventArgs> ContentSizeChanged;
		public event EventHandler<LayoutAttributesChangedEventArgs> LayoutAttributesChanged;
 
		protected CGSize ConstrainedSize;
 
		protected nfloat ConstrainedDimension;
 
		public DataTemplate CurrentTemplate { get; private set; }
 
		// 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.ToSizeF();
 
		[Export("initWithFrame:")]
		[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
		protected TemplatedCell(CGRect frame) : base(frame)
		{
		}
 
		internal IVisualElementRenderer VisualElementRenderer { get; private set; }
 
		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;
		}
 
		public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittingAttributes(
			UICollectionViewLayoutAttributes layoutAttributes)
		{
			var preferredAttributes = base.PreferredLayoutAttributesFittingAttributes(layoutAttributes);
 
			var preferredSize = preferredAttributes.Frame.Size;
 
			if (SizesAreSame(preferredSize, _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);
 
			//_isMeasured = true;
 
			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 nativeView = VisualElementRenderer.NativeView;
			nativeView.Frame = new CGRect(CGPoint.Empty, size);
 
			// Layout the Forms element 
			var nativeBounds = nativeView.Frame.ToRectangle();
			VisualElementRenderer.Element.Layout(nativeBounds);
			_size = nativeBounds.Size;
 
			return size;
		}
 
		public void Bind(DataTemplate template, object bindingContext, ItemsView itemsView)
		{
			var oldElement = VisualElementRenderer?.Element;
 
			// 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);
					ContentView.ClearSubviews();
					_size = Size.Zero;
				}
 
				// Create the content and renderer for the view 
				var view = itemTemplate.CreateContent() as View;
 
				// Set the binding context _before_ we create the renderer; that way, it's available during OnElementChanged
				view.BindingContext = bindingContext;
 
				var renderer = TemplateHelpers.CreateRenderer(view);
				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);
 
				// 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 (SelectedBackgroundView.BackgroundColor == Maui.Platform.ColorExtensions.Gray && IsUsingVSMForSelectionColor(view))
				{
					SelectedBackgroundView = new UIView
					{
						BackgroundColor = UIColor.Clear
					};
				}
			}
			else
			{
				// Same template
				if (oldElement != null)
				{
					if (oldElement.BindingContext == null || !(oldElement.BindingContext.Equals(bindingContext)))
					{
						// If the data is different, update it
 
						// Unhook the MeasureInvalidated handler, otherwise it'll fire for every invalidation during the 
						// BindingContext change
						oldElement.MeasureInvalidated -= MeasureInvalidated;
						oldElement.BindingContext = bindingContext;
						oldElement.MeasureInvalidated += MeasureInvalidated;
 
						UpdateCellSize();
					}
				}
			}
 
			CurrentTemplate = itemTemplate;
		}
 
		void SetRenderer(IVisualElementRenderer renderer)
		{
			VisualElementRenderer = renderer;
			var nativeView = VisualElementRenderer.NativeView;
 
			// Clear out any old views if this cell is being reused
			ContentView.ClearSubviews();
 
			InitializeContentConstraints(nativeView);
 
			UpdateVisualStates();
 
			renderer.Element.MeasureInvalidated += MeasureInvalidated;
		}
 
		protected void Layout(CGSize constraints)
		{
			var nativeView = VisualElementRenderer.NativeView;
 
			var width = constraints.Width;
			var height = constraints.Height;
 
			VisualElementRenderer.Element.Measure(width, height, MeasureFlags.IncludeMargins);
 
			nativeView.Frame = new CGRect(0, 0, width, height);
 
			var rectangle = nativeView.Frame.ToRectangle();
			VisualElementRenderer.Element.Layout(rectangle);
			_size = rectangle.Size;
		}
 
		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.VisualElementRenderer);
		}
 
		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();
			}
		}
 
		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()
		{
			ContentSizeChanged?.Invoke(this, EventArgs.Empty);
		}
 
		protected void OnLayoutAttributesChanged(UICollectionViewLayoutAttributes newAttributes)
		{
			LayoutAttributesChanged?.Invoke(this, new LayoutAttributesChangedEventArgs(newAttributes));
		}
 
		protected abstract bool AttributesConsistentWithConstrainedDimension(UICollectionViewLayoutAttributes attributes);
 
		bool SizesAreSame(CGSize preferredSize, Size elementSize)
		{
			const double tolerance = 0.000001;
 
			if (Math.Abs(preferredSize.Height - elementSize.Height) > tolerance)
			{
				return false;
			}
 
			if (Math.Abs(preferredSize.Width - elementSize.Width) > tolerance)
			{
				return false;
			}
 
			return true;
		}
 
		void UpdateVisualStates()
		{
			var element = VisualElementRenderer?.Element;
 
			if (element != null)
			{
				VisualStateManager.GoToState(element, Selected
					? VisualStateManager.CommonStates.Selected
					: VisualStateManager.CommonStates.Normal);
			}
		}
	}
}