From f97fb08263b0af615b95a7230d6916463754a1f6 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Mon, 14 Jan 2019 04:48:59 -0700 Subject: [PATCH] [iOS, Android] Implement Snap alignment for CollectionView (#4414) * Implement snap alignment for iOS CollectionView * Add missing End/MandatorySingle implementation for Android * Implement MandatorySingle snapping on iOS * Fix issues with Android MandatorySingle skipping items; * Fix rebase issue --- .../ExampleTemplates.cs | 13 +- .../CollectionView/EndSingleSnapHelper.cs | 23 ++ .../CollectionView/ItemsViewRenderer.cs | 13 +- .../CollectionView/SingleSnapHelper.cs | 86 ++++++++ .../CollectionView/SnapManager.cs | 6 +- .../CollectionView/StartPagerSnapHelper.cs | 72 ------- .../CollectionView/StartSingleSnapHelper.cs | 23 ++ .../Xamarin.Forms.Platform.Android.csproj | 4 +- .../CollectionViewController.cs | 3 + .../CollectionView/CollectionViewRenderer.cs | 12 +- .../CollectionView/ItemsViewLayout.cs | 101 ++++++++- .../CollectionView/SnapHelpers.cs | 198 ++++++++++++++++++ .../Xamarin.Forms.Platform.iOS.csproj | 1 + 13 files changed, 463 insertions(+), 92 deletions(-) create mode 100644 Xamarin.Forms.Platform.Android/CollectionView/EndSingleSnapHelper.cs create mode 100644 Xamarin.Forms.Platform.Android/CollectionView/SingleSnapHelper.cs delete mode 100644 Xamarin.Forms.Platform.Android/CollectionView/StartPagerSnapHelper.cs create mode 100644 Xamarin.Forms.Platform.Android/CollectionView/StartSingleSnapHelper.cs create mode 100644 Xamarin.Forms.Platform.iOS/CollectionView/SnapHelpers.cs diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ExampleTemplates.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ExampleTemplates.cs index ade2739b1..6543a764f 100644 --- a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ExampleTemplates.cs +++ b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ExampleTemplates.cs @@ -9,9 +9,8 @@ var templateLayout = new Grid { RowDefinitions = new RowDefinitionCollection { new RowDefinition(), new RowDefinition() }, - WidthRequest = 100, - HeightRequest = 100, - Padding = new Thickness(10) + WidthRequest = 200, + HeightRequest = 100 }; var image = new Image @@ -48,15 +47,15 @@ var templateLayout = new Grid { RowDefinitions = new RowDefinitionCollection { new RowDefinition(), new RowDefinition {Height = GridLength.Auto} }, - WidthRequest = 100, - HeightRequest = 130 + WidthRequest = 280, + HeightRequest = 310, }; var image = new Image { Margin = new Thickness(5), - HeightRequest = 100, - WidthRequest = 100, + HeightRequest = 280, + WidthRequest = 280, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, Aspect = Aspect.AspectFit diff --git a/Xamarin.Forms.Platform.Android/CollectionView/EndSingleSnapHelper.cs b/Xamarin.Forms.Platform.Android/CollectionView/EndSingleSnapHelper.cs new file mode 100644 index 000000000..415b5fe52 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/CollectionView/EndSingleSnapHelper.cs @@ -0,0 +1,23 @@ +using Android.Support.V7.Widget; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + internal class EndSingleSnapHelper : SingleSnapHelper + { + public override int[] CalculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, AView targetView) + { + var orientationHelper = CreateOrientationHelper(layoutManager); + var isHorizontal = layoutManager.CanScrollHorizontally(); + var rtl = isHorizontal && IsLayoutReversed(layoutManager); + + var distance = rtl + ? -orientationHelper.GetDecoratedStart(targetView) + : orientationHelper.TotalSpace - orientationHelper.GetDecoratedEnd(targetView); + + return isHorizontal + ? new[] { -distance, 1 } + : new[] { 1, -distance }; + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewRenderer.cs b/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewRenderer.cs index f56eadfdc..95be7b287 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewRenderer.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewRenderer.cs @@ -276,9 +276,10 @@ namespace Xamarin.Forms.Platform.Android UpdateFlowDirection(); // Keep track of the ItemsLayout's property changes - _layout.PropertyChanged += LayoutOnPropertyChanged; - - // TODO hartez 2018/09/17 13:16:12 This propertychanged handler needs to be torn down in Dispose and TearDownElement + if (_layout != null) + { + _layout.PropertyChanged += LayoutOnPropertyChanged; + } // Listen for ScrollTo requests ItemsView.ScrollToRequested += ScrollToRequested; @@ -291,6 +292,12 @@ namespace Xamarin.Forms.Platform.Android return; } + // Stop listening for layout property changes + if (_layout != null) + { + _layout.PropertyChanged -= LayoutOnPropertyChanged; + } + // Stop listening for property changes oldElement.PropertyChanged -= OnElementPropertyChanged; diff --git a/Xamarin.Forms.Platform.Android/CollectionView/SingleSnapHelper.cs b/Xamarin.Forms.Platform.Android/CollectionView/SingleSnapHelper.cs new file mode 100644 index 000000000..bab11a42f --- /dev/null +++ b/Xamarin.Forms.Platform.Android/CollectionView/SingleSnapHelper.cs @@ -0,0 +1,86 @@ +using Android.Support.V7.Widget; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + internal class SingleSnapHelper : PagerSnapHelper + { + // CurrentTargetPosition will have this value until the user scrolls around + protected int CurrentTargetPosition = -1; + + protected static OrientationHelper CreateOrientationHelper(RecyclerView.LayoutManager layoutManager) + { + return layoutManager.CanScrollHorizontally() + ? OrientationHelper.CreateHorizontalHelper(layoutManager) + : OrientationHelper.CreateVerticalHelper(layoutManager); + } + + protected static bool IsLayoutReversed(RecyclerView.LayoutManager layoutManager) + { + if (layoutManager is LinearLayoutManager linearLayoutManager) + { + return linearLayoutManager.ReverseLayout; + } + + return false; + } + + public override AView FindSnapView(RecyclerView.LayoutManager layoutManager) + { + if (layoutManager.ItemCount == 0) + { + return null; + } + + if (!(layoutManager is LinearLayoutManager linearLayoutManager)) + { + // Don't snap to anything if this isn't a LinearLayoutManager; + return null; + } + + var targetItemPosition = CurrentTargetPosition; + + if (targetItemPosition != -1) + { + return linearLayoutManager.FindViewByPosition(targetItemPosition); + } + + return null; + } + + public override int FindTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) + { + if (CurrentTargetPosition == -1) + { + CurrentTargetPosition = base.FindTargetSnapPosition(layoutManager, velocityX, velocityY); + return CurrentTargetPosition; + } + + var increment = 1; + + if (layoutManager.CanScrollHorizontally()) + { + if (velocityX < 0) + { + increment = -1; + } + } + else if (layoutManager.CanScrollVertically()) + { + if (velocityY < 0) + { + increment = -1; + } + } + + if (IsLayoutReversed(layoutManager)) + { + increment = increment * -1; + } + + CurrentTargetPosition = CurrentTargetPosition + increment; + + return CurrentTargetPosition; + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/CollectionView/SnapManager.cs b/Xamarin.Forms.Platform.Android/CollectionView/SnapManager.cs index efd1c591a..ae06fe340 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/SnapManager.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/SnapManager.cs @@ -65,11 +65,11 @@ namespace Xamarin.Forms.Platform.Android switch (alignment) { case SnapPointsAlignment.Start: - return new StartPagerSnapHelper(); + return new StartSingleSnapHelper(); case SnapPointsAlignment.Center: - return new PagerSnapHelper(); + return new SingleSnapHelper(); case SnapPointsAlignment.End: - break; + return new EndSingleSnapHelper(); default: throw new ArgumentOutOfRangeException(nameof(alignment), alignment, null); } diff --git a/Xamarin.Forms.Platform.Android/CollectionView/StartPagerSnapHelper.cs b/Xamarin.Forms.Platform.Android/CollectionView/StartPagerSnapHelper.cs deleted file mode 100644 index 640091c0a..000000000 --- a/Xamarin.Forms.Platform.Android/CollectionView/StartPagerSnapHelper.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Android.Support.V7.Widget; -using AView = Android.Views.View; - -namespace Xamarin.Forms.Platform.Android -{ - internal class StartPagerSnapHelper : PagerSnapHelper - { - protected static OrientationHelper CreateOrientationHelper(RecyclerView.LayoutManager layoutManager) - { - return layoutManager.CanScrollHorizontally() - ? OrientationHelper.CreateHorizontalHelper(layoutManager) - : OrientationHelper.CreateVerticalHelper(layoutManager); - } - - protected static bool IsLayoutReversed(RecyclerView.LayoutManager layoutManager) - { - if (layoutManager is LinearLayoutManager linearLayoutManager) - { - return linearLayoutManager.ReverseLayout; - } - - return false; - } - - public override int FindTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) - { - var x = base.FindTargetSnapPosition(layoutManager, velocityX, velocityY); - return x; - } - - public override AView FindSnapView(RecyclerView.LayoutManager layoutManager) - { - if (layoutManager.ItemCount == 0) - { - return null; - } - - if (!(layoutManager is LinearLayoutManager linearLayoutManager)) - { - // Don't snap to anything if this isn't a LinearLayoutManager; - return null; - } - - // Find the first fully visible item - var firstVisibleItemPosition = linearLayoutManager.FindFirstCompletelyVisibleItemPosition(); - - if (firstVisibleItemPosition == RecyclerView.NoPosition) - { - // If there are no fully visible items, drop back to default PagerSnapHelper behavior - return base.FindSnapView(layoutManager); - } - - // Return the view to snap - return linearLayoutManager.FindViewByPosition(firstVisibleItemPosition); - } - - public override int[] CalculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, AView targetView) - { - var orientationHelper = CreateOrientationHelper(layoutManager); - var isHorizontal = layoutManager.CanScrollHorizontally(); - var rtl = isHorizontal && IsLayoutReversed(layoutManager); - - var distance = rtl - ? -orientationHelper.GetDecoratedEnd(targetView) - : orientationHelper.GetDecoratedStart(targetView); - - return isHorizontal - ? new[] { distance, 1 } - : new[] { 1, distance }; - } - } -} \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/CollectionView/StartSingleSnapHelper.cs b/Xamarin.Forms.Platform.Android/CollectionView/StartSingleSnapHelper.cs new file mode 100644 index 000000000..3136fa51c --- /dev/null +++ b/Xamarin.Forms.Platform.Android/CollectionView/StartSingleSnapHelper.cs @@ -0,0 +1,23 @@ +using Android.Support.V7.Widget; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + internal class StartSingleSnapHelper : SingleSnapHelper + { + public override int[] CalculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, AView targetView) + { + var orientationHelper = CreateOrientationHelper(layoutManager); + var isHorizontal = layoutManager.CanScrollHorizontally(); + var rtl = isHorizontal && IsLayoutReversed(layoutManager); + + var distance = rtl + ? -orientationHelper.GetDecoratedEnd(targetView) + : orientationHelper.GetDecoratedStart(targetView); + + return isHorizontal + ? new[] { distance, 1 } + : new[] { 1, distance }; + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj index 1afc85f75..fe8b75c82 100644 --- a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj +++ b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj @@ -71,7 +71,9 @@ + + @@ -89,7 +91,7 @@ - + diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/CollectionViewController.cs b/Xamarin.Forms.Platform.iOS/CollectionView/CollectionViewController.cs index 349371cfb..a8ac74bb8 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/CollectionViewController.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/CollectionViewController.cs @@ -1,4 +1,7 @@ using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using CoreGraphics; using Foundation; using UIKit; diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/CollectionViewRenderer.cs b/Xamarin.Forms.Platform.iOS/CollectionView/CollectionViewRenderer.cs index 01b5f049e..7661ec79d 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/CollectionViewRenderer.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/CollectionViewRenderer.cs @@ -18,7 +18,8 @@ namespace Xamarin.Forms.Platform.iOS public class CollectionViewRenderer : ViewRenderer { CollectionViewController _collectionViewController; - ItemsViewLayout _layout; + ItemsViewLayout _flowLayout; + IItemsLayout _layout; bool _disposed; public CollectionViewRenderer() @@ -89,11 +90,12 @@ namespace Xamarin.Forms.Platform.iOS return; } - _layout = SelectLayout(newElement.ItemsLayout); - _collectionViewController = new CollectionViewController(newElement, _layout); + _layout = newElement.ItemsLayout; + _flowLayout = SelectLayout(_layout); + _collectionViewController = new CollectionViewController(newElement, _flowLayout); SetNativeControl(_collectionViewController.View); _collectionViewController.CollectionView.BackgroundColor = UIColor.Clear; - _collectionViewController.CollectionView.WeakDelegate = _layout; + _collectionViewController.CollectionView.WeakDelegate = _flowLayout; _collectionViewController.UpdateEmptyView(); // Listen for ScrollTo requests @@ -123,7 +125,7 @@ namespace Xamarin.Forms.Platform.iOS } _collectionViewController.CollectionView.ScrollToItem(indexPath, - args.ScrollToPosition.ToCollectionViewScrollPosition(_layout.ScrollDirection), + args.ScrollToPosition.ToCollectionViewScrollPosition(_flowLayout.ScrollDirection), args.IsAnimated); } diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs index c0c7971c0..58fe73e98 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs @@ -1,9 +1,11 @@ using System; using System.ComponentModel; using System.Diagnostics; +using System.Runtime.CompilerServices; using CoreGraphics; using Foundation; using UIKit; +using Xamarin.Forms.Internals; namespace Xamarin.Forms.Platform.iOS { @@ -53,7 +55,7 @@ namespace Xamarin.Forms.Platform.iOS HandlePropertyChanged(propertyChanged); } - protected virtual void HandlePropertyChanged(PropertyChangedEventArgs propertyChanged) + protected virtual void HandlePropertyChanged(PropertyChangedEventArgs propertyChanged) { } @@ -215,5 +217,102 @@ namespace Xamarin.Forms.Platform.iOS { _needCellSizeUpdate = true; } + + public override CGPoint TargetContentOffset(CGPoint proposedContentOffset, CGPoint scrollingVelocity) + { + var snapPointsType = _itemsLayout.SnapPointsType; + + if (snapPointsType == SnapPointsType.None) + { + // Nothing to do here; fall back to the default + return base.TargetContentOffset(proposedContentOffset, scrollingVelocity); + } + + var alignment = _itemsLayout.SnapPointsAlignment; + + if (snapPointsType == SnapPointsType.MandatorySingle) + { + // Mandatory snapping, single element + return ScrollSingle(alignment, proposedContentOffset, scrollingVelocity); + } + + // Get the viewport of the UICollectionView at the proposed content offset + var viewport = new CGRect(proposedContentOffset, CollectionView.Bounds.Size); + + // And find all the elements currently visible in the viewport + var visibleElements = LayoutAttributesForElementsInRect(viewport); + + if (visibleElements.Length == 0) + { + // Nothing to see here; fall back to the default + return base.TargetContentOffset(proposedContentOffset, scrollingVelocity); + } + + if (visibleElements.Length == 1) + { + // If there is only one item in the viewport, then we need to align the viewport with it + return SnapHelpers.AdjustContentOffset(proposedContentOffset, visibleElements[0].Frame, viewport, + alignment, ScrollDirection); + } + + // If there are multiple items in the viewport, we need to choose the one which is + // closest to the relevant part of the viewport while being sufficiently visible + + // Find the spot in the viewport we're trying to align with + var alignmentTarget = SnapHelpers.FindAlignmentTarget(alignment, proposedContentOffset, + CollectionView, ScrollDirection); + + // Find the closest sufficiently visible candidate + var bestCandidate = SnapHelpers.FindBestSnapCandidate(visibleElements, viewport, alignmentTarget); + + if (bestCandidate != null) + { + return SnapHelpers.AdjustContentOffset(proposedContentOffset, bestCandidate.Frame, viewport, alignment, + ScrollDirection); + } + + // If we got this far an nothing matched, it means that we have multiple items but somehow + // none of them fit at least half in the viewport. So just fall back to the first item + return SnapHelpers.AdjustContentOffset(proposedContentOffset, visibleElements[0].Frame, viewport, alignment, + ScrollDirection); + } + + CGPoint ScrollSingle(SnapPointsAlignment alignment, CGPoint proposedContentOffset, CGPoint scrollingVelocity) + { + // Get the viewport of the UICollectionView at the current content offset + var contentOffset = CollectionView.ContentOffset; + var viewport = new CGRect(contentOffset, CollectionView.Bounds.Size); + + // Find the spot in the viewport we're trying to align with + var alignmentTarget = SnapHelpers.FindAlignmentTarget(alignment, contentOffset, CollectionView, ScrollDirection); + + var visibleElements = LayoutAttributesForElementsInRect(viewport); + + // Find the current aligned item + var currentItem = SnapHelpers.FindBestSnapCandidate(visibleElements, viewport, alignmentTarget); + + if (currentItem == null) + { + // Somehow we don't currently have an item in the viewport near the target; fall back to the + // default behavior + return base.TargetContentOffset(proposedContentOffset, scrollingVelocity); + } + + // Determine the index of the current item + var currentIndex = visibleElements.IndexOf(currentItem); + + // Figure out the step size when jumping to the "next" element + var span = 1; + if (_itemsLayout is GridItemsLayout gridItemsLayout) + { + span = gridItemsLayout.Span; + } + + // Find the next item in the + currentItem = SnapHelpers.FindNextItem(visibleElements, ScrollDirection, span, scrollingVelocity, currentIndex); + + return SnapHelpers.AdjustContentOffset(CollectionView.ContentOffset, currentItem.Frame, viewport, alignment, + ScrollDirection); + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/SnapHelpers.cs b/Xamarin.Forms.Platform.iOS/CollectionView/SnapHelpers.cs new file mode 100644 index 000000000..e9cd359e1 --- /dev/null +++ b/Xamarin.Forms.Platform.iOS/CollectionView/SnapHelpers.cs @@ -0,0 +1,198 @@ +using System; +using CoreGraphics; +using UIKit; + +namespace Xamarin.Forms.Platform.iOS +{ + 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]; + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj b/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj index 087721bfd..c6d8e30d0 100644 --- a/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj +++ b/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj @@ -129,6 +129,7 @@ +