зеркало из https://github.com/DeGsoft/maui-linux.git
[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
This commit is contained in:
Родитель
29074eb045
Коммит
f97fb08263
|
@ -9,9 +9,8 @@
|
||||||
var templateLayout = new Grid
|
var templateLayout = new Grid
|
||||||
{
|
{
|
||||||
RowDefinitions = new RowDefinitionCollection { new RowDefinition(), new RowDefinition() },
|
RowDefinitions = new RowDefinitionCollection { new RowDefinition(), new RowDefinition() },
|
||||||
WidthRequest = 100,
|
WidthRequest = 200,
|
||||||
HeightRequest = 100,
|
HeightRequest = 100
|
||||||
Padding = new Thickness(10)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var image = new Image
|
var image = new Image
|
||||||
|
@ -48,15 +47,15 @@
|
||||||
var templateLayout = new Grid
|
var templateLayout = new Grid
|
||||||
{
|
{
|
||||||
RowDefinitions = new RowDefinitionCollection { new RowDefinition(), new RowDefinition {Height = GridLength.Auto} },
|
RowDefinitions = new RowDefinitionCollection { new RowDefinition(), new RowDefinition {Height = GridLength.Auto} },
|
||||||
WidthRequest = 100,
|
WidthRequest = 280,
|
||||||
HeightRequest = 130
|
HeightRequest = 310,
|
||||||
};
|
};
|
||||||
|
|
||||||
var image = new Image
|
var image = new Image
|
||||||
{
|
{
|
||||||
Margin = new Thickness(5),
|
Margin = new Thickness(5),
|
||||||
HeightRequest = 100,
|
HeightRequest = 280,
|
||||||
WidthRequest = 100,
|
WidthRequest = 280,
|
||||||
HorizontalOptions = LayoutOptions.Center,
|
HorizontalOptions = LayoutOptions.Center,
|
||||||
VerticalOptions = LayoutOptions.Center,
|
VerticalOptions = LayoutOptions.Center,
|
||||||
Aspect = Aspect.AspectFit
|
Aspect = Aspect.AspectFit
|
||||||
|
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -276,9 +276,10 @@ namespace Xamarin.Forms.Platform.Android
|
||||||
UpdateFlowDirection();
|
UpdateFlowDirection();
|
||||||
|
|
||||||
// Keep track of the ItemsLayout's property changes
|
// Keep track of the ItemsLayout's property changes
|
||||||
|
if (_layout != null)
|
||||||
|
{
|
||||||
_layout.PropertyChanged += LayoutOnPropertyChanged;
|
_layout.PropertyChanged += LayoutOnPropertyChanged;
|
||||||
|
}
|
||||||
// TODO hartez 2018/09/17 13:16:12 This propertychanged handler needs to be torn down in Dispose and TearDownElement
|
|
||||||
|
|
||||||
// Listen for ScrollTo requests
|
// Listen for ScrollTo requests
|
||||||
ItemsView.ScrollToRequested += ScrollToRequested;
|
ItemsView.ScrollToRequested += ScrollToRequested;
|
||||||
|
@ -291,6 +292,12 @@ namespace Xamarin.Forms.Platform.Android
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop listening for layout property changes
|
||||||
|
if (_layout != null)
|
||||||
|
{
|
||||||
|
_layout.PropertyChanged -= LayoutOnPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
// Stop listening for property changes
|
// Stop listening for property changes
|
||||||
oldElement.PropertyChanged -= OnElementPropertyChanged;
|
oldElement.PropertyChanged -= OnElementPropertyChanged;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,11 +65,11 @@ namespace Xamarin.Forms.Platform.Android
|
||||||
switch (alignment)
|
switch (alignment)
|
||||||
{
|
{
|
||||||
case SnapPointsAlignment.Start:
|
case SnapPointsAlignment.Start:
|
||||||
return new StartPagerSnapHelper();
|
return new StartSingleSnapHelper();
|
||||||
case SnapPointsAlignment.Center:
|
case SnapPointsAlignment.Center:
|
||||||
return new PagerSnapHelper();
|
return new SingleSnapHelper();
|
||||||
case SnapPointsAlignment.End:
|
case SnapPointsAlignment.End:
|
||||||
break;
|
return new EndSingleSnapHelper();
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(alignment), alignment, null);
|
throw new ArgumentOutOfRangeException(nameof(alignment), alignment, 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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,7 +71,9 @@
|
||||||
<Compile Include="CollectionView\CarouselViewRenderer.cs" />
|
<Compile Include="CollectionView\CarouselViewRenderer.cs" />
|
||||||
<Compile Include="CollectionView\DataChangeObserver.cs" />
|
<Compile Include="CollectionView\DataChangeObserver.cs" />
|
||||||
<Compile Include="CollectionView\EmptySource.cs" />
|
<Compile Include="CollectionView\EmptySource.cs" />
|
||||||
|
<Compile Include="CollectionView\SingleSnapHelper.cs" />
|
||||||
<Compile Include="CollectionView\EmptyViewAdapter.cs" />
|
<Compile Include="CollectionView\EmptyViewAdapter.cs" />
|
||||||
|
<Compile Include="CollectionView\EndSingleSnapHelper.cs" />
|
||||||
<Compile Include="CollectionView\ItemsViewAdapter.cs" />
|
<Compile Include="CollectionView\ItemsViewAdapter.cs" />
|
||||||
<Compile Include="CollectionView\EdgeSnapHelper.cs" />
|
<Compile Include="CollectionView\EdgeSnapHelper.cs" />
|
||||||
<Compile Include="CollectionView\EndSnapHelper.cs" />
|
<Compile Include="CollectionView\EndSnapHelper.cs" />
|
||||||
|
@ -89,7 +91,7 @@
|
||||||
<Compile Include="CollectionView\SelectableViewHolder.cs" />
|
<Compile Include="CollectionView\SelectableViewHolder.cs" />
|
||||||
<Compile Include="CollectionView\SizedItemContentView.cs" />
|
<Compile Include="CollectionView\SizedItemContentView.cs" />
|
||||||
<Compile Include="CollectionView\SnapManager.cs" />
|
<Compile Include="CollectionView\SnapManager.cs" />
|
||||||
<Compile Include="CollectionView\StartPagerSnapHelper.cs" />
|
<Compile Include="CollectionView\StartSingleSnapHelper.cs" />
|
||||||
<Compile Include="CollectionView\StartSnapHelper.cs" />
|
<Compile Include="CollectionView\StartSnapHelper.cs" />
|
||||||
<Compile Include="CollectionView\TemplatedItemViewHolder.cs" />
|
<Compile Include="CollectionView\TemplatedItemViewHolder.cs" />
|
||||||
<Compile Include="CollectionView\TextViewHolder.cs" />
|
<Compile Include="CollectionView\TextViewHolder.cs" />
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using CoreGraphics;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,8 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
public class CollectionViewRenderer : ViewRenderer<CollectionView, UIView>
|
public class CollectionViewRenderer : ViewRenderer<CollectionView, UIView>
|
||||||
{
|
{
|
||||||
CollectionViewController _collectionViewController;
|
CollectionViewController _collectionViewController;
|
||||||
ItemsViewLayout _layout;
|
ItemsViewLayout _flowLayout;
|
||||||
|
IItemsLayout _layout;
|
||||||
bool _disposed;
|
bool _disposed;
|
||||||
|
|
||||||
public CollectionViewRenderer()
|
public CollectionViewRenderer()
|
||||||
|
@ -89,11 +90,12 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_layout = SelectLayout(newElement.ItemsLayout);
|
_layout = newElement.ItemsLayout;
|
||||||
_collectionViewController = new CollectionViewController(newElement, _layout);
|
_flowLayout = SelectLayout(_layout);
|
||||||
|
_collectionViewController = new CollectionViewController(newElement, _flowLayout);
|
||||||
SetNativeControl(_collectionViewController.View);
|
SetNativeControl(_collectionViewController.View);
|
||||||
_collectionViewController.CollectionView.BackgroundColor = UIColor.Clear;
|
_collectionViewController.CollectionView.BackgroundColor = UIColor.Clear;
|
||||||
_collectionViewController.CollectionView.WeakDelegate = _layout;
|
_collectionViewController.CollectionView.WeakDelegate = _flowLayout;
|
||||||
_collectionViewController.UpdateEmptyView();
|
_collectionViewController.UpdateEmptyView();
|
||||||
|
|
||||||
// Listen for ScrollTo requests
|
// Listen for ScrollTo requests
|
||||||
|
@ -123,7 +125,7 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectionViewController.CollectionView.ScrollToItem(indexPath,
|
_collectionViewController.CollectionView.ScrollToItem(indexPath,
|
||||||
args.ScrollToPosition.ToCollectionViewScrollPosition(_layout.ScrollDirection),
|
args.ScrollToPosition.ToCollectionViewScrollPosition(_flowLayout.ScrollDirection),
|
||||||
args.IsAnimated);
|
args.IsAnimated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using CoreGraphics;
|
using CoreGraphics;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
using Xamarin.Forms.Internals;
|
||||||
|
|
||||||
namespace Xamarin.Forms.Platform.iOS
|
namespace Xamarin.Forms.Platform.iOS
|
||||||
{
|
{
|
||||||
|
@ -215,5 +217,102 @@ namespace Xamarin.Forms.Platform.iOS
|
||||||
{
|
{
|
||||||
_needCellSizeUpdate = true;
|
_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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -129,6 +129,7 @@
|
||||||
<Compile Include="CollectionView\ListViewLayout.cs" />
|
<Compile Include="CollectionView\ListViewLayout.cs" />
|
||||||
<Compile Include="CollectionView\ObservableItemsSource.cs" />
|
<Compile Include="CollectionView\ObservableItemsSource.cs" />
|
||||||
<Compile Include="CollectionView\PropertyChangedEventArgsExtensions.cs" />
|
<Compile Include="CollectionView\PropertyChangedEventArgsExtensions.cs" />
|
||||||
|
<Compile Include="CollectionView\SnapHelpers.cs" />
|
||||||
<Compile Include="CollectionView\TemplatedCell.cs" />
|
<Compile Include="CollectionView\TemplatedCell.cs" />
|
||||||
<Compile Include="CollectionView\HorizontalTemplatedCell.cs" />
|
<Compile Include="CollectionView\HorizontalTemplatedCell.cs" />
|
||||||
<Compile Include="CollectionView\VerticalTemplatedCell.cs" />
|
<Compile Include="CollectionView\VerticalTemplatedCell.cs" />
|
||||||
|
|
Загрузка…
Ссылка в новой задаче