Skip to content

Commit 0d1705a

Browse files
[release/10.0.1xx-sr2] [iOS, macOS] Fixed CollectionView group header size changes with ItemSizingStrategy (#33166)
Backport of #33161 to release/10.0.1xx-sr2 /cc @PureWeen @NanthiniMahalingam --------- Co-authored-by: NanthiniMahalingam <105482474+NanthiniMahalingam@users.noreply.github.com>
1 parent 3380b6c commit 0d1705a

File tree

7 files changed

+206
-9
lines changed

7 files changed

+206
-9
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ void UpdateTemplatedSupplementaryView(TemplatedCell2 cell, NSString elementKind,
127127

128128
var bindingContext = ItemsSource.Group(indexPath);
129129

130+
// Mark this templated cell as a supplementary view (header/footer)
131+
cell.isSupplementaryView = true;
130132
cell.isHeaderOrFooterChanged = true;
131133
cell.Bind(template, bindingContext, ItemsView);
132134
cell.isHeaderOrFooterChanged = false;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ public override UICollectionViewCell GetCell(UICollectionView collectionView, NS
119119
{
120120
TemplatedCell2.ScrollDirection = ScrollDirection;
121121

122+
// Ensure this cell is treated as a regular item cell (not a supplementary view)
123+
TemplatedCell2.isSupplementaryView = false;
122124
TemplatedCell2.Bind(ItemsView.ItemTemplate, ItemsSource[indexpathAdjusted], ItemsView);
123125
}
124126
else if (cell is DefaultCell2 DefaultCell2)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ void UpdateTemplatedSupplementaryView(TemplatedCell2 cell, NSString elementKind)
130130
{
131131
bool isHeader = elementKind == UICollectionElementKindSectionKey.Header;
132132
cell.isHeaderOrFooterChanged = true;
133+
cell.isSupplementaryView = true;
133134

134135
if (isHeader)
135136
{

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

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public event EventHandler<LayoutAttributesChangedEventArgs2> LayoutAttributesCha
4040
Size _measuredSize;
4141
Size _cachedConstraints;
4242

43+
// Indicates the cell is being used as a supplementary view (group header/footer)
44+
internal bool isSupplementaryView = false;
4345
internal bool MeasureInvalidated => _measureInvalidated;
4446

4547
// Flags changes confined to the header/footer, preventing unnecessary recycling and revalidation of templated cells.
@@ -107,20 +109,28 @@ public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittin
107109

108110
if (_measureInvalidated || _cachedConstraints != constraints)
109111
{
110-
// Check if we should use the cached first item size for MeasureFirstItem optimization
111-
var cachedSize = GetCachedFirstItemSizeFromHandler();
112-
if (cachedSize != CGSize.Empty)
112+
// Only use the cached first-item measurement for actual item cells (not headers/footers)
113+
if (!isSupplementaryView)
113114
{
114-
_measuredSize = cachedSize.ToSize();
115-
// Even when we have a cached measurement, we still need to call Measure
116-
// to update the virtual view's internal state and bookkeeping
117-
virtualView.Measure(constraints.Width, _measuredSize.Height);
115+
var cachedSize = GetCachedFirstItemSizeFromHandler();
116+
if (cachedSize != CGSize.Empty)
117+
{
118+
_measuredSize = cachedSize.ToSize();
119+
// Even when we have a cached measurement, we still need to call Measure
120+
// to update the virtual view's internal state and bookkeeping
121+
virtualView.Measure(constraints.Width, _measuredSize.Height);
122+
}
123+
else
124+
{
125+
_measuredSize = virtualView.Measure(constraints.Width, constraints.Height);
126+
// If this is the first item being measured, cache it for MeasureFirstItem strategy
127+
SetCachedFirstItemSizeToHandler(_measuredSize.ToCGSize());
128+
}
118129
}
119130
else
120131
{
132+
// For headers/footers, always measure directly without using or updating the first-item cache
121133
_measuredSize = virtualView.Measure(constraints.Width, constraints.Height);
122-
// If this is the first item being measured, cache it for MeasureFirstItem strategy
123-
SetCachedFirstItemSizeToHandler(_measuredSize.ToCGSize());
124134
}
125135
_cachedConstraints = constraints;
126136
_needsArrange = true;
@@ -194,6 +204,7 @@ public override void LayoutSubviews()
194204
public override void PrepareForReuse()
195205
{
196206
//Unbind();
207+
isSupplementaryView = false;
197208
base.PrepareForReuse();
198209
}
199210

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
x:Class="Maui.Controls.Sample.Issues.Issue33130"
5+
xmlns:local="clr-namespace:Maui.Controls.Sample"
6+
Title="Issue33130">
7+
<Grid Margin="20"
8+
RowDefinitions="Auto, *">
9+
<StackLayout Grid.Row="0"
10+
Spacing="10"
11+
Margin="0,10">
12+
<Button Text="Switch to MeasureFirstItem"
13+
Clicked="OnSwitchToMeasureFirstItem"
14+
AutomationId="SwitchStrategyButton"/>
15+
<Label x:Name="StatusLabel"
16+
AutomationId="StatusLabel"
17+
Text="ItemSizingStrategy: MeasureAllItems"/>
18+
</StackLayout>
19+
20+
<local:CollectionView2 Grid.Row="1"
21+
x:Name="TestCollectionView"
22+
ItemsSource="{Binding Animals}"
23+
IsGrouped="true"
24+
AutomationId="TestCollectionView"
25+
ItemSizingStrategy="MeasureAllItems">
26+
<local:CollectionView2.ItemTemplate>
27+
<DataTemplate>
28+
<Grid Padding="10">
29+
<Grid.RowDefinitions>
30+
<RowDefinition Height="Auto"/>
31+
<RowDefinition Height="Auto"/>
32+
</Grid.RowDefinitions>
33+
<Grid.ColumnDefinitions>
34+
<ColumnDefinition Width="Auto"/>
35+
<ColumnDefinition Width="*"/>
36+
</Grid.ColumnDefinitions>
37+
<Image Grid.RowSpan="2"
38+
Source="{Binding ImageUrl}"
39+
Aspect="AspectFill"
40+
WidthRequest="80"
41+
HeightRequest="80"/>
42+
<Label Grid.Column="1"
43+
Text="{Binding Name}"
44+
FontAttributes="Bold"/>
45+
<Label Grid.Row="1"
46+
Grid.Column="1"
47+
Text="{Binding Location}"
48+
FontAttributes="Italic"
49+
VerticalOptions="End"/>
50+
</Grid>
51+
</DataTemplate>
52+
</local:CollectionView2.ItemTemplate>
53+
<local:CollectionView2.GroupHeaderTemplate>
54+
<DataTemplate>
55+
<Label x:Name="GroupHeaderLabel"
56+
Text="{Binding Name}"
57+
BackgroundColor="LightGray"
58+
FontSize="20"
59+
FontAttributes="Bold"
60+
AutomationId="GroupHeader"/>
61+
</DataTemplate>
62+
</local:CollectionView2.GroupHeaderTemplate>
63+
<local:CollectionView2.GroupFooterTemplate>
64+
<DataTemplate>
65+
<Label Text="{Binding Count, StringFormat='Total animals: {0:D}'}"
66+
Margin="0,0,0,10"/>
67+
</DataTemplate>
68+
</local:CollectionView2.GroupFooterTemplate>
69+
</local:CollectionView2>
70+
</Grid>
71+
</ContentPage>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.Collections.ObjectModel;
3+
using Microsoft.Maui.Controls;
4+
5+
namespace Maui.Controls.Sample.Issues;
6+
7+
[Issue(IssueTracker.Github, 33130, "CollectionView group header size changes with ItemSizingStrategy", PlatformAffected.iOS | PlatformAffected.macOS)]
8+
public partial class Issue33130 : ContentPage
9+
{
10+
public Issue33130()
11+
{
12+
InitializeComponent();
13+
BindingContext = new GroupedAnimalsViewModel();
14+
}
15+
16+
private void OnSwitchToMeasureFirstItem(object sender, EventArgs e)
17+
{
18+
TestCollectionView.ItemSizingStrategy = ItemSizingStrategy.MeasureFirstItem;
19+
StatusLabel.Text = $"ItemSizingStrategy: {TestCollectionView.ItemSizingStrategy}";
20+
}
21+
}
22+
23+
public class GroupedAnimalsViewModel
24+
{
25+
public ObservableCollection<GroupHeaderTestAnimalGroup> Animals { get; set; }
26+
27+
public GroupedAnimalsViewModel()
28+
{
29+
Animals = new ObservableCollection<GroupHeaderTestAnimalGroup>
30+
{
31+
new GroupHeaderTestAnimalGroup("Bears")
32+
{
33+
new GroupHeaderTestAnimal { Name = "Grizzly Bear", Location = "North America", ImageUrl = "bear.jpg" },
34+
new GroupHeaderTestAnimal { Name = "Polar Bear", Location = "Arctic", ImageUrl = "bear.jpg" },
35+
},
36+
new GroupHeaderTestAnimalGroup("Monkeys")
37+
{
38+
new GroupHeaderTestAnimal { Name = "Baboon", Location = "Africa", ImageUrl = "monkey.jpg" },
39+
new GroupHeaderTestAnimal { Name = "Capuchin Monkey", Location = "South America", ImageUrl = "monkey.jpg" },
40+
new GroupHeaderTestAnimal { Name = "Spider Monkey", Location = "Central America", ImageUrl = "monkey.jpg" },
41+
},
42+
new GroupHeaderTestAnimalGroup("Elephants")
43+
{
44+
new GroupHeaderTestAnimal { Name = "African Elephant", Location = "Africa", ImageUrl = "elephant.jpg" },
45+
new GroupHeaderTestAnimal { Name = "Asian Elephant", Location = "Asia", ImageUrl = "elephant.jpg" },
46+
}
47+
};
48+
}
49+
}
50+
51+
public class GroupHeaderTestAnimalGroup : ObservableCollection<GroupHeaderTestAnimal>
52+
{
53+
public string Name { get; set; }
54+
55+
public GroupHeaderTestAnimalGroup(string name) : base()
56+
{
57+
Name = name;
58+
}
59+
}
60+
61+
public class GroupHeaderTestAnimal
62+
{
63+
public string Name { get; set; }
64+
public string Location { get; set; }
65+
public string ImageUrl { get; set; }
66+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using NUnit.Framework;
2+
using UITest.Appium;
3+
using UITest.Core;
4+
5+
namespace Microsoft.Maui.TestCases.Tests.Issues;
6+
7+
public class Issue33130 : _IssuesUITest
8+
{
9+
public override string Issue => "CollectionView group header size changes with ItemSizingStrategy";
10+
11+
public Issue33130(TestDevice device) : base(device) { }
12+
[Test]
13+
[Category(UITestCategories.CollectionView)]
14+
public void GroupHeaderSizeShouldNotChangeWithItemSizingStrategy()
15+
{
16+
// Wait for the CollectionView to load
17+
App.WaitForElement("TestCollectionView");
18+
App.WaitForElement("GroupHeader");
19+
20+
// Get the initial header size (before changing ItemSizingStrategy)
21+
var headerElementBefore = App.FindElement("GroupHeader");
22+
var headerRectBefore = headerElementBefore.GetRect();
23+
24+
Assert.That(headerRectBefore.Height, Is.GreaterThan(0), "Header should have a height before strategy change");
25+
26+
// Switch ItemSizingStrategy
27+
App.WaitForElement("SwitchStrategyButton");
28+
App.Tap("SwitchStrategyButton");
29+
30+
// Get the header size after changing ItemSizingStrategy
31+
var headerElementAfter = App.FindElement("GroupHeader");
32+
var headerRectAfter = headerElementAfter.GetRect();
33+
34+
Assert.That(headerRectAfter.Height, Is.GreaterThan(0), "Header should have a height after strategy change");
35+
36+
// The header size should remain the same (within a small tolerance for rendering differences)
37+
// Allow for small rounding differences but not significant changes
38+
var heightDifference = Math.Abs(headerRectBefore.Height - headerRectAfter.Height);
39+
40+
// Assert that the height difference is minimal (less than 5 pixels tolerance)
41+
Assert.That(heightDifference, Is.EqualTo(0),
42+
$"Header height should not change significantly. Before: {headerRectBefore.Height}, After: {headerRectAfter.Height}, Difference: {heightDifference}");
43+
}
44+
}

0 commit comments

Comments
 (0)