diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue6077.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue6077.cs index 100ba996c1b..c708b48413f 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue6077.cs +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue6077.cs @@ -5,6 +5,8 @@ using System.Runtime.CompilerServices; using Xamarin.Forms.CustomAttributes; using Xamarin.Forms.Internals; +using Xamarin.Forms.PlatformConfiguration; +using Xamarin.Forms.PlatformConfiguration.iOSSpecific; #if UITEST using Xamarin.Forms.Core.UITests; @@ -15,7 +17,7 @@ namespace Xamarin.Forms.Controls.Issues { #if UITEST - [Category(UITestCategories.ManualReview)] + [NUnit.Framework.Category(UITestCategories.CollectionView)] #endif [Preserve(AllMembers = true)] [Issue(IssueTracker.Github, 6077, "CollectionView (iOS) using horizontal grid does not display last column of uneven item count", PlatformAffected.iOS)] @@ -32,26 +34,16 @@ public MainViewModel() CreateItemsCollection(); } - void CreateItemsCollection() + void CreateItemsCollection(int items = 5) { - _items.Add(new ItemModel + for (int n = 0; n < items; n++) { - Title = "Item 1", - }); - _items.Add(new ItemModel - { - Title = "Item 2", - }); - _items.Add(new ItemModel - { - Title = "Item 3", - }); + _items.Add(new ItemModel + { + Title = $"Item {n + 1}", + }); + } - _items.Add(new ItemModel - { - Title = "Item 4", - }); - Items = new ObservableCollection(_items); } @@ -83,29 +75,25 @@ protected void OnPropertyChanged([CallerMemberName] string propertyName = "") public class ItemModel { - string _title; - public string Title - { - get { return _title; } - set { _title = value; } - } + public string Title { get; set; } } ContentPage CreateRoot() { var page = new ContentPage { Title = "Issue6077" }; - var cv = new CollectionView(); + var cv = new CollectionView { ItemSizingStrategy = ItemSizingStrategy.MeasureAllItems }; var itemsLayout = new GridItemsLayout(3, ItemsLayoutOrientation.Horizontal); + cv.ItemsLayout = itemsLayout; var template = new DataTemplate(() => { - var grid = new Grid { }; + var grid = new Grid { HeightRequest = 100, WidthRequest = 50, BackgroundColor = Color.AliceBlue }; grid.RowDefinitions = new RowDefinitionCollection { new RowDefinition { Height = new GridLength(100) } }; - grid.ColumnDefinitions = new ColumnDefinitionCollection { new ColumnDefinition { Width = new GridLength(100) } }; + grid.ColumnDefinitions = new ColumnDefinitionCollection { new ColumnDefinition { Width = new GridLength(50) } }; var label = new Label { }; @@ -119,8 +107,10 @@ ContentPage CreateRoot() }); cv.ItemTemplate = template; - cv.SetBinding(CollectionView.ItemsSourceProperty, new Binding("Items")); + cv.SetBinding(ItemsView.ItemsSourceProperty, new Binding("Items")); + page.Content = cv; + BindingContext = new MainViewModel(); return page; @@ -128,9 +118,20 @@ ContentPage CreateRoot() protected override void Init() { +#if APP Device.SetFlags(new List(Device.Flags ?? new List()) { "CollectionView_Experimental" }); PushAsync(CreateRoot()); +#endif } + +#if UITEST + [Test] + public void LastColumnShouldBeVisible() + { + // If the partial column shows up, then Item 5 will be in it + RunningApp.WaitForElement("Item 5"); + } +#endif } } diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/GridViewLayout.cs b/Xamarin.Forms.Platform.iOS/CollectionView/GridViewLayout.cs index 8a93cec6e12..53fdd646ba7 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/GridViewLayout.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/GridViewLayout.cs @@ -1,7 +1,6 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; +using System.ComponentModel; using CoreGraphics; +using Foundation; using UIKit; namespace Xamarin.Forms.Platform.iOS @@ -36,7 +35,7 @@ public override void ConstrainTo(CGSize size) ? size.Width / _itemsLayout.Span : size.Height / _itemsLayout.Span; - // TODO hartez 2018/09/12 14:52:24 We need to truncate the decimal part of ConstrainedDimension + // We need to truncate the decimal part of ConstrainedDimension // or we occasionally run into situations where the rows/columns don't fit // But this can run into situations where we have an extra gap because we're cutting off too much // and we have a small gap; need to determine where the cut-off is that leads to layout dropping a whole row/column @@ -51,5 +50,95 @@ public override void ConstrainTo(CGSize size) ConstrainedDimension = (int)ConstrainedDimension; DetermineCellSize(); } + + /* `CollectionViewContentSize` and `LayoutAttributesForElementsInRect` are overridden here to work around what + * appears to be a bug in the UICollectionViewFlowLayout implementation: for horizontally scrolling grid + * layouts with auto-sized cells, trailing items which don't fill out a column are never displayed. + * For example, with a span of 3 and either 4 or 5 items, the resulting layout looks like + * + * Item1 + * Item2 + * Item3 + * + * But with 6 items, it looks like + * + * Item1 Item4 + * Item2 Item5 + * Item3 Item6 + * + * IOW, if there are not enough items to fill out the last column, the last column is ignored. + * + * These overrides detect and correct that situation. + */ + + public override CGSize CollectionViewContentSize + { + get + { + if (!NeedsPartialColumnAdjustment()) + { + return base.CollectionViewContentSize; + } + + var contentSize = base.CollectionViewContentSize; + + // Add space for the missing column at the end + var correctedSize = new CGSize(contentSize.Width + EstimatedItemSize.Width, contentSize.Height); + + return correctedSize; + } + } + + public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect) + { + if (!NeedsPartialColumnAdjustment()) + { + return base.LayoutAttributesForElementsInRect(rect); + } + + // When we implement Groups, we'll have to iterate over all of them to adjust and this will + // be a lot more complicated. But until then, we only have to worry about section 0 + int section = 0; + + var fullColumns = base.LayoutAttributesForElementsInRect(rect); + + var itemCount = CollectionView.NumberOfItemsInSection(section); + var missingCellCount = itemCount % _itemsLayout.Span; + + UICollectionViewLayoutAttributes[] allCells = new UICollectionViewLayoutAttributes[fullColumns.Length + missingCellCount]; + fullColumns.CopyTo(allCells, 0); + + for (int n = fullColumns.Length; n < allCells.Length; n++) + { + allCells[n] = LayoutAttributesForItem(NSIndexPath.FromItemSection(n, section)); + } + + return allCells; + } + + bool NeedsPartialColumnAdjustment(int section = 0) + { + if (ScrollDirection == UICollectionViewScrollDirection.Vertical) + { + // The bug only occurs with Horizontal scrolling + return false; + } + + if (EstimatedItemSize.IsEmpty) + { + // The bug only occurs when using Autolayout; with a set ItemSize, we don't have to worry about it + return false; + } + + var itemCount = CollectionView.NumberOfItemsInSection(section); + + if (itemCount % _itemsLayout.Span == 0) + { + // All of the columns are full; the bug only occurs when we have a partial column + return false; + } + + return true; + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs index dccc76d9d11..782344c4d78 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs @@ -27,6 +27,15 @@ protected ItemsViewLayout(ItemsLayout itemsLayout) : UICollectionViewScrollDirection.Vertical; Initialize(scrollDirection); + + if (Forms.IsiOS11OrNewer) + { + // `ContentInset` is actually the default value, but I'm leaving this here as a note to + // future maintainers; it's likely that someone will want a Platform Specific to change this behavior + // (Setting it to `SafeArea` lets you do the thing where the header/footer of your UICollectionView + // fills the screen width in landscape while your items are automatically shifted to avoid the notch) + SectionInsetReference = UICollectionViewFlowLayoutSectionInsetReference.ContentInset; + } } protected override void Dispose(bool disposing) diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewRenderer.cs b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewRenderer.cs index 8a70494b9d4..8edb1210d51 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewRenderer.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewRenderer.cs @@ -85,6 +85,16 @@ protected virtual void SetUpNewElement(ItemsView newElement) UpdateLayout(); ItemsViewController = CreateController(newElement, _layout); + + if (Forms.IsiOS11OrNewer) + { + // We set this property to keep iOS from trying to be helpful about insetting all the + // CollectionView content when we're in landscape mode (to avoid the notch) + // The SetUseSafeArea Platform Specific is already taking care of this for us + // That said, at some point it's possible folks will want a PS for controlling this behavior + ItemsViewController.CollectionView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never; + } + SetNativeControl(ItemsViewController.View); ItemsViewController.CollectionView.BackgroundColor = UIColor.Clear; ItemsViewController.UpdateEmptyView();