[iOS] Implement ItemSizingStrategy hint property for CollectionView (#5094)
authorE.Z. Hart <hartez@users.noreply.github.com>
Mon, 4 Feb 2019 12:38:38 +0000 (05:38 -0700)
committerRui Marinho <me@ruimarinho.net>
Mon, 4 Feb 2019 12:38:38 +0000 (12:38 +0000)
* Add ItemSizingStrategy to CollectionView in Core;
Create test harness for changing ItemSizingStrategy;
Handle ItemSizingStrategy and ItemSizingStrategy changes on iOS;

* Update test bed to better illustrate changes

* No need for DetermineCellSize to be internal

* Conserve precious bits by removing `private` modifier

partially implements #3172

Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/DataTemplateGallery.cs
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ExampleTemplates.cs
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/VariableSizeTemplateGridGallery.cs [new file with mode: 0644]
Xamarin.Forms.Core/Items/ItemSizingStrategy.cs [new file with mode: 0644]
Xamarin.Forms.Core/Items/ItemsView.cs
Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs
Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs
Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewRenderer.cs

index f171994..270e262 100644 (file)
                                                        new TemplateCodeCollectionViewGridGallery (), Navigation),
                                                GalleryBuilder.NavButton("Horizontal Grid (Code)", () => 
                                                        new TemplateCodeCollectionViewGridGallery (ItemsLayoutOrientation.Horizontal), Navigation),
+
+                                               GalleryBuilder.NavButton("ItemSizing Strategy", () => 
+                                                       new VariableSizeTemplateGridGallery (ItemsLayoutOrientation.Horizontal), Navigation),
+                                               
                                        }
                                }
                        };
index 6543a76..2bef5bf 100644 (file)
@@ -1,4 +1,7 @@
-namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
+using System;
+using System.Globalization;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
 {
        internal class ExampleTemplates
        {
                                return templateLayout;
                        });
                }
+
+               public static DataTemplate VariableSizeTemplate()
+               {
+                       var indexHeightConverter = new IndexRequestConverter(3, 50, 150);
+                       var indexWidthConverter = new IndexRequestConverter(3, 100, 300);
+                       var colorConverter = new IndexColorConverter();
+
+                       return new DataTemplate(() =>
+                       {
+                               var layout = new Frame();
+
+                               layout.SetBinding(VisualElement.HeightRequestProperty, new Binding("Index", converter: indexHeightConverter));
+                               layout.SetBinding(VisualElement.WidthRequestProperty, new Binding("Index", converter: indexWidthConverter));
+                               layout.SetBinding(VisualElement.BackgroundColorProperty, new Binding("Index", converter: colorConverter));
+
+                               var image = new Image
+                               {
+                                       Aspect = Aspect.AspectFit
+                               };
+
+                               image.SetBinding(VisualElement.HeightRequestProperty, new Binding("Index", converter: indexHeightConverter));
+                               image.SetBinding(VisualElement.WidthRequestProperty, new Binding("Index", converter: indexWidthConverter));
+
+                               image.SetBinding(Image.SourceProperty, new Binding("Image"));
+
+                               layout.Content = image;
+
+                               return layout;
+                       });
+               }
+
+               class IndexRequestConverter : IValueConverter
+               {
+                       readonly int _cutoff;
+                       readonly int _lowValue;
+                       readonly int _highValue;
+
+                       public IndexRequestConverter(int cutoff, int lowValue, int highValue)
+                       {
+                               _cutoff = cutoff;
+                               _lowValue = lowValue;
+                               _highValue = highValue;
+                       }
+
+                       public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+                       {
+                               var index = (int)value;
+
+                               return index < _cutoff ? _lowValue : (object)_highValue;
+                       }
+
+                       public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
+               }
+
+               class IndexColorConverter : IValueConverter
+               {
+                       Color[] _colors = new Color[] { Color.Red, Color.Green, Color.Blue, Color.Orange, Color.BlanchedAlmond };
+
+                       public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+                       {
+                               var index = (int)value;
+                               return _colors[index % _colors.Length];
+                       }
+
+                       public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
+               }
        }
 }
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/VariableSizeTemplateGridGallery.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/VariableSizeTemplateGridGallery.cs
new file mode 100644 (file)
index 0000000..ce6852b
--- /dev/null
@@ -0,0 +1,65 @@
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
+{
+       internal class VariableSizeTemplateGridGallery : ContentPage
+       {
+               public VariableSizeTemplateGridGallery(ItemsLayoutOrientation orientation = ItemsLayoutOrientation.Vertical)
+               {
+                       var layout = new Grid
+                       { 
+                               RowDefinitions = new RowDefinitionCollection
+                               {
+                                       new RowDefinition { Height = GridLength.Auto },
+                                       new RowDefinition { Height = GridLength.Auto },
+                                       new RowDefinition { Height = GridLength.Auto },
+                                       new RowDefinition { Height = GridLength.Star }
+                               }
+                       };
+
+                       var itemsLayout = new GridItemsLayout(2, orientation);
+
+                       var itemTemplate = ExampleTemplates.VariableSizeTemplate();
+
+                       var collectionView = new CollectionView {ItemsLayout = itemsLayout, ItemTemplate = itemTemplate,
+                               ItemSizingStrategy = ItemSizingStrategy.MeasureFirstItem };
+
+                       var generator = new ItemsSourceGenerator(collectionView, 100);
+
+                       var explanation = new Label();
+                       UpdateExplanation(explanation, collectionView.ItemSizingStrategy);
+
+                       var sizingStrategySelector = new EnumSelector<ItemSizingStrategy>(() => collectionView.ItemSizingStrategy,
+                               mode => {
+                                       collectionView.ItemSizingStrategy = mode;
+                                       UpdateExplanation(explanation, collectionView.ItemSizingStrategy);
+                               });
+
+                       layout.Children.Add(generator);
+
+                       layout.Children.Add(sizingStrategySelector );
+                       Grid.SetRow(sizingStrategySelector , 1);
+
+                       layout.Children.Add(explanation);
+                       Grid.SetRow(explanation, 2);
+
+                       layout.Children.Add(collectionView);
+                       Grid.SetRow(collectionView, 3);
+
+                       Content = layout;
+
+                       generator.GenerateItems();
+               }
+
+               static void UpdateExplanation(Label explanation, ItemSizingStrategy strategy)
+               {
+                       switch (strategy)
+                       {
+                               case ItemSizingStrategy.MeasureAllItems:
+                                       explanation.Text = "Each item is individually measured.";
+                                       break;
+                               case ItemSizingStrategy.MeasureFirstItem:
+                                       explanation.Text = "The first item is measured, and that size is given to all subsequent cells.";
+                                       break;
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/Items/ItemSizingStrategy.cs b/Xamarin.Forms.Core/Items/ItemSizingStrategy.cs
new file mode 100644 (file)
index 0000000..c7cb166
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Xamarin.Forms
+{
+       public enum ItemSizingStrategy
+       {
+               MeasureAllItems,
+               MeasureFirstItem
+       }
+}
\ No newline at end of file
index baf2942..01236c0 100644 (file)
@@ -65,6 +65,15 @@ namespace Xamarin.Forms
                        set => SetValue(ItemTemplateProperty, value);
                }
 
+               public static readonly BindableProperty ItemSizingStrategyProperty =
+                       BindableProperty.Create(nameof(ItemSizingStrategy), typeof(ItemSizingStrategy), typeof(ItemsView));
+
+               public ItemSizingStrategy ItemSizingStrategy
+               {
+                       get => (ItemSizingStrategy)GetValue(ItemSizingStrategyProperty);
+                       set => SetValue(ItemSizingStrategyProperty, value);
+               }
+
                public void ScrollTo(int index, int groupIndex = -1,
                        ScrollToPosition position = ScrollToPosition.MakeVisible, bool animate = true)
                {
index d04f571..b329228 100644 (file)
@@ -13,7 +13,7 @@ namespace Xamarin.Forms.Platform.iOS
        {
                IItemsViewSource _itemsSource;
                readonly ItemsView _itemsView;
-               readonly ItemsViewLayout _layout;
+               ItemsViewLayout _layout;
                bool _initialConstraintsSet;
                bool _wasEmpty;
 
@@ -27,14 +27,36 @@ namespace Xamarin.Forms.Platform.iOS
                {
                        _itemsView = itemsView;
                        _itemsSource = ItemsSourceFactory.Create(_itemsView.ItemsSource, CollectionView);
-                       _layout = layout;
 
+                       UpdateLayout(layout);
+               }
+
+               public void UpdateLayout(ItemsViewLayout layout)
+               {
+                       _layout = layout;
                        _layout.GetPrototype = GetPrototype;
-                       _layout.UniformSize = false; // todo hartez Link this to ItemsView.ItemSizingStrategy hint
 
-                       Delegator = new UICollectionViewDelegator(_layout);
+                       // If we're updating from a previous layout, we should keep any settings for the SelectableItemsViewController around
+                       var selectableItemsViewController = Delegator?.SelectableItemsViewController;
+                       Delegator = new UICollectionViewDelegator(_layout)
+                       {
+                               SelectableItemsViewController = selectableItemsViewController
+                       };
 
                        CollectionView.Delegate = Delegator;
+
+                       if (CollectionView.CollectionViewLayout != _layout)
+                       {
+                               // We're updating from a previous layout
+
+                               // Make sure the new layout is sized properly
+                               _layout.ConstrainTo(CollectionView.Bounds.Size);
+                               
+                               CollectionView.SetCollectionViewLayout(_layout, false);
+                               
+                               // Reload the data so the currently visible cells get laid out according to the new layout
+                               CollectionView.ReloadData();
+                       }
                }
 
                public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
index cb8f9da..b0c86e7 100644 (file)
@@ -51,7 +51,6 @@ namespace Xamarin.Forms.Platform.iOS
 
                void LayoutOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChanged)
                {
-                       HandlePropertyChanged(propertyChanged);
                }
 
                protected virtual void HandlePropertyChanged(PropertyChangedEventArgs propertyChanged)
@@ -62,8 +61,7 @@ namespace Xamarin.Forms.Platform.iOS
 
                public Func<UICollectionViewCell> GetPrototype { get; set; }
 
-               // TODO hartez 2018/09/14 17:24:22 Long term, this needs to use the ItemSizingStrategy enum and not be locked into bool 
-               public bool UniformSize { get; set; }
+               internal ItemSizingStrategy ItemSizingStrategy { get; set; }
 
                public abstract void ConstrainTo(CGSize size);
 
@@ -140,7 +138,7 @@ namespace Xamarin.Forms.Platform.iOS
                        //              has at least one item), Autolayout will kick in for the first cell and size it correctly
                        // If GetPrototype() _can_ return a cell, this estimate will be updated once that cell is measured
                        EstimatedItemSize = new CGSize(1, 1);
-                       
+
                        if (!(GetPrototype() is ItemsViewCell prototype))
                        {
                                _determiningCellSize = false;
@@ -152,7 +150,7 @@ namespace Xamarin.Forms.Platform.iOS
 
                        var measure = prototype.Measure();
 
-                       if (UniformSize)
+                       if (ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem)
                        {
                                // This is the size we'll give all of our cells from here on out
                                ItemSize = measure;
@@ -212,7 +210,7 @@ namespace Xamarin.Forms.Platform.iOS
                {
                        _needCellSizeUpdate = true;
                }
-               
+
                public override CGPoint TargetContentOffset(CGPoint proposedContentOffset, CGPoint scrollingVelocity)
                {
                        var snapPointsType = _itemsLayout.SnapPointsType;
@@ -254,12 +252,12 @@ namespace Xamarin.Forms.Platform.iOS
                        // closest to the relevant part of the viewport while being sufficiently visible
 
                        // Find the spot in the viewport we're trying to align with
-                       var alignmentTarget = SnapHelpers.FindAlignmentTarget(alignment, proposedContentOffset, 
+                       var alignmentTarget = SnapHelpers.FindAlignmentTarget(alignment, proposedContentOffset,
                                CollectionView, ScrollDirection);
 
                        // Find the closest sufficiently visible candidate
                        var bestCandidate = SnapHelpers.FindBestSnapCandidate(visibleElements, viewport, alignmentTarget);
-                       
+
                        if (bestCandidate != null)
                        {
                                return SnapHelpers.AdjustContentOffset(proposedContentOffset, bestCandidate.Frame, viewport, alignment,
@@ -277,7 +275,7 @@ namespace Xamarin.Forms.Platform.iOS
                        // Get the viewport of the UICollectionView at the current content offset
                        var contentOffset = CollectionView.ContentOffset;
                        var viewport = new CGRect(contentOffset, CollectionView.Bounds.Size);
-                                                               
+
                        // Find the spot in the viewport we're trying to align with
                        var alignmentTarget = SnapHelpers.FindAlignmentTarget(alignment, contentOffset, CollectionView, ScrollDirection);
 
index fdee044..3fd2cb7 100644 (file)
@@ -43,6 +43,10 @@ namespace Xamarin.Forms.Platform.iOS
                        {
                                ItemsViewController.UpdateEmptyView();
                        }
+                       else if (changedProperty.Is(ItemsView.ItemSizingStrategyProperty))
+                       {
+                               UpdateItemSizingStrategy();
+                       }
                }
 
                protected virtual ItemsViewLayout SelectLayout(IItemsLayout layoutSpecification)
@@ -79,7 +83,7 @@ namespace Xamarin.Forms.Platform.iOS
                                return;
                        }
 
-                       _layout = SelectLayout(newElement.ItemsLayout);
+                       UpdateLayout();
                        ItemsViewController = CreateController(newElement, _layout);
                        SetNativeControl(ItemsViewController.View);
                        ItemsViewController.CollectionView.BackgroundColor = UIColor.Clear;
@@ -89,6 +93,32 @@ namespace Xamarin.Forms.Platform.iOS
                        newElement.ScrollToRequested += ScrollToRequested;
                }
 
+               protected virtual void UpdateLayout()
+               {
+                       _layout = SelectLayout(Element.ItemsLayout);
+                       _layout.ItemSizingStrategy = Element.ItemSizingStrategy;
+
+                       if (ItemsViewController != null)
+                       {
+                               ItemsViewController.UpdateLayout(_layout);
+                       }
+               }
+
+               protected virtual void UpdateItemSizingStrategy()
+               {
+                       if (ItemsViewController?.CollectionView?.VisibleCells.Length == 0)
+                       {
+                               // The CollectionView isn't really up and running yet, so we can just set the strategy and move on
+                               _layout.ItemSizingStrategy = Element.ItemSizingStrategy;
+                       }
+                       else
+                       {
+                               // We're changing the strategy for a CollectionView mid-stream; 
+                               // we'll just have to swap out the whole UICollectionViewLayout
+                               UpdateLayout();
+                       }
+               }
+
                protected virtual ItemsViewController CreateController(ItemsView newElement, ItemsViewLayout layout)
                {
                        return new ItemsViewController(newElement, layout);