Skip to content

Commit dc6b02b

Browse files
albyrock87PureWeen
andcommitted
Fix CV1 GridItemsLayout centering single item AND Fix Empty view not resizing when bounds change (#29639)
* Fix CV1 empty view not resizing when bounds change and grid layout not left aligning single item * - rework test * - fix test --------- Co-authored-by: Shane Neuville <shneuvil@microsoft.com>
1 parent b434c6a commit dc6b02b

File tree

7 files changed

+300
-16
lines changed

7 files changed

+300
-16
lines changed

src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,12 @@ public override void ViewWillLayoutSubviews()
209209
}
210210

211211
base.ViewWillLayoutSubviews();
212-
213-
if (needsCellLayout || !_laidOut)
212+
213+
if (needsCellLayout || // A cell changed its measure
214+
!_laidOut || // We have never laid out
215+
// With no cells, nothing will trigger a layout when bounds change,
216+
// but we still need to properly lay out supplementary views
217+
ItemsSource.ItemCount == 0)
214218
{
215219
// We don't want to mess up with ContentOffset while refreshing, given that's also gonna cause
216220
// a change in the content's offset Y.
@@ -239,24 +243,32 @@ private protected virtual void LayoutSupplementaryViews()
239243
void InvalidateLayoutIfItemsMeasureChanged()
240244
{
241245
var visibleCells = CollectionView.VisibleCells;
242-
List<NSIndexPath> invalidatedPaths = null;
246+
List<TemplatedCell> invalidatedCells = null;
243247

244248
var visibleCellsLength = visibleCells.Length;
245249
for (int n = 0; n < visibleCellsLength; n++)
246250
{
247251
if (visibleCells[n] is TemplatedCell { MeasureInvalidated: true } cell)
248252
{
249-
invalidatedPaths ??= new List<NSIndexPath>(visibleCellsLength);
250-
var path = CollectionView.IndexPathForCell(cell);
251-
invalidatedPaths.Add(path);
253+
invalidatedCells ??= [];
254+
invalidatedCells.Add(cell);
252255
}
253256
}
254257

255-
if (invalidatedPaths != null)
258+
if (invalidatedCells is not null)
256259
{
257-
var layoutInvalidationContext = new UICollectionViewFlowLayoutInvalidationContext();
258-
layoutInvalidationContext.InvalidateItems(invalidatedPaths.ToArray());
259-
CollectionView.CollectionViewLayout.InvalidateLayout(layoutInvalidationContext);
260+
// GridLayout has a special positioning override when there's only one item
261+
// so we have to invalidate the layout entirely to trigger that special case.
262+
if (ItemsSource.ItemCount == 1)
263+
{
264+
CollectionView.CollectionViewLayout.InvalidateLayout();
265+
}
266+
else
267+
{
268+
var layoutInvalidationContext = new UICollectionViewFlowLayoutInvalidationContext();
269+
layoutInvalidationContext.InvalidateItems(invalidatedCells.Select(CollectionView.IndexPathForCell).ToArray());
270+
CollectionView.CollectionViewLayout.InvalidateLayout(layoutInvalidationContext);
271+
}
260272
}
261273
}
262274

src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,23 +207,22 @@ void InvalidateLayoutIfItemsMeasureChanged()
207207
{
208208
var collectionView = CollectionView;
209209
var visibleCells = collectionView.VisibleCells;
210-
List<NSIndexPath> invalidatedPaths = null;
210+
List<TemplatedCell2> invalidatedCells = null;
211211

212212
var visibleCellsLength = visibleCells.Length;
213213
for (int n = 0; n < visibleCellsLength; n++)
214214
{
215215
if (visibleCells[n] is TemplatedCell2 { MeasureInvalidated: true } cell)
216216
{
217-
invalidatedPaths ??= new List<NSIndexPath>(visibleCellsLength);
218-
var path = collectionView.IndexPathForCell(cell);
219-
invalidatedPaths.Add(path);
217+
invalidatedCells ??= [];
218+
invalidatedCells.Add(cell);
220219
}
221220
}
222221

223-
if (invalidatedPaths != null)
222+
if (invalidatedCells is not null)
224223
{
225224
var layoutInvalidationContext = new UICollectionViewLayoutInvalidationContext();
226-
layoutInvalidationContext.InvalidateItems(invalidatedPaths.ToArray());
225+
layoutInvalidationContext.InvalidateItems(invalidatedCells.Select(CollectionView.IndexPathForCell).ToArray());
227226
collectionView.CollectionViewLayout.InvalidateLayout(layoutInvalidationContext);
228227
}
229228
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
using System.Collections.ObjectModel;
2+
using Microsoft.Maui.Controls.Shapes;
3+
4+
namespace Maui.Controls.Sample.Issues;
5+
6+
[Issue(IssueTracker.Github, 29595, "iOS CV: GridItemsLayout not left-aligning a single item", PlatformAffected.iOS)]
7+
public class Issue29595 : ContentPage
8+
{
9+
readonly ObservableCollection<string> _items = [];
10+
11+
public Issue29595()
12+
{
13+
var grid = new Grid();
14+
15+
var cv = new CollectionView
16+
{
17+
Margin = 10,
18+
VerticalOptions = LayoutOptions.Fill,
19+
ItemsLayout = new GridItemsLayout(3, ItemsLayoutOrientation.Vertical)
20+
{
21+
HorizontalItemSpacing = 8,
22+
VerticalItemSpacing = 8
23+
},
24+
ItemTemplate = GetItemTemplate(),
25+
ItemsSource = _items
26+
};
27+
28+
grid.Add(cv);
29+
30+
Content = grid;
31+
}
32+
33+
static DataTemplate GetItemTemplate(double fontSize = 14)
34+
{
35+
return new DataTemplate(() =>
36+
{
37+
var border = new Border
38+
{
39+
StrokeThickness = 0,
40+
StrokeShape = new RoundRectangle { CornerRadius = 32 }
41+
};
42+
43+
var innerGrid = new Grid
44+
{
45+
BackgroundColor = Colors.WhiteSmoke,
46+
RowDefinitions =
47+
{
48+
new RowDefinition { Height = new GridLength(1, GridUnitType.Star) },
49+
new RowDefinition { Height = GridLength.Auto }
50+
}
51+
};
52+
53+
var image = new FFImageLoadingStubImage
54+
{
55+
Aspect = Aspect.AspectFill,
56+
Source = "dotnet_bot.png"
57+
};
58+
Grid.SetRow(image, 0);
59+
60+
var label = new Label
61+
{
62+
Text = "Test",
63+
AutomationId = "StubLabel",
64+
FontSize = fontSize,
65+
FontFamily = "OpenSansRegular",
66+
TextColor = Colors.Black,
67+
HorizontalOptions = LayoutOptions.Center,
68+
VerticalOptions = LayoutOptions.Center
69+
};
70+
Grid.SetRow(label, 1);
71+
72+
innerGrid.Add(image);
73+
innerGrid.Add(label);
74+
75+
border.Content = innerGrid;
76+
return border;
77+
});
78+
}
79+
80+
protected override async void OnAppearing()
81+
{
82+
base.OnAppearing();
83+
await Task.Delay(300);
84+
_items.Add("item1");
85+
}
86+
}
87+
88+
/// <summary>
89+
/// This is a normal image which simulates FFImageLoading loading behavior which may trigger an additional measure pass
90+
/// once the image is loaded, and the new measure could be different from the previous one.
91+
/// </summary>
92+
file class FFImageLoadingStubImage : Image
93+
{
94+
int counter;
95+
96+
protected async override void OnPropertyChanged(string propertyName = null)
97+
{
98+
base.OnPropertyChanged(propertyName);
99+
100+
if (propertyName == SourceProperty.PropertyName)
101+
{
102+
++counter;
103+
await Task.Delay(100);
104+
InvalidateMeasure();
105+
}
106+
}
107+
108+
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
109+
{
110+
var desiredSize = base.OnMeasure(double.PositiveInfinity, double.PositiveInfinity);
111+
var desiredWidth = double.IsNaN(desiredSize.Request.Width) ? 0 : desiredSize.Request.Width + counter;
112+
var desiredHeight = double.IsNaN(desiredSize.Request.Height) ? 0 : desiredSize.Request.Height;
113+
114+
if (double.IsNaN(widthConstraint))
115+
widthConstraint = 0;
116+
if (double.IsNaN(heightConstraint))
117+
heightConstraint = 0;
118+
119+
if (Math.Abs(desiredWidth) < double.Epsilon || Math.Abs(desiredHeight) < double.Epsilon)
120+
return new SizeRequest(new Size(0, 0));
121+
122+
if (double.IsPositiveInfinity(widthConstraint) && double.IsPositiveInfinity(heightConstraint))
123+
{
124+
return new SizeRequest(new Size(desiredWidth, desiredHeight));
125+
}
126+
127+
if (double.IsPositiveInfinity(widthConstraint))
128+
{
129+
var factor = heightConstraint / desiredHeight;
130+
return new SizeRequest(new Size(desiredWidth * factor, desiredHeight * factor));
131+
}
132+
133+
if (double.IsPositiveInfinity(heightConstraint))
134+
{
135+
var factor = widthConstraint / desiredWidth;
136+
return new SizeRequest(new Size(desiredWidth * factor, desiredHeight * factor));
137+
}
138+
139+
var fitsWidthRatio = widthConstraint / desiredWidth;
140+
var fitsHeightRatio = heightConstraint / desiredHeight;
141+
142+
if (double.IsNaN(fitsWidthRatio))
143+
fitsWidthRatio = 0;
144+
if (double.IsNaN(fitsHeightRatio))
145+
fitsHeightRatio = 0;
146+
147+
if (Math.Abs(fitsWidthRatio) < double.Epsilon && Math.Abs(fitsHeightRatio) < double.Epsilon)
148+
return new SizeRequest(new Size(0, 0));
149+
150+
if (Math.Abs(fitsWidthRatio) < double.Epsilon)
151+
return new SizeRequest(new Size(desiredWidth * fitsHeightRatio, desiredHeight * fitsHeightRatio));
152+
153+
if (Math.Abs(fitsHeightRatio) < double.Epsilon)
154+
return new SizeRequest(new Size(desiredWidth * fitsWidthRatio, desiredHeight * fitsWidthRatio));
155+
156+
var ratioFactor = Math.Min(fitsWidthRatio, fitsHeightRatio);
157+
158+
return new SizeRequest(new Size(desiredWidth * ratioFactor, desiredHeight * ratioFactor));
159+
}
160+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
namespace Maui.Controls.Sample.Issues;
2+
3+
[Issue(IssueTracker.Github, 29634, "iOS CV: Empty view not resizing when bounds change", PlatformAffected.iOS)]
4+
public class Issue29634 : ContentPage
5+
{
6+
CollectionView _collectionView;
7+
public Issue29634()
8+
{
9+
Grid grid = null;
10+
var button = new Button
11+
{
12+
Margin = new Thickness(0, 0, 0, 5),
13+
BackgroundColor = Colors.LightSeaGreen,
14+
FontSize = 12,
15+
TextColor = Colors.DarkSlateGray,
16+
Text = "Button text",
17+
Command = new Command(() => grid.WidthRequest = 200),
18+
AutomationId = "RunTest"
19+
};
20+
21+
button.SizeChanged += async (sender, e) =>
22+
{
23+
await Task.Yield(); // Ensure the layout pass is complete before checking size
24+
if (sender is Button b && Content is VerticalStackLayout l && grid.WidthRequest == 200)
25+
{
26+
if (l.Children.Count > 1)
27+
l.Children.RemoveAt(1); // Remove the previous label if it exists
28+
29+
if (b.Width == 200)
30+
l.Add(new Label() { Text = "Button Successfully resized", AutomationId = "SuccessLabel" });
31+
else
32+
l.Add(new Label() { Text = $"Button Failed To Resize to 200: {b.Width}x{b.Height}", AutomationId = "FailLabel" });
33+
}
34+
};
35+
36+
_collectionView = new CollectionView
37+
{
38+
ItemsLayout = new LinearItemsLayout(ItemsLayoutOrientation.Horizontal) { ItemSpacing = 10 },
39+
ItemTemplate = new DataTemplate(),
40+
EmptyView = button
41+
};
42+
43+
grid = new Grid
44+
{
45+
WidthRequest = 400,
46+
HeightRequest = 200,
47+
ColumnDefinitions =
48+
[
49+
new ColumnDefinition(GridLength.Auto),
50+
new ColumnDefinition(GridLength.Star)
51+
],
52+
Children = { _collectionView }
53+
};
54+
55+
Grid.SetColumn(_collectionView, 1);
56+
57+
Content = new VerticalStackLayout
58+
{
59+
Children =
60+
{
61+
grid
62+
}
63+
};
64+
}
65+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#if IOSUITEST
2+
using NUnit.Framework;
3+
using UITest.Appium;
4+
using UITest.Core;
5+
6+
namespace Microsoft.Maui.TestCases.Tests.Issues;
7+
public class Issue29595 : _IssuesUITest
8+
{
9+
public override string Issue => "iOS CV: GridItemsLayout not left-aligning a single item";
10+
11+
public Issue29595(TestDevice device)
12+
: base(device)
13+
{ }
14+
15+
[Test]
16+
[Category(UITestCategories.CollectionView)]
17+
public void VerifyGridItemsLayoutLeftAlignsSingleItem()
18+
{
19+
App.WaitForElement("StubLabel");
20+
VerifyScreenshot();
21+
}
22+
}
23+
#endif
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#if TEST_FAILS_ON_ANDROID && TEST_FAILS_ON_WINDOWS
2+
using NUnit.Framework;
3+
using UITest.Appium;
4+
using UITest.Core;
5+
6+
namespace Microsoft.Maui.TestCases.Tests.Issues;
7+
8+
public class Issue29634 : _IssuesUITest
9+
{
10+
public override string Issue => "iOS CV: Empty view not resizing when bounds change";
11+
12+
public Issue29634(TestDevice device)
13+
: base(device)
14+
{ }
15+
16+
[Test]
17+
[Category(UITestCategories.CollectionView)]
18+
public void VerifyEmptyViewResizesWhenBoundsChange()
19+
{
20+
App.WaitForElement("RunTest");
21+
App.Tap("RunTest");
22+
App.WaitForElement("SuccessLabel");
23+
}
24+
}
25+
#endif
77.4 KB
Loading

0 commit comments

Comments
 (0)