Implement ItemsUpdatingScrollMode on Android and iOS (#6879)
authorE.Z. Hart <hartez@users.noreply.github.com>
Tue, 16 Jul 2019 17:40:26 +0000 (11:40 -0600)
committerGitHub <noreply@github.com>
Tue, 16 Jul 2019 17:40:26 +0000 (11:40 -0600)
17 files changed:
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewItemsUpdatingScrollMode.cs [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/CollectionViewGallery.cs
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/EnumSelector.cs
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ScrollModeGalleries/ScrollModeGallery.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ScrollModeGalleries/ScrollModeTestGallery.xaml [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ScrollModeGalleries/ScrollModeTestGallery.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj
Xamarin.Forms.Core/Items/ItemsUpdatingScrollMode.cs [new file with mode: 0644]
Xamarin.Forms.Core/Items/ItemsView.cs
Xamarin.Forms.Platform.Android/CollectionView/DataChangeObserver.cs
Xamarin.Forms.Platform.Android/CollectionView/ItemsViewRenderer.cs
Xamarin.Forms.Platform.Android/CollectionView/ScrollHelper.cs
Xamarin.Forms.Platform.iOS/CollectionView/IndexPathExtensions.cs [new file with mode: 0644]
Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs
Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewRenderer.cs
Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj

diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewItemsUpdatingScrollMode.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewItemsUpdatingScrollMode.cs
new file mode 100644 (file)
index 0000000..2d6896b
--- /dev/null
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xamarin.Forms.CustomAttributes;
+using Xamarin.Forms.Internals;
+
+#if UITEST
+using Xamarin.Forms.Core.UITests;
+using Xamarin.UITest;
+using NUnit.Framework;
+#endif
+
+namespace Xamarin.Forms.Controls.Issues
+{
+#if UITEST
+       [Category(UITestCategories.CollectionView)]
+#endif
+       [Preserve(AllMembers = true)]
+       [Issue(IssueTracker.None, 8888888, "CollectionView ItemsUpdatingScrollMode", PlatformAffected.All)]
+       public class CollectionViewItemsUpdatingScrollMode : TestNavigationPage
+       {
+               protected override void Init()
+               {
+#if APP
+                       Device.SetFlags(new List<string>(Device.Flags ?? new List<string>()) { "CollectionView_Experimental" });
+
+                       PushAsync(new GalleryPages.CollectionViewGalleries.ScrollModeGalleries.ScrollModeTestGallery());
+#endif
+               }
+
+#if UITEST
+               [Test]
+               public void KeepItemsInView()
+               {
+                       RunningApp.WaitForElement("ScrollToMiddle");
+                       RunningApp.Tap("ScrollToMiddle");       
+                       RunningApp.WaitForElement("Vegetables.jpg, 10");
+                       RunningApp.Tap("AddItemAbove"); 
+                       RunningApp.WaitForNoElement("photo.jpg, 9");
+               }
+
+               [Test]
+               public void KeepScrollOffset()
+               {
+                       RunningApp.WaitForElement("SelectScrollMode");
+                       RunningApp.Tap("SelectScrollMode");
+                       RunningApp.Tap("KeepScrollOffset");
+
+                       RunningApp.WaitForElement("ScrollToMiddle");
+                       RunningApp.Tap("ScrollToMiddle");       
+                       RunningApp.WaitForElement("Vegetables.jpg, 10");
+                       RunningApp.Tap("AddItemAbove"); 
+                       RunningApp.WaitForElement("photo.jpg, 9");
+               }
+
+               [Test]
+               public void KeepLastItemInView()
+               {
+                       RunningApp.WaitForElement("SelectScrollMode");
+                       RunningApp.Tap("SelectScrollMode");
+                       RunningApp.Tap("KeepLastItemInView");
+
+                       RunningApp.WaitForElement("ScrollToMiddle");
+                       RunningApp.Tap("ScrollToMiddle");       
+                       RunningApp.WaitForElement("Vegetables.jpg, 10");
+                       RunningApp.Tap("AddItemToEnd"); 
+                       RunningApp.WaitForElement("Added item");
+               }
+#endif
+       }
+}
index 8fa5d61..e250e29 100644 (file)
@@ -9,6 +9,7 @@
     <Import_RootNamespace>Xamarin.Forms.Controls.Issues</Import_RootNamespace>
   </PropertyGroup>
   <ItemGroup>
+    <Compile Include="$(MSBuildThisFileDirectory)CollectionViewItemsUpdatingScrollMode.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Issue5046.xaml.cs">
       <DependentUpon>Issue5046.xaml</DependentUpon>
       <SubType>Code</SubType>
index 1e06a83..285ecf1 100644 (file)
@@ -1,6 +1,7 @@
 using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.EmptyViewGalleries;
 using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries;
 using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionGalleries;
+using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.ScrollModeGalleries;
 
 namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
 {
@@ -23,6 +24,7 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
                                        GalleryBuilder.NavButton("Selection Galleries", () => new SelectionGallery(), Navigation),
                                        GalleryBuilder.NavButton("Propagation Galleries", () => new PropagationGallery(), Navigation),
                                        GalleryBuilder.NavButton("Grouping Galleries", () => new GroupingGallery(), Navigation),
+                                       GalleryBuilder.NavButton("Scroll Mode Galleries", () => new ScrollModeGallery(), Navigation),
                                }
                        };
                }
index d92c9e1..e2b0016 100644 (file)
@@ -8,7 +8,7 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
                
                readonly Picker _picker;
 
-               public EnumSelector(Func<T> getValue, Action<T> setValue)
+               public EnumSelector(Func<T> getValue, Action<T> setValue, string automationId = "")
                {
                        _setValue = setValue;
 
@@ -26,7 +26,8 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
                        {
                                WidthRequest = 200,
                                ItemsSource = source,
-                               SelectedItem = getValue().ToString()
+                               SelectedItem = getValue().ToString(),
+                               AutomationId = automationId
                        };
 
                        _picker.SelectedIndexChanged += PickerOnSelectedIndexChanged;
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ScrollModeGalleries/ScrollModeGallery.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ScrollModeGalleries/ScrollModeGallery.cs
new file mode 100644 (file)
index 0000000..a1f3780
--- /dev/null
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.ScrollModeGalleries
+{
+       internal class ScrollModeGallery : ContentPage
+       {
+               public ScrollModeGallery()
+               {
+                       var descriptionLabel =
+                                       new Label { Text = "Scroll Mode Galleries", Margin = new Thickness(2, 2, 2, 2) };
+
+                       Title = "Scroll Mode Galleries";
+
+                       Content = new ScrollView
+                       {
+                               Content = new StackLayout
+                               {
+                                       Children =
+                                       {
+                                               descriptionLabel,
+                                               GalleryBuilder.NavButton("Scroll Modes Testing", () =>
+                                                       new ScrollModeTestGallery(), Navigation)
+                                       }
+                               }
+                       };
+               }
+       }
+}
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ScrollModeGalleries/ScrollModeTestGallery.xaml b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ScrollModeGalleries/ScrollModeTestGallery.xaml
new file mode 100644 (file)
index 0000000..7ea33f0
--- /dev/null
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+             x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.ScrollModeGalleries.ScrollModeTestGallery">
+    <ContentPage.Content>
+        <Grid x:Name="Grid">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto"></RowDefinition>
+                <RowDefinition Height="Auto"></RowDefinition>
+                <RowDefinition Height="Auto"></RowDefinition>
+                <RowDefinition Height="Auto"></RowDefinition>
+                <RowDefinition Height="Auto"></RowDefinition>
+                <RowDefinition Height="*"></RowDefinition>
+            </Grid.RowDefinitions>
+
+            <Button x:Name="ScrollToMiddle" FontSize="10" AutomationId="ScrollToMiddle" Text="Scroll To Middle" 
+                    Grid.Row="1" HeightRequest="40" Clicked="ScrollToMiddle_Clicked" />
+            <Button x:Name="AddItemAbove" FontSize="10" AutomationId="AddItemAbove" Text="Add Item Above" Grid.Row="2" 
+                    HeightRequest="40" Clicked="AddItemAbove_Clicked" />
+            <Button x:Name="AddItemBelow" FontSize="10" AutomationId="AddItemBelow" Text="Add Item Below" Grid.Row="3"
+                    HeightRequest="40" Clicked="AddItemBelow_Clicked" />
+            <Button x:Name="AddItemToEnd" FontSize="10" AutomationId="AddItemToEnd" Text="Add Item To End" Grid.Row="4"
+                    HeightRequest="40" Clicked="AddItemToEnd_Clicked" />
+
+            <CollectionView x:Name="CollectionView" Grid.Row="5" />
+
+        </Grid>
+    </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ScrollModeGalleries/ScrollModeTestGallery.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ScrollModeGalleries/ScrollModeTestGallery.xaml.cs
new file mode 100644 (file)
index 0000000..a597fa7
--- /dev/null
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.ScrollModeGalleries
+{
+       [XamlCompilation(XamlCompilationOptions.Compile)]
+       public partial class ScrollModeTestGallery : ContentPage
+       {
+               readonly DemoFilteredItemSource _demoFilteredItemSource = new DemoFilteredItemSource(20);
+
+               public ScrollModeTestGallery()
+               {
+                       InitializeComponent();
+
+                       var scrollModeSelector = new EnumSelector<ItemsUpdatingScrollMode>(() => CollectionView.ItemsUpdatingScrollMode,
+                       mode => CollectionView.ItemsUpdatingScrollMode = mode, "SelectScrollMode");
+
+                       Grid.Children.Add(scrollModeSelector);
+
+                       CollectionView.ItemTemplate = ExampleTemplates.PhotoTemplate();
+                       CollectionView.ItemsSource = _demoFilteredItemSource.Items;
+               }
+
+               void ScrollToMiddle_Clicked(object sender, EventArgs e)
+               {
+                       CollectionView.ScrollTo(_demoFilteredItemSource.Items.Count / 2, position: ScrollToPosition.Start, animate: false);
+               }
+
+               void AddItemAbove_Clicked(object sender, EventArgs e)
+               {
+                       var index = (_demoFilteredItemSource.Items.Count / 2) - 1;
+
+                       _demoFilteredItemSource.Items.Insert(index,
+                               new CollectionViewGalleryTestItem(DateTime.Now,
+                               "Inserted item",
+                               "coffee.png",
+                               index));
+               }
+
+               void AddItemBelow_Clicked(object sender, EventArgs e)
+               {
+                       var index = (_demoFilteredItemSource.Items.Count / 2) + 2;
+
+                       _demoFilteredItemSource.Items.Insert(index,
+                               new CollectionViewGalleryTestItem(DateTime.Now,
+                               "Inserted item",
+                               "coffee.png",
+                               index));
+               }
+
+               void AddItemToEnd_Clicked(object sender, EventArgs e)
+               {
+                       _demoFilteredItemSource.Items.Add(
+                               new CollectionViewGalleryTestItem(DateTime.Now, 
+                               "Added item", 
+                               "coffee.png", 
+                               _demoFilteredItemSource.Items.Count));
+               }
+       }
+}
\ No newline at end of file
index e745817..84c40ae 100644 (file)
     <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\SwitchGrouping.xaml">
       <Generator>MSBuild:Compile</Generator>
     </None>
+    <None Update="GalleryPages\CollectionViewGalleries\ScrollModeGalleries\ScrollModeTestGallery.xaml">
+      <Generator>MSBuild:Compile</Generator>
+    </None>
+    <None Update="GalleryPages\CollectionViewGalleries\SelectionGalleries\SelectionChangedCommandParameter.xaml">
+      <Generator>MSBuild:Compile</Generator>
+    </None>
   </ItemGroup>
   <Target Name="CreateControllGalleryConfig" BeforeTargets="Build">
     <CreateItem Include="blank.config">
diff --git a/Xamarin.Forms.Core/Items/ItemsUpdatingScrollMode.cs b/Xamarin.Forms.Core/Items/ItemsUpdatingScrollMode.cs
new file mode 100644 (file)
index 0000000..d072a7b
--- /dev/null
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Xamarin.Forms
+{
+       public enum ItemsUpdatingScrollMode
+       {
+               KeepItemsInView = 0,
+               KeepScrollOffset,
+               KeepLastItemInView
+       }
+}
index 6a508d7..f4c13ba 100644 (file)
@@ -92,13 +92,6 @@ namespace Xamarin.Forms
                internal override ReadOnlyCollection<Element> LogicalChildrenInternal => _logicalChildren.AsReadOnly();
 #endif
 
-               // TODO hartez 2018/08/29 17:35:10 Should ItemsView be abstract? With ItemsLayout as an interface?
-               // Trying to come up with a reasonable way to restrict CarouselView to ListItemsLayout(LinearLayout) 
-               // ((because setting Carousel to grid is ... weird? And by default it just won't do anything.))
-               // And allow CollectionView to use a broader set of Layout options
-               // So the Bindable property only exists at the CarouselView/CollectionView (i.e., concrete class) level
-               // but some version of IItemsLayout is still here?
-
                public static readonly BindableProperty ItemsLayoutProperty =
                        BindableProperty.Create(nameof(ItemsLayout), typeof(IItemsLayout), typeof(ItemsView), 
                                ListItemsLayout.Vertical);
@@ -127,6 +120,16 @@ namespace Xamarin.Forms
                        set => SetValue(ItemSizingStrategyProperty, value);
                }
 
+               public static readonly BindableProperty ItemsUpdatingScrollModeProperty =
+                       BindableProperty.Create(nameof(ItemsUpdatingScrollMode), typeof(ItemsUpdatingScrollMode), typeof(ItemsView),
+                               default(ItemsUpdatingScrollMode));
+
+               public ItemsUpdatingScrollMode ItemsUpdatingScrollMode
+               {
+                       get => (ItemsUpdatingScrollMode)GetValue(ItemsUpdatingScrollModeProperty);
+                       set => SetValue(ItemsUpdatingScrollModeProperty, value);
+               }
+
                public void ScrollTo(int index, int groupIndex = -1,
                        ScrollToPosition position = ScrollToPosition.MakeVisible, bool animate = true)
                {
index be5e086..9b09cc4 100644 (file)
@@ -1,5 +1,6 @@
 using System;
 using Android.Support.V7.Widget;
+using static Android.Support.V7.Widget.RecyclerView;
 using Object = Java.Lang.Object;
 
 namespace Xamarin.Forms.Platform.Android
@@ -7,12 +8,34 @@ namespace Xamarin.Forms.Platform.Android
        internal class DataChangeObserver : RecyclerView.AdapterDataObserver
        {
                readonly Action _onDataChange;
+               public bool Observing { get; private set; }
 
                public DataChangeObserver(Action onDataChange) : base()
                {
                        _onDataChange = onDataChange;
                }
 
+               public void Start(Adapter adapter)
+               {
+                       if (Observing)
+                       {
+                               return;
+                       }
+
+                       adapter.RegisterAdapterDataObserver(this);
+                       Observing = true;
+               }
+
+               public void Stop(Adapter adapter)
+               {
+                       if (Observing && adapter != null)
+                       {
+                               adapter.UnregisterAdapterDataObserver(this);
+                       }
+
+                       Observing = false;
+               }
+
                public override void OnChanged()
                {
                        base.OnChanged();
index 437aeee..d02f400 100644 (file)
@@ -30,8 +30,8 @@ namespace Xamarin.Forms.Platform.Android
                ScrollHelper _scrollHelper;
 
                EmptyViewAdapter _emptyViewAdapter;
-               DataChangeObserver _dataChangeViewObserver;
-               bool _watchingForEmpty;
+               readonly DataChangeObserver _emptyCollectionObserver;
+               readonly DataChangeObserver _itemsUpdateScrollObserver;
 
                ScrollBarVisibility _defaultHorizontalScrollVisibility = ScrollBarVisibility.Default;
                ScrollBarVisibility _defaultVerticalScrollVisibility = ScrollBarVisibility.Default;
@@ -45,11 +45,14 @@ namespace Xamarin.Forms.Platform.Android
                        _automationPropertiesProvider = new AutomationPropertiesProvider(this);
                        _effectControlProvider = new EffectControlProvider(this);
 
+                       _emptyCollectionObserver = new DataChangeObserver(UpdateEmptyViewVisibility);
+                       _itemsUpdateScrollObserver = new DataChangeObserver(AdjustScrollForItemUpdate);
+
                        VerticalScrollBarEnabled = false;
                        HorizontalScrollBarEnabled = false;
                }
 
-               ScrollHelper ScrollHelper => _scrollHelper ?? (_scrollHelper = new ScrollHelper(this));
+               ScrollHelper ScrollHelper => _scrollHelper = _scrollHelper ?? new ScrollHelper(this);
 
                // TODO hartez 2018/10/24 19:27:12 Region all the interface implementations     
 
@@ -216,7 +219,7 @@ namespace Xamarin.Forms.Platform.Android
                        {
                                UpdateAdapter();
                        }
-                       else if(changedProperty.Is(ItemsView.HorizontalScrollBarVisibilityProperty))
+                       else if (changedProperty.Is(ItemsView.HorizontalScrollBarVisibilityProperty))
                        {
                                UpdateHorizontalScrollBarVisibility();
                        }
@@ -224,6 +227,10 @@ namespace Xamarin.Forms.Platform.Android
                        {
                                UpdateVerticalScrollBarVisibility();
                        }
+                       else if (changedProperty.Is(ItemsView.ItemsUpdatingScrollModeProperty))
+                       {
+                               UpdateItemsUpdatingScrollMode();
+                       }
                }
 
                protected virtual void UpdateItemsSource()
@@ -233,11 +240,16 @@ namespace Xamarin.Forms.Platform.Android
                                return;
                        }
 
-                       // Stop watching the old adapter to see if it's empty (if we are watching)
-                       Unwatch(ItemsViewAdapter ?? GetAdapter());
+                       // Stop watching the old adapter 
+                       var adapter = ItemsViewAdapter ?? GetAdapter();
+                       _emptyCollectionObserver.Stop(adapter);
+                       _itemsUpdateScrollObserver.Stop(adapter);
 
                        UpdateAdapter();
 
+                       // Set up any properties which require observing data changes in the adapter
+                       UpdateItemsUpdatingScrollMode();
+
                        UpdateEmptyView();
                }
 
@@ -252,34 +264,6 @@ namespace Xamarin.Forms.Platform.Android
                        oldItemViewAdapter?.Dispose();
                }
 
-               void Unwatch(Adapter adapter)
-               {
-                       if (_watchingForEmpty && adapter != null && _dataChangeViewObserver != null)
-                       {
-                               adapter.UnregisterAdapterDataObserver(_dataChangeViewObserver);
-                       }
-
-                       _watchingForEmpty = false;
-               }
-
-               // TODO hartez 2018/10/24 19:25:14 I don't like these method names; too generic         
-               // TODO hartez 2018/11/05 22:37:42 Also, thinking all the EmptyView stuff should be moved to a helper   
-               void Watch(Adapter adapter)
-               {
-                       if (_watchingForEmpty)
-                       {
-                               return;
-                       }
-
-                       if (_dataChangeViewObserver == null)
-                       {
-                               _dataChangeViewObserver = new DataChangeObserver(UpdateEmptyViewVisibility);
-                       }
-
-                       adapter.RegisterAdapterDataObserver(_dataChangeViewObserver);
-                       _watchingForEmpty = true;
-               }
-
                protected virtual void SetUpNewElement(ItemsView newElement)
                {
                        if (newElement == null)
@@ -371,11 +355,15 @@ namespace Xamarin.Forms.Platform.Android
 
                        if (ItemsViewAdapter != null)
                        {
-                               Unwatch(ItemsViewAdapter);
-                               
+                               // Stop watching for empty items or scroll adjustments
+                               _emptyCollectionObserver.Stop(ItemsViewAdapter);
+                               _itemsUpdateScrollObserver.Stop(ItemsViewAdapter);
+
+                               // Unhook whichever adapter is active
                                SetAdapter(null);
 
-                               ItemsViewAdapter.Dispose();
+                               _emptyViewAdapter?.Dispose();
+                               ItemsViewAdapter?.Dispose();
                        }
 
                        if (_snapManager != null)
@@ -463,16 +451,34 @@ namespace Xamarin.Forms.Platform.Android
                                _emptyViewAdapter.EmptyView = emptyView;
                                _emptyViewAdapter.EmptyViewTemplate = emptyViewTemplate;
 
-                               Watch(ItemsViewAdapter);
+                               _emptyCollectionObserver.Start(ItemsViewAdapter);
                        }
                        else
                        {
-                               Unwatch(ItemsViewAdapter);
+                               _emptyCollectionObserver.Stop(ItemsViewAdapter);
                        }
 
                        UpdateEmptyViewVisibility();
                }
 
+               protected virtual void UpdateItemsUpdatingScrollMode()
+               {
+                       if (ItemsViewAdapter == null || ItemsView == null)
+                       {
+                               return;
+                       }
+
+                       if (ItemsView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView)
+                       {
+                               // Keeping the current items in view is the default, so we don't need to watch for data changes
+                               _itemsUpdateScrollObserver.Stop(ItemsViewAdapter);
+                       }
+                       else
+                       {
+                               _itemsUpdateScrollObserver.Start(ItemsViewAdapter);
+                       }
+               }
+
                protected virtual void ReconcileFlowDirectionAndLayout()
                {
                        if (!(GetLayoutManager() is LinearLayoutManager linearLayoutManager))
@@ -567,5 +573,18 @@ namespace Xamarin.Forms.Platform.Android
                                SetLayoutManager(SelectLayoutManager(_layout));
                        }
                }
+
+               internal void AdjustScrollForItemUpdate()
+               {
+                       if (ItemsView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView)
+                       {
+                               ScrollTo(new ScrollToRequestEventArgs(ItemsViewAdapter.ItemCount, 0,
+                                       Xamarin.Forms.ScrollToPosition.MakeVisible, true));
+                       }
+                       else if (ItemsView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepScrollOffset)
+                       {
+                               ScrollHelper.UndoNextScrollAdjustment();
+                       }
+               }
        }
 }
\ No newline at end of file
index bad723a..3bd8f00 100644 (file)
@@ -4,16 +4,41 @@ using Android.Support.V7.Widget;
 
 namespace Xamarin.Forms.Platform.Android
 {
-       internal class ScrollHelper
+       internal class ScrollHelper : RecyclerView.OnScrollListener
        {
                readonly RecyclerView _recyclerView;
                Action _pendingScrollAdjustment;
 
+               bool _undoNextScrollAdjustment;
+               bool _maintainingScrollOffsets;
+
+               int _lastScrollX;
+               int _lastScrollY;
+               int _lastDeltaX;
+               int _lastDeltaY;
+
                public ScrollHelper(RecyclerView recyclerView)
                {
                        _recyclerView = recyclerView;
                }
 
+               // Used by the renderer to maintain scroll offset when using ItemsUpdatingScrollMode KeepScrollOffset
+               public void UndoNextScrollAdjustment()
+               {
+                       // Don't start tracking the scroll offsets until we really need to
+                       if (!_maintainingScrollOffsets)
+                       {
+                               _maintainingScrollOffsets = true;
+                               //_recyclerView.ScrollChange += ScrollChange;
+                               _recyclerView.AddOnScrollListener(this);
+                       }
+
+                       _undoNextScrollAdjustment = true;
+
+                       _lastScrollX = _recyclerView.ComputeHorizontalScrollOffset();
+                       _lastScrollY = _recyclerView.ComputeVerticalScrollOffset();
+               }
+
                public void AdjustScroll()
                {
                        _pendingScrollAdjustment?.Invoke();
@@ -169,5 +194,36 @@ namespace Xamarin.Forms.Platform.Android
 
                        _recyclerView.ScrollBy(offset, 0);
                }
+
+               void TrackOffsets()
+               {
+                       var newXOffset = _recyclerView.ComputeHorizontalScrollOffset();
+                       var newYOffset = _recyclerView.ComputeVerticalScrollOffset();
+
+                       _lastDeltaX = Math.Max(newXOffset - _lastScrollX, 0);
+                       _lastDeltaY = Math.Max(newYOffset - _lastScrollY, 0);
+
+                       _lastScrollX = newXOffset;
+                       _lastScrollY = newYOffset;
+
+                       if (_undoNextScrollAdjustment)
+                       {
+                               // This last scroll adjustment happened because a new item was added and it caused the scroll
+                               // offset to shift; since the ItemsUpdatingScrollMode is set to KeepScrollOffset; we need to undo 
+                               // that shift and stay where we were before the item was added
+
+                               _undoNextScrollAdjustment = false;
+                               _recyclerView.ScrollBy(-_lastDeltaX, -_lastDeltaY);
+
+                               _lastDeltaX = 0;
+                               _lastDeltaY = 0;
+                       }
+               }
+
+               public override void OnScrolled(RecyclerView recyclerView, int dx, int dy)
+               {
+                       base.OnScrolled(recyclerView, dx, dy);
+                       TrackOffsets();
+               }
        }
 }
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/IndexPathExtensions.cs b/Xamarin.Forms.Platform.iOS/CollectionView/IndexPathExtensions.cs
new file mode 100644 (file)
index 0000000..cf71284
--- /dev/null
@@ -0,0 +1,42 @@
+using Foundation;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+       internal static class IndexPathExtensions
+       {
+               public static bool IsLessThanOrEqualToPath(this NSIndexPath path, NSIndexPath otherPath)
+               {
+                       if (path.Section < otherPath.Section)
+                       {
+                               return true;
+                       }
+
+                       if (path.Section == otherPath.Section)
+                       {
+                               return path.Item <= otherPath.Item;
+                       }
+
+                       return false;
+               }
+
+               public static NSIndexPath FindFirst(this NSIndexPath[] paths)
+               {
+                       NSIndexPath firstPath = null;
+                       foreach (var path in paths)
+                       {
+                               if (firstPath == null)
+                               {
+                                       firstPath = path;
+                                       continue;
+                               }
+
+                               if (path.IsLessThanOrEqualToPath(firstPath))
+                               {
+                                       firstPath = path;
+                               }
+                       }
+
+                       return firstPath;
+               }
+       }
+}
index 73a3f95..27b14d7 100644 (file)
@@ -13,6 +13,11 @@ namespace Xamarin.Forms.Platform.iOS
                readonly ItemsLayout _itemsLayout;
                bool _determiningCellSize;
                bool _disposed;
+               bool _adjustContentOffset;
+               CGSize _adjustmentSize0;
+               CGSize _adjustmentSize1;
+
+               public ItemsUpdatingScrollMode ItemsUpdatingScrollMode { get; set; }
 
                protected ItemsViewLayout(ItemsLayout itemsLayout, ItemSizingStrategy itemSizingStrategy)
                {
@@ -422,5 +427,140 @@ namespace Xamarin.Forms.Platform.iOS
                        // without them, so we need to do it manually here
                        return UICollectionViewLayoutAttributes.CreateForSupplementaryView(kind, indexPath);
                }
+
+               public override void PrepareLayout()
+               {
+                       base.PrepareLayout();
+
+                       // PrepareLayout is the only good place to consistently track the content size changes
+                       TrackOffsetAdjustment();
+               }
+
+               public override void PrepareForCollectionViewUpdates(UICollectionViewUpdateItem[] updateItems)
+               {
+                       base.PrepareForCollectionViewUpdates(updateItems);
+
+                       if (ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepScrollOffset)
+                       {
+                               // This is the default behavior for iOS, no need to do anything
+                               return;
+                       }
+
+                       if (ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView
+                          || ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView)
+                       {
+                               // If this update will shift the visible items,  we'll have to adjust for 
+                               // that later in TargetContentOffsetForProposedContentOffset
+                               _adjustContentOffset = UpdateWillShiftVisibleItems(CollectionView, updateItems);
+                       }
+               }
+
+               public override CGPoint TargetContentOffsetForProposedContentOffset(CGPoint proposedContentOffset)
+               {
+                       if (_adjustContentOffset)
+                       {
+                               _adjustContentOffset = false;
+
+                               // PrepareForCollectionViewUpdates detected that an item update was going to shift the viewport
+                               // and we want to make sure it stays in place
+                               return proposedContentOffset + ComputeOffsetAdjustment();
+                       }
+
+                       return base.TargetContentOffsetForProposedContentOffset(proposedContentOffset);
+               }
+
+               public override void FinalizeCollectionViewUpdates()
+               {
+                       base.FinalizeCollectionViewUpdates();
+
+                       if (ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView)
+                       {
+                               ForceScrollToLastItem(CollectionView);
+                       }
+               }
+
+               void TrackOffsetAdjustment()
+               {
+                       // Keep track of the previous sizes of the CollectionView content so we can adjust the viewport
+                       // offsets if we're in ItemsUpdatingScrollMode.KeepItemsInView
+
+                       // We keep track of the last two adjustments because the only place we can consistently track this
+                       // is PrepareLayout, and by the time PrepareLayout has been called, the CollectionViewContentSize
+                       // has already been updated
+
+                       if (_adjustmentSize0.IsEmpty)
+                       {
+                               _adjustmentSize0 = CollectionViewContentSize;
+                       }
+                       else if (_adjustmentSize1.IsEmpty)
+                       {
+                               _adjustmentSize1 = CollectionViewContentSize;
+                       }
+                       else
+                       {
+                               _adjustmentSize0 = _adjustmentSize1;
+                               _adjustmentSize1 = CollectionViewContentSize;
+                       }
+               }
+
+               CGSize ComputeOffsetAdjustment()
+               {
+                       return CollectionViewContentSize - _adjustmentSize0;
+               }
+
+               static bool UpdateWillShiftVisibleItems(UICollectionView collectionView, UICollectionViewUpdateItem[] updateItems)
+               {
+                       // Find the first visible item
+                       var firstPath = collectionView.IndexPathsForVisibleItems.FindFirst();
+
+                       if (firstPath == null)
+                       {
+                               // No visible items to shift
+                               return false;
+                       }
+
+                       // Determine whether any of the new items will be "before" the first visible item
+                       foreach (var item in updateItems)
+                       {
+                               if (item.UpdateAction == UICollectionUpdateAction.Delete
+                                       || item.UpdateAction == UICollectionUpdateAction.Insert
+                                       || item.UpdateAction == UICollectionUpdateAction.Move)
+                               {
+                                       if (item.IndexPathAfterUpdate == null)
+                                       {
+                                               continue;
+                                       }
+
+                                       if (item.IndexPathAfterUpdate.IsLessThanOrEqualToPath(firstPath))
+                                       {
+                                               // If any of these items will end up "before" the first visible item, then the items will shift
+                                               return true;
+                                       }
+                               }
+                       }
+
+                       return false;
+               }
+
+               static void ForceScrollToLastItem(UICollectionView collectionView)
+               {
+                       var sections = (int)collectionView.NumberOfSections();
+
+                       if (sections == 0)
+                       {
+                               return;
+                       }
+
+                       for (int section = sections - 1; section >= 0; section--)
+                       {
+                               var itemCount = collectionView.NumberOfItemsInSection(section);
+                               if (itemCount > 0)
+                               {
+                                       var lastIndexPath = NSIndexPath.FromItemSection(itemCount - 1, section);
+                                       collectionView.ScrollToItem(lastIndexPath, UICollectionViewScrollPosition.Bottom, true);
+                                       return;
+                               }
+                       }
+               }
        }
 }
index 299a3e7..5b0d59d 100644 (file)
@@ -57,6 +57,10 @@ namespace Xamarin.Forms.Platform.iOS
                        {
                                UpdateVerticalScrollBarVisibility();
                        }
+                       else if (changedProperty.Is(ItemsView.ItemsUpdatingScrollModeProperty))
+                       {
+                               UpdateItemsUpdatingScrollMode();
+                       }
                }
 
                protected virtual ItemsViewLayout SelectLayout(IItemsLayout layoutSpecification, ItemSizingStrategy itemSizingStrategy)
@@ -131,6 +135,11 @@ namespace Xamarin.Forms.Platform.iOS
                        UpdateLayout();
                }
 
+               protected virtual void UpdateItemsUpdatingScrollMode()
+               {
+                       _layout.ItemsUpdatingScrollMode = Element.ItemsUpdatingScrollMode;
+               }
+
                protected virtual ItemsViewController CreateController(ItemsView newElement, ItemsViewLayout layout)
                {
                        return new ItemsViewController(newElement, layout);
index 512cc6b..f9937cb 100644 (file)
     <Compile Include="CollectionView\HorizontalDefaultSupplementalView.cs" />
     <Compile Include="CollectionView\HorizontalTemplatedHeaderView.cs" />
     <Compile Include="CollectionView\IItemsViewSource.cs" />
+    <Compile Include="CollectionView\IndexPathExtensions.cs" />
     <Compile Include="CollectionView\ItemsSourceFactory.cs" />
     <Compile Include="CollectionView\ItemsViewCell.cs" />
     <Compile Include="CollectionView\DefaultCell.cs" />