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

Commit

Permalink
Move layout change resolution calls up to VisualElementRenderer (#13640)
Browse files Browse the repository at this point in the history
* Move layout change resolution call up to VisualElement
Fixes #13418
Fixes #13492

* Signal layout request when CollectionView is in a layout
Fixes #13551

* Fix layout error loop from test 12714

* Fix autolayout issues when CollectionView size is less than span;

* Provide measurement for default text cells (no ItemTemplate);

* Cleanup
  • Loading branch information
hartez authored Feb 5, 2021
1 parent b7056ad commit 3d054b8
Show file tree
Hide file tree
Showing 11 changed files with 196 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ protected override void Init()

var source = new List<Item> { new Item { Text = Success } };
cv.ItemsSource = source;

Content = cv;

Appearing += (sender, args) => { cv.IsVisible = true; };
Expand All @@ -49,7 +50,7 @@ class Item

#if UITEST
[Test]
public void SettingGroupedCollectionViewItemSourceNullShouldNotCrash()
public void CollectionShouldInvalidateOnVisibilityChange()
{
RunningApp.WaitForElement(Success);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Item> Source1 { get; } = new ObservableCollection<Item>();
public ObservableCollection<Item> Source2 { get; } = new ObservableCollection<Item>();

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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<Compile Include="$(MSBuildThisFileDirectory)CollectionViewGroupTypeIssue.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue11214.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue13109.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue13551.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RadioButtonTemplateFromStyle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ShellWithCustomRendererDisabledAnimations.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ShellFlyoutContent.cs" />
Expand Down
10 changes: 0 additions & 10 deletions Xamarin.Forms.Platform.Android/Platform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions Xamarin.Forms.Platform.Android/VisualElementRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
6 changes: 0 additions & 6 deletions Xamarin.Forms.Platform.UAP/LayoutRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
5 changes: 5 additions & 0 deletions Xamarin.Forms.Platform.UAP/VisualElementRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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++)
Expand Down
17 changes: 16 additions & 1 deletion Xamarin.Forms.Platform.iOS/CollectionView/GridViewLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);
}
}
Expand Down
20 changes: 14 additions & 6 deletions Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -610,6 +617,7 @@ void ItemsViewPropertyChanged(object sender, PropertyChangedEventArgs changedPro
if (ItemsView.IsVisible)
{
Layout.InvalidateLayout();
CollectionView.LayoutIfNeeded();
}
}
}
Expand Down
20 changes: 0 additions & 20 deletions Xamarin.Forms.Platform.iOS/Platform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions Xamarin.Forms.Platform.iOS/VisualElementRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 3d054b8

Please sign in to comment.