--- /dev/null
+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
+ }
+}
<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>
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
{
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),
}
};
}
readonly Picker _picker;
- public EnumSelector(Func<T> getValue, Action<T> setValue)
+ public EnumSelector(Func<T> getValue, Action<T> setValue, string automationId = "")
{
_setValue = setValue;
{
WidthRequest = 200,
ItemsSource = source,
- SelectedItem = getValue().ToString()
+ SelectedItem = getValue().ToString(),
+ AutomationId = automationId
};
_picker.SelectedIndexChanged += PickerOnSelectedIndexChanged;
--- /dev/null
+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)
+ }
+ }
+ };
+ }
+ }
+}
--- /dev/null
+<?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
--- /dev/null
+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
<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">
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Xamarin.Forms
+{
+ public enum ItemsUpdatingScrollMode
+ {
+ KeepItemsInView = 0,
+ KeepScrollOffset,
+ KeepLastItemInView
+ }
+}
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);
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)
{
using System;
using Android.Support.V7.Widget;
+using static Android.Support.V7.Widget.RecyclerView;
using Object = Java.Lang.Object;
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();
ScrollHelper _scrollHelper;
EmptyViewAdapter _emptyViewAdapter;
- DataChangeObserver _dataChangeViewObserver;
- bool _watchingForEmpty;
+ readonly DataChangeObserver _emptyCollectionObserver;
+ readonly DataChangeObserver _itemsUpdateScrollObserver;
ScrollBarVisibility _defaultHorizontalScrollVisibility = ScrollBarVisibility.Default;
ScrollBarVisibility _defaultVerticalScrollVisibility = ScrollBarVisibility.Default;
_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
{
UpdateAdapter();
}
- else if(changedProperty.Is(ItemsView.HorizontalScrollBarVisibilityProperty))
+ else if (changedProperty.Is(ItemsView.HorizontalScrollBarVisibilityProperty))
{
UpdateHorizontalScrollBarVisibility();
}
{
UpdateVerticalScrollBarVisibility();
}
+ else if (changedProperty.Is(ItemsView.ItemsUpdatingScrollModeProperty))
+ {
+ UpdateItemsUpdatingScrollMode();
+ }
}
protected virtual void UpdateItemsSource()
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();
}
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)
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)
_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))
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
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();
_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
--- /dev/null
+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;
+ }
+ }
+}
readonly ItemsLayout _itemsLayout;
bool _determiningCellSize;
bool _disposed;
+ bool _adjustContentOffset;
+ CGSize _adjustmentSize0;
+ CGSize _adjustmentSize1;
+
+ public ItemsUpdatingScrollMode ItemsUpdatingScrollMode { get; set; }
protected ItemsViewLayout(ItemsLayout itemsLayout, ItemSizingStrategy itemSizingStrategy)
{
// 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;
+ }
+ }
+ }
}
}
{
UpdateVerticalScrollBarVisibility();
}
+ else if (changedProperty.Is(ItemsView.ItemsUpdatingScrollModeProperty))
+ {
+ UpdateItemsUpdatingScrollMode();
+ }
}
protected virtual ItemsViewLayout SelectLayout(IItemsLayout layoutSpecification, ItemSizingStrategy itemSizingStrategy)
UpdateLayout();
}
+ protected virtual void UpdateItemsUpdatingScrollMode()
+ {
+ _layout.ItemsUpdatingScrollMode = Element.ItemsUpdatingScrollMode;
+ }
+
protected virtual ItemsViewController CreateController(ItemsView newElement, ItemsViewLayout layout)
{
return new ItemsViewController(newElement, layout);
<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" />