new TemplateCodeCollectionViewGridGallery (), Navigation),
GalleryBuilder.NavButton("Horizontal Grid (Code)", () =>
new TemplateCodeCollectionViewGridGallery (ItemsLayoutOrientation.Horizontal), Navigation),
+
+ GalleryBuilder.NavButton("ItemSizing Strategy", () =>
+ new VariableSizeTemplateGridGallery (ItemsLayoutOrientation.Horizontal), Navigation),
+
}
}
};
-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
--- /dev/null
+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
--- /dev/null
+namespace Xamarin.Forms
+{
+ public enum ItemSizingStrategy
+ {
+ MeasureAllItems,
+ MeasureFirstItem
+ }
+}
\ No newline at end of file
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)
{
{
IItemsViewSource _itemsSource;
readonly ItemsView _itemsView;
- readonly ItemsViewLayout _layout;
+ ItemsViewLayout _layout;
bool _initialConstraintsSet;
bool _wasEmpty;
{
_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)
void LayoutOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChanged)
{
- HandlePropertyChanged(propertyChanged);
}
protected virtual void HandlePropertyChanged(PropertyChangedEventArgs propertyChanged)
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);
// 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;
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;
{
_needCellSizeUpdate = true;
}
-
+
public override CGPoint TargetContentOffset(CGPoint proposedContentOffset, CGPoint scrollingVelocity)
{
var snapPointsType = _itemsLayout.SnapPointsType;
// 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,
// 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);
{
ItemsViewController.UpdateEmptyView();
}
+ else if (changedProperty.Is(ItemsView.ItemSizingStrategyProperty))
+ {
+ UpdateItemSizingStrategy();
+ }
}
protected virtual ItemsViewLayout SelectLayout(IItemsLayout layoutSpecification)
return;
}
- _layout = SelectLayout(newElement.ItemsLayout);
+ UpdateLayout();
ItemsViewController = CreateController(newElement, _layout);
SetNativeControl(ItemsViewController.View);
ItemsViewController.CollectionView.BackgroundColor = UIColor.Clear;
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);