File: Handlers\Items\iOS\SnapHelpers.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using CoreGraphics;
using ObjCRuntime;
using UIKit;
 
namespace Microsoft.Maui.Controls.Handlers.Items
{
	internal class SnapHelpers
	{
		public static CGPoint AdjustContentOffset(CGPoint proposedContentOffset, CGRect itemFrame,
			CGRect viewport, SnapPointsAlignment alignment, UICollectionViewScrollDirection scrollDirection)
		{
			var offset = GetViewportOffset(itemFrame, viewport, alignment, scrollDirection);
			return new CGPoint(proposedContentOffset.X - offset.X, proposedContentOffset.Y - offset.Y);
		}
 
		public static CGPoint FindAlignmentTarget(SnapPointsAlignment snapPointsAlignment,
			CGPoint contentOffset, UICollectionView collectionView, UICollectionViewScrollDirection scrollDirection)
		{
			var inset = collectionView.ContentInset;
			var bounds = collectionView.Bounds;
 
			switch (scrollDirection)
			{
				case UICollectionViewScrollDirection.Vertical:
					var y = FindAlignmentTarget(snapPointsAlignment, contentOffset.Y, inset.Top,
						contentOffset.Y + bounds.Height, inset.Bottom);
					return new CGPoint(contentOffset.X, y);
				case UICollectionViewScrollDirection.Horizontal:
					var x = FindAlignmentTarget(snapPointsAlignment, contentOffset.X, inset.Left,
						contentOffset.X + bounds.Width, inset.Right);
					return new CGPoint(x, contentOffset.Y);
				default:
					throw new ArgumentOutOfRangeException();
			}
		}
 
		public static UICollectionViewLayoutAttributes FindBestSnapCandidate(UICollectionViewLayoutAttributes[] items,
			CGRect viewport, CGPoint alignmentTarget)
		{
			UICollectionViewLayoutAttributes bestCandidate = null;
 
			foreach (UICollectionViewLayoutAttributes item in items)
			{
				if (!IsAtLeastHalfVisible(item, viewport))
				{
					continue;
				}
 
				bestCandidate = bestCandidate == null ? item : Nearer(bestCandidate, item, alignmentTarget);
			}
 
			return bestCandidate;
		}
 
		static nfloat Area(CGRect rect)
		{
			return rect.Height * rect.Width;
		}
 
		static CGPoint Center(CGRect rect)
		{
			return new CGPoint(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
		}
 
		static nfloat DistanceSquared(CGRect rect, CGPoint target)
		{
			var rectCenter = Center(rect);
 
			return (target.X - rectCenter.X) * (target.X - rectCenter.X) +
					(target.Y - rectCenter.Y) * (target.Y - rectCenter.Y);
		}
 
		static int Clamp(int n, int min, int max)
		{
			if (n < min)
			{
				return min;
			}
 
			if (n > max)
			{
				return max;
			}
 
			return n;
		}
 
		static nfloat FindAlignmentTarget(SnapPointsAlignment snapPointsAlignment, nfloat start, nfloat startInset,
			nfloat end, nfloat endInset)
		{
			switch (snapPointsAlignment)
			{
				case SnapPointsAlignment.Center:
					var viewPortStart = start + startInset;
					var viewPortEnd = end - endInset;
					var viewPortSize = viewPortEnd - viewPortStart;
 
					return viewPortStart + (viewPortSize / 2);
 
				case SnapPointsAlignment.End:
					return end - endInset;
 
				case SnapPointsAlignment.Start:
				default:
					return start + startInset;
			}
		}
 
		static CGPoint GetViewportOffset(CGRect itemFrame, CGRect viewport, SnapPointsAlignment snapPointsAlignment,
			UICollectionViewScrollDirection scrollDirection)
		{
			if (scrollDirection == UICollectionViewScrollDirection.Horizontal)
			{
				if (snapPointsAlignment == SnapPointsAlignment.Start)
				{
					return new CGPoint(viewport.Left - itemFrame.Left, 0);
				}
 
				if (snapPointsAlignment == SnapPointsAlignment.End)
				{
					return new CGPoint(viewport.Right - itemFrame.Right, 0);
				}
 
				var centerViewport = Center(viewport);
				var centerItem = Center(itemFrame);
 
				return new CGPoint(centerViewport.X - centerItem.X, 0);
			}
 
			if (snapPointsAlignment == SnapPointsAlignment.Start)
			{
				return new CGPoint(0, viewport.Top - itemFrame.Top);
			}
 
			if (snapPointsAlignment == SnapPointsAlignment.End)
			{
				return new CGPoint(0, viewport.Bottom - itemFrame.Bottom);
			}
 
			var centerViewport1 = Center(viewport);
			var centerItem1 = Center(itemFrame);
 
			return new CGPoint(0, centerViewport1.Y - centerItem1.Y);
		}
 
		static bool IsAtLeastHalfVisible(UICollectionViewLayoutAttributes item, CGRect viewport)
		{
			var itemFrame = item.Frame;
			var visibleArea = Area(CGRect.Intersect(itemFrame, viewport));
 
			return visibleArea >= Area(itemFrame) / 2;
		}
 
		static UICollectionViewLayoutAttributes Nearer(UICollectionViewLayoutAttributes a,
			UICollectionViewLayoutAttributes b,
			CGPoint target)
		{
			var dA = DistanceSquared(a.Frame, target);
			var dB = DistanceSquared(b.Frame, target);
 
			if (dA < dB)
			{
				return a;
			}
 
			return b;
		}
 
		public static UICollectionViewLayoutAttributes FindNextItem(UICollectionViewLayoutAttributes[] items,
			UICollectionViewScrollDirection direction, int step, CGPoint scrollingVelocity, int currentIndex)
		{
			var velocity = direction == UICollectionViewScrollDirection.Horizontal
				? scrollingVelocity.X
				: scrollingVelocity.Y;
 
			if (velocity == 0)
			{
				// The user isn't scrolling at all, just stay where we are
				return items[currentIndex];
			}
 
			// Move the index up or down by increment, depending on the velocity
			if (velocity > 0)
			{
				currentIndex = currentIndex + step;
			}
			else if (velocity < 0)
			{
				currentIndex = currentIndex - step;
			}
 
			// Make sure we're not out of bounds
			currentIndex = Clamp(currentIndex, 0, items.Length - 1);
 
			return items[currentIndex];
		}
	}
}