diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue13203.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue13203.cs index a2c1fa1b694..ddf41f83e2d 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue13203.cs +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue13203.cs @@ -37,6 +37,7 @@ protected override void Init() var source = new List { new Item { Text = Success } }; cv.ItemsSource = source; + Content = cv; Appearing += (sender, args) => { cv.IsVisible = true; }; @@ -49,7 +50,7 @@ class Item #if UITEST [Test] - public void SettingGroupedCollectionViewItemSourceNullShouldNotCrash() + public void CollectionShouldInvalidateOnVisibilityChange() { RunningApp.WaitForElement(Success); } diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue13551.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue13551.cs new file mode 100644 index 00000000000..d5b05cb2a8b --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue13551.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Text; +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; + +#if UITEST +using Xamarin.UITest; +using NUnit.Framework; +using Xamarin.Forms.Core.UITests; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ + [Issue(IssueTracker.Github, 13551, "[Bug] [iOS] CollectionView does not display items if `IsVisible` modified via a binding/trigger", PlatformAffected.iOS)] +#if UITEST + [NUnit.Framework.Category(UITestCategories.CollectionView)] +#endif + public class Issue13551 : TestContentPage + { + const string Success1 = "Success1"; + const string Success2 = "Success2"; + + public ObservableCollection Source1 { get; } = new ObservableCollection(); + public ObservableCollection Source2 { get; } = new ObservableCollection(); + + CollectionView BindingWithConverter() + { + var cv = new CollectionView + { + IsVisible = true, + + ItemTemplate = new DataTemplate(() => + { + var label = new Label(); + label.SetBinding(Label.TextProperty, new Binding(nameof(Item.Text))); + return label; + }) + }; + + cv.SetBinding(CollectionView.ItemsSourceProperty, new Binding("Source1")); + cv.SetBinding(VisualElement.IsVisibleProperty, new Binding("Source1.Count", converter: new IntToBoolConverter())); + + return cv; + } + + CollectionView WithTrigger() + { + var cv = new CollectionView + { + IsVisible = true, + + ItemTemplate = new DataTemplate(() => + { + var label = new Label(); + label.SetBinding(Label.TextProperty, new Binding(nameof(Item.Text))); + return label; + }) + }; + + cv.SetBinding(CollectionView.ItemsSourceProperty, new Binding("Source2")); + + var trigger = new DataTrigger(typeof(CollectionView)); + trigger.Value = 0; + trigger.Setters.Add(new Setter() { Property = VisualElement.IsVisibleProperty, Value = false }); + trigger.Binding = new Binding("Source2.Count"); + + cv.Triggers.Add(trigger); + + return cv; + } + + protected override void Init() + { + BindingContext = this; + + var cv1 = BindingWithConverter(); + var cv2 = WithTrigger(); + + var grid = new Grid + { + RowDefinitions = new RowDefinitionCollection + { + new RowDefinition() { Height = GridLength.Star }, + new RowDefinition() { Height = GridLength.Star }, + } + }; + + grid.Children.Add(cv1); + grid.Children.Add(cv2); + Grid.SetRow(cv2, 1); + + Content = grid; + + Device.StartTimer(TimeSpan.FromMilliseconds(300), () => + { + Device.BeginInvokeOnMainThread(() => + { + Source1.Add(new Item { Text = Success1 }); + Source2.Add(new Item { Text = Success2 }); + }); + + return false; + }); + } + + class IntToBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is int val && val > 0; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + public class Item + { + public string Text { get; set; } + } + +#if UITEST + [Test] + public void CollectionInLayoutShouldInvalidateOnVisibilityChange() + { + RunningApp.WaitForElement(Success1); + RunningApp.WaitForElement(Success2); + } +#endif + } +} diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index dd67bc6c1dc..84a65ef8817 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -12,6 +12,7 @@ + diff --git a/Xamarin.Forms.Platform.Android/Platform.cs b/Xamarin.Forms.Platform.Android/Platform.cs index 8e45c57b536..8a060bc1612 100644 --- a/Xamarin.Forms.Platform.Android/Platform.cs +++ b/Xamarin.Forms.Platform.Android/Platform.cs @@ -1315,16 +1315,6 @@ protected override void Dispose(bool disposing) bool ILayoutChanges.HasLayoutOccurred => _hasLayoutOccurred; - protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) - { - if (Element is Layout layout) - { - layout.ResolveLayoutChanges(); - } - - base.OnMeasure(widthMeasureSpec, heightMeasureSpec); - } - protected override void OnLayout(bool changed, int left, int top, int right, int bottom) { base.OnLayout(changed, left, top, right, bottom); diff --git a/Xamarin.Forms.Platform.Android/VisualElementRenderer.cs b/Xamarin.Forms.Platform.Android/VisualElementRenderer.cs index ae9c18823c3..0c422224571 100644 --- a/Xamarin.Forms.Platform.Android/VisualElementRenderer.cs +++ b/Xamarin.Forms.Platform.Android/VisualElementRenderer.cs @@ -505,5 +505,15 @@ internal virtual void SendVisualElementInitialized(VisualElement element, AView void IVisualElementRenderer.SetLabelFor(int? id) => ViewCompat.SetLabelFor(this, id ?? ViewCompat.GetLabelFor(this)); + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + if (Element is Layout layout) + { + layout.ResolveLayoutChanges(); + } + + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Platform.UAP/LayoutRenderer.cs b/Xamarin.Forms.Platform.UAP/LayoutRenderer.cs index ef4509be5d5..b0240ce948b 100644 --- a/Xamarin.Forms.Platform.UAP/LayoutRenderer.cs +++ b/Xamarin.Forms.Platform.UAP/LayoutRenderer.cs @@ -80,11 +80,5 @@ void UpdateClipToBounds() Clip = new RectangleGeometry { Rect = new WRect(0, 0, ActualWidth, ActualHeight) }; } } - - protected override Windows.Foundation.Size MeasureOverride(Windows.Foundation.Size availableSize) - { - Element?.ResolveLayoutChanges(); - return base.MeasureOverride(availableSize); - } } } \ No newline at end of file diff --git a/Xamarin.Forms.Platform.UAP/VisualElementRenderer.cs b/Xamarin.Forms.Platform.UAP/VisualElementRenderer.cs index 85f28f777df..7d5232f9d2a 100644 --- a/Xamarin.Forms.Platform.UAP/VisualElementRenderer.cs +++ b/Xamarin.Forms.Platform.UAP/VisualElementRenderer.cs @@ -294,6 +294,11 @@ protected override Windows.Foundation.Size MeasureOverride(Windows.Foundation.Si if (Element == null || availableSize.Width * availableSize.Height == 0) return new Windows.Foundation.Size(0, 0); + if (Element is Layout layout) + { + layout.ResolveLayoutChanges(); + } + Element.IsInNativeLayout = true; for (var i = 0; i < ElementController.LogicalChildren.Count; i++) diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/GridViewLayout.cs b/Xamarin.Forms.Platform.iOS/CollectionView/GridViewLayout.cs index 9aff468926f..cc872db6f42 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/GridViewLayout.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/GridViewLayout.cs @@ -57,7 +57,16 @@ public override void ConstrainTo(CGSize size) // Truncating to 177 means the rows fit, but there's a very slight gap // There may not be anything we can do about this. - ConstrainedDimension = (int)ConstrainedDimension; + // Possibly the solution is to round to the tenths or hundredths place, we should look into that. + // But for the moment, we need a special case for dimensions < 1, because upon transition from invisible to visible, + // Forms will briefly layout the CollectionView at a size of 1,1. For a spanned collectionview, that means we + // need to accept a constrained dimension of 1/span. If we don't, autolayout will start throwing a flurry of + // exceptions (which we can't catch) and either crash the app or spin until we kill the app. + if (ConstrainedDimension > 1) + { + ConstrainedDimension = (int)ConstrainedDimension; + } + DetermineCellSize(); } @@ -288,6 +297,12 @@ static nfloat ReduceSpacingToFitIfNeeded(nfloat available, nfloat requestedSpaci } var maxSpacing = (available - span) / (span - 1); + + if (maxSpacing < 0) + { + return 0; + } + return (nfloat)Math.Min(requestedSpacing, maxSpacing); } } diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs index 6222bdcecb8..995957b72bd 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs @@ -160,10 +160,6 @@ public override void ViewWillLayoutSubviews() { base.ViewWillLayoutSubviews(); - // We can't set this constraint up on ViewDidLoad, because Forms does other stuff that resizes the view - // and we end up with massive layout errors. And View[Will/Did]Appear do not fire for this controller - // reliably. So until one of those options is cleared up, we set this flag so that the initial constraints - // are set up the first time this method is called. EnsureLayoutInitialized(); LayoutEmptyView(); @@ -566,11 +562,22 @@ TemplatedCell CreateAppropriateCellForLayout() return new VerticalCell(frame); } - public TemplatedCell CreateMeasurementCell(NSIndexPath indexPath) + public UICollectionViewCell CreateMeasurementCell(NSIndexPath indexPath) { if (ItemsView.ItemTemplate == null) { - return null; + var frame = new CGRect(0, 0, ItemsViewLayout.EstimatedItemSize.Width, ItemsViewLayout.EstimatedItemSize.Height); + + if (ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal) + { + var cell1 = new HorizontalDefaultCell(frame); + UpdateDefaultCell(cell1, indexPath); + return cell1; + } + + var cell = new VerticalDefaultCell(frame); + UpdateDefaultCell(cell, indexPath); + return cell; } TemplatedCell templatedCell = CreateAppropriateCellForLayout(); @@ -610,6 +617,7 @@ void ItemsViewPropertyChanged(object sender, PropertyChangedEventArgs changedPro if (ItemsView.IsVisible) { Layout.InvalidateLayout(); + CollectionView.LayoutIfNeeded(); } } } diff --git a/Xamarin.Forms.Platform.iOS/Platform.cs b/Xamarin.Forms.Platform.iOS/Platform.cs index fb04e9f3156..35d70d8c931 100644 --- a/Xamarin.Forms.Platform.iOS/Platform.cs +++ b/Xamarin.Forms.Platform.iOS/Platform.cs @@ -586,26 +586,6 @@ public override UIView HitTest(CGPoint point, UIEvent uievent) return result; } - - void ResolveLayoutChanges() - { - if (Element is Layout layout) - { - layout.ResolveLayoutChanges(); - } - } - - public override void LayoutSubviews() - { - ResolveLayoutChanges(); - base.LayoutSubviews(); - } - - public override CGSize SizeThatFits(CGSize size) - { - ResolveLayoutChanges(); - return base.SizeThatFits(size); - } } internal static string ResolveMsAppDataUri(Uri uri) diff --git a/Xamarin.Forms.Platform.iOS/VisualElementRenderer.cs b/Xamarin.Forms.Platform.iOS/VisualElementRenderer.cs index f46911fb939..4d6ed225fb5 100644 --- a/Xamarin.Forms.Platform.iOS/VisualElementRenderer.cs +++ b/Xamarin.Forms.Platform.iOS/VisualElementRenderer.cs @@ -312,13 +312,25 @@ public void SetElement(TElement element) } #if __MOBILE__ + + void ResolveLayoutChanges() + { + if (Element is Layout layout) + { + layout.ResolveLayoutChanges(); + } + } + public override SizeF SizeThatFits(SizeF size) { + ResolveLayoutChanges(); return new SizeF(0, 0); } public override void LayoutSubviews() { + ResolveLayoutChanges(); + base.LayoutSubviews(); if (_blur != null && Superview != null)