[Android/iOS] RemainingItemsThreshold and Scrolled implementation for CollectionView...
authoradrianknight89 <adrianknight89@outlook.com>
Thu, 25 Jul 2019 13:07:28 +0000 (08:07 -0500)
committerGerald Versluis <gerald.versluis@microsoft.com>
Thu, 25 Jul 2019 13:07:28 +0000 (15:07 +0200)
* 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

14 files changed:
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github5623.xaml [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github5623.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
Xamarin.Forms.Core/Items/ItemsView.cs
Xamarin.Forms.Core/Items/ItemsViewScrolledEventArgs.cs [new file with mode: 0644]
Xamarin.Forms.Platform.Android/CollectionView/EmptyViewAdapter.cs
Xamarin.Forms.Platform.Android/CollectionView/ItemsViewAdapter.cs
Xamarin.Forms.Platform.Android/CollectionView/ItemsViewRenderer.cs
Xamarin.Forms.Platform.Android/CollectionView/PositionalSmoothScroller.cs
Xamarin.Forms.Platform.Android/CollectionView/RecyclerViewScrollListener.cs [new file with mode: 0644]
Xamarin.Forms.Platform.Android/CollectionView/SnapManager.cs
Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj
Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs
Xamarin.Forms.Platform.iOS/CollectionView/UICollectionViewDelegator.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 (file)
index 0000000..aa478d9
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<controls:TestContentPage
+    xmlns="http://xamarin.com/schemas/2014/forms"
+    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+    xmlns:controls="clr-namespace:Xamarin.Forms.Controls"
+    x:Class="Xamarin.Forms.Controls.Issues.Github5623">
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="200"/>
+            <RowDefinition/>
+        </Grid.RowDefinitions>
+
+        <StackLayout Orientation="Vertical" Spacing="5" Grid.Row="0" VerticalOptions="Center">
+            <Label x:Name="Label" LineBreakMode="WordWrap" Text="Scroll down until you hit 99" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
+            <Label x:Name="Label1" LineBreakMode="WordWrap" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
+            <Label x:Name="Label2" LineBreakMode="WordWrap" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
+            <Label x:Name="Label3" LineBreakMode="WordWrap" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
+            <Label x:Name="Label4" LineBreakMode="WordWrap" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
+            <Label x:Name="Label5" LineBreakMode="WordWrap" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
+            <Label x:Name="Label6" LineBreakMode="WordWrap" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
+            <Label x:Name="Label7" LineBreakMode="WordWrap" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
+        </StackLayout>
+
+        <CollectionView Grid.Row="1" AutomationId="CollectionView5623" ItemSizingStrategy="{Binding ItemSizingStrategy}" ItemsSource="{Binding Items}" Scrolled="CollectionView_OnScrolled" RemainingItemsThreshold="25" RemainingItemsThresholdReached="CollectionView_RemainingItemsThresholdReached" RemainingItemsThresholdReachedCommand="{Binding RemainingItemsThresholdReachedCommand}">
+            <CollectionView.ItemTemplate>
+                <DataTemplate>
+                    <Grid HeightRequest="{Binding Height}" BackgroundColor="{Binding BackgroundColor}">
+                        <StackLayout Spacing="10" HorizontalOptions="Center" VerticalOptions="Center">
+                            <Label Text="{Binding Text}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
+                            <Label Text="{Binding HeightText}" FontSize="Micro" HorizontalTextAlignment="Center" VerticalTextAlignment="End"/>
+                        </StackLayout>
+                    </Grid>
+                </DataTemplate>
+            </CollectionView.ItemTemplate>
+        </CollectionView>
+
+        <BoxView Grid.Row="1" HorizontalOptions="FillAndExpand" VerticalOptions="Center" BackgroundColor="Red" HeightRequest="5"/>
+    </Grid>
+</controls:TestContentPage>
\ 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 (file)
index 0000000..e810fd2
--- /dev/null
@@ -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<string> { 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<Model5623>;
+                               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<ObservableCollection<Model5623>> GetNextSetAsync()
+               {
+                       return await Task.Run(() =>
+                       {
+                               var collection = new ObservableCollection<Model5623>();
+                               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<Model5623> Items { get; set; }
+
+               public Command RemainingItemsThresholdReachedCommand { get; set; }
+
+               public ItemSizingStrategy ItemSizingStrategy { get; set; } = ItemSizingStrategy.MeasureAllItems;
+
+               public ViewModel5623()
+               {
+                       var collection = new ObservableCollection<Model5623>();
+                       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
index 3d85fef..a0396e7 100644 (file)
     <Compile Include="$(MSBuildThisFileDirectory)Issue6258.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Issue3150.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Issue6262.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)Github5623.xaml.cs">
+      <DependentUpon>Github5623.xaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla59172.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)FlagTestHelpers.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Issue6260.cs" />
       <SubType>Code</SubType>
     </Compile>
     <Compile Include="$(MSBuildThisFileDirectory)Issue4600.cs" />
-    <Compile Include="$(MSBuildThisFileDirectory)Issue5252.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Issue5057.xaml.cs">
       <DependentUpon>Issue5057.xaml</DependentUpon>
       <SubType>Code</SubType>
       <Generator>MSBuild:Compile</Generator>
     </EmbeddedResource>
   </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="$(MSBuildThisFileDirectory)Github5623.xaml">
+      <SubType>Designer</SubType>
+      <Generator>MSBuild:Compile</Generator>
+    </EmbeddedResource>
+  </ItemGroup>
 </Project>
\ No newline at end of file
index f4c13ba..8335530 100644 (file)
@@ -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<ScrollToRequestEventArgs> ScrollToRequested;
 
+               public event EventHandler<ItemsViewScrolledEventArgs> 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 (file)
index 0000000..ef309ec
--- /dev/null
@@ -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
index 9faeb7c..881c098 100644 (file)
@@ -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;
                }
 
index c7dc573..375766d 100644 (file)
@@ -21,7 +21,7 @@ namespace Xamarin.Forms.Platform.Android
 
                internal ItemsViewAdapter(ItemsView itemsView, Func<View, Context, ItemContentView> createItemContentView = null)
                {
-                       CollectionView.VerifyCollectionViewFlagEnabled(nameof(ItemsViewAdapter));
+                       Xamarin.Forms.CollectionView.VerifyCollectionViewFlagEnabled(nameof(ItemsViewAdapter));
 
                        ItemsView = itemsView;
                        _createItemContentView = createItemContentView;
index d02f400..caf7f35 100644 (file)
@@ -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
index 6899ef5..8f97d9b 100644 (file)
@@ -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 (file)
index 0000000..540469b
--- /dev/null
@@ -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<int, int>();
+                       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
index a4d0e6e..99dd5c7 100644 (file)
@@ -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;
                }
index 990eeb9..1c6de5e 100644 (file)
@@ -72,6 +72,7 @@
     <Compile Include="CollectionView\DataChangeObserver.cs" />
     <Compile Include="CollectionView\EmptySource.cs" />
     <Compile Include="CollectionView\NongreedySnapHelper.cs" />
+    <Compile Include="CollectionView\RecyclerViewScrollListener.cs" />
     <Compile Include="CollectionView\SingleSnapHelper.cs" />
     <Compile Include="CollectionView\EmptyViewAdapter.cs" />
     <Compile Include="CollectionView\EndSingleSnapHelper.cs" />
index 2a1724a..da5ecc4 100644 (file)
@@ -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;
index bfa45c8..2d31e20 100644 (file)
@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 using CoreGraphics;
 using Foundation;
 using UIKit;
@@ -7,22 +8,73 @@ namespace Xamarin.Forms.Platform.iOS
 {
        public class UICollectionViewDelegator : UICollectionViewDelegateFlowLayout
        {
-               public ItemsViewLayout ItemsViewLayout { get; private set; }
-               public ItemsViewController ItemsViewController { get; private set; }
-               public SelectableItemsViewController SelectableItemsViewController
+               float _previousHorizontalOffset, _previousVerticalOffset;
+
+               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)
                {
-                       get => ItemsViewController as SelectableItemsViewController;
+                       ItemsViewLayout = itemsViewLayout;
+                       ItemsViewController = itemsViewController;
                }
 
-               public GroupableItemsViewController GroupableItemsViewController
+               public override void DraggingStarted(UIScrollView scrollView)
                {
-                       get => ItemsViewController as GroupableItemsViewController;
+                       _previousHorizontalOffset = (float)scrollView.ContentOffset.X;
+                       _previousVerticalOffset = (float)scrollView.ContentOffset.Y;
                }
 
-               public UICollectionViewDelegator(ItemsViewLayout itemsViewLayout, ItemsViewController itemsViewController)
+               public override void DraggingEnded(UIScrollView scrollView, bool willDecelerate)
                {
-                       ItemsViewLayout = itemsViewLayout;
-                       ItemsViewController = itemsViewController;
+                       _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,