From 03afb32d2b37ca3d1ad868dc5c9de2868ff3098d Mon Sep 17 00:00:00 2001 From: adrianknight89 Date: Thu, 25 Jul 2019 08:07:28 -0500 Subject: [PATCH] [Android/iOS] RemainingItemsThreshold and Scrolled implementation for CollectionView (#5754) * infinite scroll capability * remove added line * scroll event implementation * add offset calculation for GridLayoutManager * removed custom layout managers * renamed variables * changed comment * implement CenterItemIndex * fixed pageSize * removed artifact * handle the case when the layout manager is not linear * fix comment * call base dispose * code review changes * remove unused references * indentation fix * fix compilation issues * revert cleanup * fix * fix crash * fix test * fix index issue * removed unused method * fix * moved private variables into app scope * Added back UI test * Name to AutomationID * added command parameter and addressed minor suggestions --- .../Github5623.xaml | 39 ++++ .../Github5623.xaml.cs | 196 ++++++++++++++++++ ...rin.Forms.Controls.Issues.Shared.projitems | 11 +- Xamarin.Forms.Core/Items/ItemsView.cs | 60 +++++- .../Items/ItemsViewScrolledEventArgs.cs | 21 ++ .../CollectionView/EmptyViewAdapter.cs | 2 +- .../CollectionView/ItemsViewAdapter.cs | 2 +- .../CollectionView/ItemsViewRenderer.cs | 17 +- .../PositionalSmoothScroller.cs | 2 +- .../RecyclerViewScrollListener.cs | 120 +++++++++++ .../CollectionView/SnapManager.cs | 2 +- .../Xamarin.Forms.Platform.Android.csproj | 1 + .../CollectionView/ItemsViewController.cs | 2 +- .../UICollectionViewDelegator.cs | 72 ++++++- 14 files changed, 525 insertions(+), 22 deletions(-) create mode 100644 Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github5623.xaml create mode 100644 Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github5623.xaml.cs create mode 100644 Xamarin.Forms.Core/Items/ItemsViewScrolledEventArgs.cs create mode 100644 Xamarin.Forms.Platform.Android/CollectionView/RecyclerViewScrollListener.cs diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github5623.xaml b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github5623.xaml new file mode 100644 index 000000000..aa478d97a --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github5623.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github5623.xaml.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github5623.xaml.cs new file mode 100644 index 000000000..e810fd2d2 --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github5623.xaml.cs @@ -0,0 +1,196 @@ +using System.Collections.ObjectModel; +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; +using System; +using System.Security.Cryptography; +using Xamarin.Forms.Xaml; +using System.Threading; +using System.Collections.Generic; +using System.Threading.Tasks; + +#if UITEST +using Xamarin.UITest; +using Xamarin.UITest.Queries; +using NUnit.Framework; +using Xamarin.Forms.Core.UITests; +using System.Linq; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ +#if UITEST + [Category(UITestCategories.CollectionView)] +#endif +#if APP + [XamlCompilation(XamlCompilationOptions.Compile)] +#endif + [Preserve(AllMembers = true)] + [Issue(IssueTracker.Github, 5623, "CollectionView with Incremental Collection (RemainingItemsThreshold)", PlatformAffected.All)] + public partial class Github5623 : TestContentPage + { +#if APP + int _itemCount = 10; + const int MaximumItemCount = 100; + const int PageSize = 10; + static readonly SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(1, 1); + + public Github5623() + { + Device.SetFlags(new List { CollectionView.CollectionViewExperimental }); + + InitializeComponent(); + + BindingContext = new ViewModel5623(); + } + + async void CollectionView_RemainingItemsThresholdReached(object sender, System.EventArgs e) + { + await SemaphoreSlim.WaitAsync(); + try + { + var itemsSource = (sender as CollectionView).ItemsSource as ObservableCollection; + var nextSet = await GetNextSetAsync(); + + // nothing to add + if (nextSet.Count == 0) + return; + + Device.BeginInvokeOnMainThread(() => + { + foreach (var item in nextSet) + { + itemsSource.Add(item); + } + }); + + System.Diagnostics.Debug.WriteLine("Count: " + itemsSource.Count); + } + finally + { + SemaphoreSlim.Release(); + } + } + + void CollectionView_OnScrolled(object sender, ItemsViewScrolledEventArgs e) + { + Label1.Text = "HorizontalDelta: " + e.HorizontalDelta; + Label2.Text = "VerticalDelta: " + e.VerticalDelta; + Label3.Text = "HorizontalOffset: " + e.HorizontalOffset; + Label4.Text = "VerticalOffset: " + e.VerticalOffset; + Label5.Text = "FirstVisibleItemIndex: " + e.FirstVisibleItemIndex; + Label6.Text = "CenterItemIndex: " + e.CenterItemIndex; + Label7.Text = "LastVisibleItemIndex: " + e.LastVisibleItemIndex; + } + + async Task> GetNextSetAsync() + { + return await Task.Run(() => + { + var collection = new ObservableCollection(); + var count = PageSize; + + if (_itemCount + count > MaximumItemCount) + count = MaximumItemCount - _itemCount; + + for (var i = _itemCount; i < _itemCount + count; i++) + { + collection.Add(new Model5623((BindingContext as ViewModel5623).ItemSizingStrategy == ItemSizingStrategy.MeasureAllItems) + { + Text = i.ToString(), + BackgroundColor = i % 2 == 0 ? Color.AntiqueWhite : Color.Lavender + }); + } + + _itemCount += count; + + return collection; + }); + } +#endif + + protected override void Init() + { + + } + +#if UITEST + [Test] + public void CollectionViewInfiniteScroll() + { + RunningApp.WaitForElement ("CollectionView5623"); + + var colView = RunningApp.Query("CollectionView5623").Single(); + + AppResult[] lastCellResults = null; + + RunningApp.RetryUntilPresent(() => + { + RunningApp.DragCoordinates(colView.Rect.CenterX, colView.Rect.Y + colView.Rect.Height - 50, colView.Rect.CenterX, colView.Rect.Y + 5); + + lastCellResults = RunningApp.Query("99"); + + return lastCellResults; + }, 100, 1); + + Assert.IsTrue(lastCellResults?.Any() ?? false); + } +#endif + } + + [Preserve(AllMembers = true)] + public class ViewModel5623 + { + public ObservableCollection Items { get; set; } + + public Command RemainingItemsThresholdReachedCommand { get; set; } + + public ItemSizingStrategy ItemSizingStrategy { get; set; } = ItemSizingStrategy.MeasureAllItems; + + public ViewModel5623() + { + var collection = new ObservableCollection(); + var pageSize = 10; + + for (var i = 0; i < pageSize; i++) + { + collection.Add(new Model5623(ItemSizingStrategy == ItemSizingStrategy.MeasureAllItems) + { + Text = i.ToString(), + BackgroundColor = i % 2 == 0 ? Color.AntiqueWhite : Color.Lavender + }); + } + + Items = collection; + + RemainingItemsThresholdReachedCommand = new Command(() => + { + System.Diagnostics.Debug.WriteLine($"{nameof(RemainingItemsThresholdReachedCommand)} called"); + }); + } + } + + [Preserve(AllMembers = true)] + public class Model5623 + { + RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider(); + + public string Text { get; set; } + + public Color BackgroundColor { get; set; } + + public int Height { get; set; } = 200; + + public string HeightText { get; private set; } + + public Model5623(bool isUneven) + { + var byteArray = new byte[4]; + provider.GetBytes(byteArray); + + if (isUneven) + Height = 100 + (BitConverter.ToInt32(byteArray, 0) % 300 + 300) % 300; + + HeightText = "(Height: " + Height + ")"; + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index 3d85fefb7..a0396e78f 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -25,6 +25,10 @@ + + Github5623.xaml + Code + @@ -480,7 +484,6 @@ Code - Issue5057.xaml Code @@ -1304,4 +1307,10 @@ MSBuild:Compile + + + Designer + MSBuild:Compile + + \ No newline at end of file diff --git a/Xamarin.Forms.Core/Items/ItemsView.cs b/Xamarin.Forms.Core/Items/ItemsView.cs index f4c13baf7..833553008 100644 --- a/Xamarin.Forms.Core/Items/ItemsView.cs +++ b/Xamarin.Forms.Core/Items/ItemsView.cs @@ -2,8 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Text.RegularExpressions; +using System.Windows.Input; using Xamarin.Forms.Internals; namespace Xamarin.Forms @@ -44,6 +43,23 @@ namespace Xamarin.Forms set => SetValue(ItemsSourceProperty, value); } + public static readonly BindableProperty RemainingItemsThresholdReachedCommandProperty = + BindableProperty.Create(nameof(RemainingItemsThresholdReachedCommand), typeof(ICommand), typeof(ItemsView), null); + + public ICommand RemainingItemsThresholdReachedCommand + { + get => (ICommand)GetValue(RemainingItemsThresholdReachedCommandProperty); + set => SetValue(RemainingItemsThresholdReachedCommandProperty, value); + } + + public static readonly BindableProperty RemainingItemsThresholdReachedCommandParameterProperty = BindableProperty.Create(nameof(RemainingItemsThresholdReachedCommandParameter), typeof(object), typeof(ItemsView), default(object)); + + public object RemainingItemsThresholdReachedCommandParameter + { + get => GetValue(RemainingItemsThresholdReachedCommandParameterProperty); + set => SetValue(RemainingItemsThresholdReachedCommandParameterProperty, value); + } + public static readonly BindableProperty HorizontalScrollBarVisibilityProperty = BindableProperty.Create( nameof(HorizontalScrollBarVisibility), typeof(ScrollBarVisibility), @@ -69,6 +85,15 @@ namespace Xamarin.Forms set => SetValue(VerticalScrollBarVisibilityProperty, value); } + public static readonly BindableProperty RemainingItemsThresholdProperty = + BindableProperty.Create(nameof(RemainingItemsThreshold), typeof(int), typeof(ItemsView), -1, validateValue: (bindable, value) => (int)value >= -1); + + public int RemainingItemsThreshold + { + get => (int)GetValue(RemainingItemsThresholdProperty); + set => SetValue(RemainingItemsThresholdProperty, value); + } + public void AddLogicalChild(Element element) { _logicalChildren.Add(element); @@ -142,8 +167,29 @@ namespace Xamarin.Forms OnScrollToRequested(new ScrollToRequestEventArgs(item, group, position, animate)); } + public void SendRemainingItemsThresholdReached() + { + RemainingItemsThresholdReached?.Invoke(this, EventArgs.Empty); + + if (RemainingItemsThresholdReachedCommand?.CanExecute(RemainingItemsThresholdReachedCommandParameter) == true) + RemainingItemsThresholdReachedCommand?.Execute(RemainingItemsThresholdReachedCommandParameter); + + OnRemainingItemsThresholdReached(); + } + + public void SendScrolled(ItemsViewScrolledEventArgs e) + { + Scrolled?.Invoke(this, e); + + OnScrolled(e); + } + public event EventHandler ScrollToRequested; + public event EventHandler Scrolled; + + public event EventHandler RemainingItemsThresholdReached; + protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) { // TODO hartez 2018-05-22 05:04 PM This 40,40 is what LV1 does; can we come up with something less arbitrary? @@ -161,5 +207,15 @@ namespace Xamarin.Forms { ScrollToRequested?.Invoke(this, e); } + + protected virtual void OnRemainingItemsThresholdReached() + { + + } + + protected virtual void OnScrolled(ItemsViewScrolledEventArgs e) + { + + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Core/Items/ItemsViewScrolledEventArgs.cs b/Xamarin.Forms.Core/Items/ItemsViewScrolledEventArgs.cs new file mode 100644 index 000000000..ef309ecea --- /dev/null +++ b/Xamarin.Forms.Core/Items/ItemsViewScrolledEventArgs.cs @@ -0,0 +1,21 @@ +using System; + +namespace Xamarin.Forms +{ + public class ItemsViewScrolledEventArgs : EventArgs + { + public double HorizontalDelta { get; set; } + + public double VerticalDelta { get; set; } + + public double HorizontalOffset { get; set; } + + public double VerticalOffset { get; set; } + + public int FirstVisibleItemIndex { get; set; } + + public int CenterItemIndex { get; set; } + + public int LastVisibleItemIndex { get; set; } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/CollectionView/EmptyViewAdapter.cs b/Xamarin.Forms.Platform.Android/CollectionView/EmptyViewAdapter.cs index 9faeb7cd7..881c09807 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/EmptyViewAdapter.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/EmptyViewAdapter.cs @@ -43,7 +43,7 @@ namespace Xamarin.Forms.Platform.Android public EmptyViewAdapter(ItemsView itemsView) { - CollectionView.VerifyCollectionViewFlagEnabled(nameof(EmptyViewAdapter)); + Xamarin.Forms.CollectionView.VerifyCollectionViewFlagEnabled(nameof(EmptyViewAdapter)); ItemsView = itemsView; } diff --git a/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewAdapter.cs b/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewAdapter.cs index c7dc5738e..375766d85 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewAdapter.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewAdapter.cs @@ -21,7 +21,7 @@ namespace Xamarin.Forms.Platform.Android internal ItemsViewAdapter(ItemsView itemsView, Func createItemContentView = null) { - CollectionView.VerifyCollectionViewFlagEnabled(nameof(ItemsViewAdapter)); + Xamarin.Forms.CollectionView.VerifyCollectionViewFlagEnabled(nameof(ItemsViewAdapter)); ItemsView = itemsView; _createItemContentView = createItemContentView; diff --git a/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewRenderer.cs b/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewRenderer.cs index d02f40036..caf7f35f8 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewRenderer.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewRenderer.cs @@ -1,13 +1,11 @@ using System; using System.ComponentModel; -using System.Linq; using Android.Content; using Android.Graphics; using Android.Support.V7.Widget; -using Android.Util; using Android.Views; -using Android.Widget; using Xamarin.Forms.Internals; +using Xamarin.Forms.Platform.Android.CollectionView; using Xamarin.Forms.Platform.Android.FastRenderers; using AViewCompat = Android.Support.V4.View.ViewCompat; @@ -28,6 +26,7 @@ namespace Xamarin.Forms.Platform.Android IItemsLayout _layout; SnapManager _snapManager; ScrollHelper _scrollHelper; + RecyclerViewScrollListener _recyclerViewScrollListener; EmptyViewAdapter _emptyViewAdapter; readonly DataChangeObserver _emptyCollectionObserver; @@ -40,7 +39,7 @@ namespace Xamarin.Forms.Platform.Android public ItemsViewRenderer(Context context) : base(new ContextThemeWrapper(context, Resource.Style.collectionViewStyle)) { - CollectionView.VerifyCollectionViewFlagEnabled(nameof(ItemsViewRenderer)); + Xamarin.Forms.CollectionView.VerifyCollectionViewFlagEnabled(nameof(ItemsViewRenderer)); _automationPropertiesProvider = new AutomationPropertiesProvider(this); _effectControlProvider = new EffectControlProvider(this); @@ -305,6 +304,9 @@ namespace Xamarin.Forms.Platform.Android // Listen for ScrollTo requests ItemsView.ScrollToRequested += ScrollToRequested; + + _recyclerViewScrollListener = new RecyclerViewScrollListener(ItemsView, ItemsViewAdapter); + AddOnScrollListener(_recyclerViewScrollListener); } void UpdateVerticalScrollBarVisibility() @@ -353,6 +355,13 @@ namespace Xamarin.Forms.Platform.Android // Stop listening for ScrollTo requests oldElement.ScrollToRequested -= ScrollToRequested; + if (_recyclerViewScrollListener != null) + { + _recyclerViewScrollListener.Dispose(); + ClearOnScrollListeners(); + _recyclerViewScrollListener = null; + } + if (ItemsViewAdapter != null) { // Stop watching for empty items or scroll adjustments diff --git a/Xamarin.Forms.Platform.Android/CollectionView/PositionalSmoothScroller.cs b/Xamarin.Forms.Platform.Android/CollectionView/PositionalSmoothScroller.cs index 6899ef579..8f97d9b9f 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/PositionalSmoothScroller.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/PositionalSmoothScroller.cs @@ -9,7 +9,7 @@ namespace Xamarin.Forms.Platform.Android public PositionalSmoothScroller(Context context, ScrollToPosition scrollToPosition) : base(context) { - CollectionView.VerifyCollectionViewFlagEnabled(nameof(PositionalSmoothScroller)); + Xamarin.Forms.CollectionView.VerifyCollectionViewFlagEnabled(nameof(PositionalSmoothScroller)); _scrollToPosition = scrollToPosition; } diff --git a/Xamarin.Forms.Platform.Android/CollectionView/RecyclerViewScrollListener.cs b/Xamarin.Forms.Platform.Android/CollectionView/RecyclerViewScrollListener.cs new file mode 100644 index 000000000..540469b57 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/CollectionView/RecyclerViewScrollListener.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Linq; +using Android.Graphics; +using Android.Support.V7.Widget; + +namespace Xamarin.Forms.Platform.Android.CollectionView +{ + public class RecyclerViewScrollListener : RecyclerView.OnScrollListener + { + bool _disposed; + int _horizontalOffset, _verticalOffset; + ItemsView _itemsView; + ItemsViewAdapter _itemsViewAdapter; + + public RecyclerViewScrollListener(ItemsView itemsView, ItemsViewAdapter itemsViewAdapter) + { + _itemsView = itemsView; + _itemsViewAdapter = itemsViewAdapter; + } + + public override void OnScrolled(RecyclerView recyclerView, int dx, int dy) + { + base.OnScrolled(recyclerView, dx, dy); + + // TODO: These offsets will be incorrect upon row size or count change. + // They are currently provided in place of LayoutManager's default offset calculation + // because it does not report accurate values in the presence of uneven rows. + // See https://stackoverflow.com/questions/27507715/android-how-to-get-the-current-x-offset-of-recyclerview + _horizontalOffset += dx; + _verticalOffset += dy; + + var firstVisibleItemIndex = -1; + var lastVisibleItemIndex = -1; + var centerItemIndex = -1; + + if (recyclerView.GetLayoutManager() is LinearLayoutManager linearLayoutManager) + { + firstVisibleItemIndex = linearLayoutManager.FindFirstVisibleItemPosition(); + lastVisibleItemIndex = linearLayoutManager.FindLastVisibleItemPosition(); + centerItemIndex = CalculateCenterItemIndex(firstVisibleItemIndex, lastVisibleItemIndex, linearLayoutManager); + } + + var itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs + { + HorizontalDelta = dx, + VerticalDelta = dy, + HorizontalOffset = _horizontalOffset, + VerticalOffset = _verticalOffset, + FirstVisibleItemIndex = firstVisibleItemIndex, + CenterItemIndex = centerItemIndex, + LastVisibleItemIndex = lastVisibleItemIndex + }; + + _itemsView.SendScrolled(itemsViewScrolledEventArgs); + + // Don't send RemainingItemsThresholdReached event for non-linear layout managers + // This can also happen if a layout pass has not happened yet + if (lastVisibleItemIndex == -1) + return; + + switch (_itemsView.RemainingItemsThreshold) + { + case -1: + return; + case 0: + if (lastVisibleItemIndex == _itemsViewAdapter.ItemCount - 1) + _itemsView.SendRemainingItemsThresholdReached(); + break; + default: + if (_itemsViewAdapter.ItemCount - 1 - lastVisibleItemIndex <= _itemsView.RemainingItemsThreshold) + _itemsView.SendRemainingItemsThresholdReached(); + break; + } + } + + static int CalculateCenterItemIndex(int firstVisibleItemIndex, int lastVisibleItemIndex, LinearLayoutManager linearLayoutManager) + { + // This can happen if a layout pass has not happened yet + if (firstVisibleItemIndex == -1) + return firstVisibleItemIndex; + + var keyValuePairs = new Dictionary(); + for (var i = firstVisibleItemIndex; i <= lastVisibleItemIndex; i++) + { + var view = linearLayoutManager.FindViewByPosition(i); + var rect = new Rect(); + + view.GetLocalVisibleRect(rect); + keyValuePairs[i] = rect.Height(); + } + + var center = keyValuePairs.Values.Sum() / 2.0; + foreach (var keyValuePair in keyValuePairs) + { + center -= keyValuePair.Value; + + if (center <= 0) + return keyValuePair.Key; + } + + return firstVisibleItemIndex; + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _itemsView = null; + _itemsViewAdapter = null; + } + + _disposed = true; + + base.Dispose(disposing); + } + } +} \ 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 a4d0e6e8c..99dd5c784 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/SnapManager.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/SnapManager.cs @@ -11,7 +11,7 @@ namespace Xamarin.Forms.Platform.Android public SnapManager(ItemsView itemsView, RecyclerView recyclerView) { - CollectionView.VerifyCollectionViewFlagEnabled(nameof(SnapManager)); + Xamarin.Forms.CollectionView.VerifyCollectionViewFlagEnabled(nameof(SnapManager)); _recyclerView = recyclerView; _itemsView = itemsView; } diff --git a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj index 990eeb95c..1c6de5e8d 100644 --- a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj +++ b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj @@ -72,6 +72,7 @@ + diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs index 2a1724acf..da5ecc44b 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs @@ -9,7 +9,7 @@ namespace Xamarin.Forms.Platform.iOS // TODO hartez 2018/06/01 14:21:24 Add a method for updating the layout public class ItemsViewController : UICollectionViewController { - protected IItemsViewSource ItemsSource { get; set; } + public IItemsViewSource ItemsSource { get; protected set; } public ItemsView ItemsView { get; } protected ItemsViewLayout ItemsViewLayout { get; set; } bool _initialConstraintsSet; diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/UICollectionViewDelegator.cs b/Xamarin.Forms.Platform.iOS/CollectionView/UICollectionViewDelegator.cs index bfa45c878..2d31e2017 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/UICollectionViewDelegator.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/UICollectionViewDelegator.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using CoreGraphics; using Foundation; using UIKit; @@ -7,17 +8,13 @@ namespace Xamarin.Forms.Platform.iOS { public class UICollectionViewDelegator : UICollectionViewDelegateFlowLayout { - public ItemsViewLayout ItemsViewLayout { get; private set; } - public ItemsViewController ItemsViewController { get; private set; } - public SelectableItemsViewController SelectableItemsViewController - { - get => ItemsViewController as SelectableItemsViewController; - } + float _previousHorizontalOffset, _previousVerticalOffset; - public GroupableItemsViewController GroupableItemsViewController - { - get => ItemsViewController as GroupableItemsViewController; - } + public ItemsViewLayout ItemsViewLayout { get; } + public ItemsViewController ItemsViewController { get; } + public SelectableItemsViewController SelectableItemsViewController => ItemsViewController as SelectableItemsViewController; + + public GroupableItemsViewController GroupableItemsViewController => ItemsViewController as GroupableItemsViewController; public UICollectionViewDelegator(ItemsViewLayout itemsViewLayout, ItemsViewController itemsViewController) { @@ -25,6 +22,61 @@ namespace Xamarin.Forms.Platform.iOS ItemsViewController = itemsViewController; } + public override void DraggingStarted(UIScrollView scrollView) + { + _previousHorizontalOffset = (float)scrollView.ContentOffset.X; + _previousVerticalOffset = (float)scrollView.ContentOffset.Y; + } + + public override void DraggingEnded(UIScrollView scrollView, bool willDecelerate) + { + _previousHorizontalOffset = 0; + _previousVerticalOffset = 0; + } + + public override void Scrolled(UIScrollView scrollView) + { + var indexPathsForVisibleItems = ItemsViewController.CollectionView.IndexPathsForVisibleItems.OrderBy(x => x.Row).ToList(); + + if (indexPathsForVisibleItems.Count == 0) + return; + + var firstVisibleItemIndex = (int)indexPathsForVisibleItems.First().Item; + var centerPoint = new CGPoint(ItemsViewController.CollectionView.Center.X + ItemsViewController.CollectionView.ContentOffset.X, ItemsViewController.CollectionView.Center.Y + ItemsViewController.CollectionView.ContentOffset.Y); + var centerIndexPath = ItemsViewController.CollectionView.IndexPathForItemAtPoint(centerPoint); + var centerItemIndex = centerIndexPath?.Row ?? firstVisibleItemIndex; + var lastVisibleItemIndex = (int)indexPathsForVisibleItems.Last().Item; + var itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs + { + HorizontalDelta = scrollView.ContentOffset.X - _previousHorizontalOffset, + VerticalDelta = scrollView.ContentOffset.Y - _previousVerticalOffset, + HorizontalOffset = scrollView.ContentOffset.X, + VerticalOffset = scrollView.ContentOffset.Y, + FirstVisibleItemIndex = firstVisibleItemIndex, + CenterItemIndex = centerItemIndex, + LastVisibleItemIndex = lastVisibleItemIndex + }; + + ItemsViewController.ItemsView.SendScrolled(itemsViewScrolledEventArgs); + + _previousHorizontalOffset = (float)scrollView.ContentOffset.X; + _previousVerticalOffset = (float)scrollView.ContentOffset.Y; + + switch (ItemsViewController.ItemsView.RemainingItemsThreshold) + { + case -1: + return; + case 0: + if (lastVisibleItemIndex == ItemsViewController.ItemsSource.ItemCount - 1) + ItemsViewController.ItemsView.SendRemainingItemsThresholdReached(); + break; + default: + if (ItemsViewController.ItemsSource.ItemCount - 1 - lastVisibleItemIndex <= ItemsViewController.ItemsView.RemainingItemsThreshold) + ItemsViewController.ItemsView.SendRemainingItemsThresholdReached(); + break; + } + } + public override UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout, nint section) {