Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Commit

Permalink
Work around UICollectionViewFlowLayout bug to display partial columns;
Browse files Browse the repository at this point in the history
…fixes #6077

Fix content inset behaviors when dealing with safe area on newer iOS versions
  • Loading branch information
hartez committed May 21, 2019
1 parent 87db2a2 commit 2786f7c
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)]
Expand All @@ -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<ItemModel>(_items);
}

Expand Down Expand Up @@ -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 { };
Expand All @@ -119,18 +107,31 @@ 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;
}

protected override void Init()
{
#if APP
Device.SetFlags(new List<string>(Device.Flags ?? new List<string>()) { "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
}
}
97 changes: 93 additions & 4 deletions Xamarin.Forms.Platform.iOS/CollectionView/GridViewLayout.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
}
}
9 changes: 9 additions & 0 deletions Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 2786f7c

Please sign in to comment.