Skip to content

Commit 3ce1420

Browse files
NanthiniMahalingamsheiksyedm
authored andcommitted
[iOS, macOS] Fixed CollectionView group header size changes with ItemSizingStrategy (#33161)
<!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Root cause - When the ItemSizingStrategy is set to measure the first item, the collection view’s grouped item header or footer template size is taken from the first item’s size instead of the actual size of the grouped item header or footer template. ### Description of changes - I have ignored the first measured item’s size when applying it to the grouped header or footer template, the collection view header or footer view, and the collection view header or footer template in PreferredLayoutAttributesFittingAttributes of TemplatedCell2. ### Issues Fixed Fixes #33130 Validated the behaviour in the following platforms - [x] Android - [x] Windows , - [x] iOS, - [x] MacOS ### Output **iOS** |Before|After| |--|--| | <video src="https://github.com/user-attachments/assets/deb143a4-5df0-463d-915c-16254bad658c" >| <video src="https://github.com/user-attachments/assets/56ba82de-f6d9-4b05-8ea7-2a12031282eb">| **macOS** |Before|After| |--|--| | <video src="https://github.com/user-attachments/assets/094b3e91-8198-483f-8b87-f3f5777e48b4" >| <video src="https://github.com/user-attachments/assets/5937e5c3-feb5-4138-8c47-9f7a31095a16">|
1 parent cff7f35 commit 3ce1420

7 files changed

Lines changed: 223 additions & 9 deletions

File tree

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: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
Title="Issue33130">
6+
<Grid Margin="20"
7+
RowDefinitions="Auto, *">
8+
<StackLayout Grid.Row="0"
9+
Spacing="10"
10+
Margin="0,10">
11+
<Button Text="Switch to MeasureFirstItem"
12+
Clicked="OnSwitchToMeasureFirstItem"
13+
AutomationId="SwitchStrategyButton"/>
14+
<Label x:Name="StatusLabel"
15+
AutomationId="StatusLabel"
16+
Text="ItemSizingStrategy: MeasureAllItems"/>
17+
</StackLayout>
18+
19+
<CollectionView Grid.Row="1"
20+
x:Name="TestCollectionView"
21+
ItemsSource="{Binding Animals}"
22+
IsGrouped="true"
23+
AutomationId="TestCollectionView"
24+
ItemSizingStrategy="MeasureAllItems">
25+
<CollectionView.Header>
26+
<Label Text="Animals Collection"
27+
FontSize="24"
28+
AutomationId="CollectionViewHeader"
29+
FontAttributes="Bold"
30+
HorizontalOptions="Center"/>
31+
</CollectionView.Header>
32+
<CollectionView.ItemTemplate>
33+
<DataTemplate>
34+
<Grid Padding="10">
35+
<Grid.RowDefinitions>
36+
<RowDefinition Height="Auto"/>
37+
<RowDefinition Height="Auto"/>
38+
</Grid.RowDefinitions>
39+
<Grid.ColumnDefinitions>
40+
<ColumnDefinition Width="Auto"/>
41+
<ColumnDefinition Width="*"/>
42+
</Grid.ColumnDefinitions>
43+
<Image Grid.RowSpan="2"
44+
Source="{Binding ImageUrl}"
45+
Aspect="AspectFill"
46+
WidthRequest="80"
47+
HeightRequest="80"/>
48+
<Label Grid.Column="1"
49+
Text="{Binding Name}"
50+
FontAttributes="Bold"/>
51+
<Label Grid.Row="1"
52+
Grid.Column="1"
53+
Text="{Binding Location}"
54+
FontAttributes="Italic"
55+
VerticalOptions="End"/>
56+
</Grid>
57+
</DataTemplate>
58+
</CollectionView.ItemTemplate>
59+
<CollectionView.GroupHeaderTemplate>
60+
<DataTemplate>
61+
<Label x:Name="GroupHeaderLabel"
62+
Text="{Binding Name}"
63+
BackgroundColor="LightGray"
64+
FontSize="20"
65+
FontAttributes="Bold"
66+
AutomationId="GroupHeader"/>
67+
</DataTemplate>
68+
</CollectionView.GroupHeaderTemplate>
69+
<CollectionView.GroupFooterTemplate>
70+
<DataTemplate>
71+
<Label Text="{Binding Count, StringFormat='Total animals: {0:D}'}"
72+
Margin="0,0,0,10"/>
73+
</DataTemplate>
74+
</CollectionView.GroupFooterTemplate>
75+
</CollectionView>
76+
</Grid>
77+
</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 Issue33130ViewModel();
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 Issue33130ViewModel
24+
{
25+
public ObservableCollection<Issue33130AnimalGroup> Animals { get; set; }
26+
27+
public Issue33130ViewModel()
28+
{
29+
Animals = new ObservableCollection<Issue33130AnimalGroup>
30+
{
31+
new Issue33130AnimalGroup("Bears")
32+
{
33+
new Issue33130Animal { Name = "Grizzly Bear", Location = "North America", ImageUrl = "bear.jpg" },
34+
new Issue33130Animal { Name = "Polar Bear", Location = "Arctic", ImageUrl = "bear.jpg" },
35+
},
36+
new Issue33130AnimalGroup("Monkeys")
37+
{
38+
new Issue33130Animal { Name = "Baboon", Location = "Africa", ImageUrl = "monkey.jpg" },
39+
new Issue33130Animal { Name = "Capuchin Monkey", Location = "South America", ImageUrl = "monkey.jpg" },
40+
new Issue33130Animal { Name = "Spider Monkey", Location = "Central America", ImageUrl = "monkey.jpg" },
41+
},
42+
new Issue33130AnimalGroup("Elephants")
43+
{
44+
new Issue33130Animal { Name = "African Elephant", Location = "Africa", ImageUrl = "elephant.jpg" },
45+
new Issue33130Animal { Name = "Asian Elephant", Location = "Asia", ImageUrl = "elephant.jpg" },
46+
}
47+
};
48+
}
49+
}
50+
51+
public class Issue33130AnimalGroup : ObservableCollection<Issue33130Animal>
52+
{
53+
public string Name { get; set; }
54+
55+
public Issue33130AnimalGroup(string name) : base()
56+
{
57+
Name = name;
58+
}
59+
}
60+
61+
public class Issue33130Animal
62+
{
63+
public string Name { get; set; }
64+
public string Location { get; set; }
65+
public string ImageUrl { get; set; }
66+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
App.WaitForElement("CollectionViewHeader");
20+
21+
// Get the initial header size (before changing ItemSizingStrategy)
22+
var headerElementBefore = App.FindElement("GroupHeader");
23+
var headerRectBefore = headerElementBefore.GetRect();
24+
25+
var collectionViewHeaderBefore = App.FindElement("CollectionViewHeader");
26+
var collectionViewHeaderRectBefore = collectionViewHeaderBefore.GetRect();
27+
28+
Assert.That(headerRectBefore.Height, Is.GreaterThan(0), "Header should have a height before strategy change");
29+
Assert.That(collectionViewHeaderRectBefore.Height, Is.GreaterThan(0), "CollectionView header should have a height before strategy change");
30+
31+
// Switch ItemSizingStrategy
32+
App.WaitForElement("SwitchStrategyButton");
33+
App.Tap("SwitchStrategyButton");
34+
35+
// Get the header size after changing ItemSizingStrategy
36+
var headerElementAfter = App.FindElement("GroupHeader");
37+
var headerRectAfter = headerElementAfter.GetRect();
38+
var collectionViewHeaderAfter = App.FindElement("CollectionViewHeader");
39+
var collectionViewHeaderRectAfter = collectionViewHeaderAfter.GetRect();
40+
41+
Assert.That(headerRectAfter.Height, Is.GreaterThan(0), "Header should have a height after strategy change");
42+
Assert.That(collectionViewHeaderRectAfter.Height, Is.GreaterThan(0), "CollectionView header should have a height after strategy change");
43+
44+
// The header size should remain the same (within a small tolerance for rendering differences)
45+
// Allow for small rounding differences but not significant changes
46+
var groupHeaderHeightDifference = Math.Abs(headerRectBefore.Height - headerRectAfter.Height);
47+
var collectionViewHeaderHeightDifference = Math.Abs(collectionViewHeaderRectBefore.Height - collectionViewHeaderRectAfter.Height);
48+
49+
// Assert that the height difference is minimal (less than 5 pixels tolerance)
50+
Assert.That(groupHeaderHeightDifference, Is.LessThan(5),
51+
$"Header height should not change significantly. Before: {headerRectBefore.Height}, After: {headerRectAfter.Height}, Difference: {groupHeaderHeightDifference}");
52+
53+
Assert.That(collectionViewHeaderHeightDifference, Is.LessThan(5), $"CollectionView header height should not change significantly. Before: {collectionViewHeaderRectBefore.Height}, After: {collectionViewHeaderRectAfter.Height}, Difference: {collectionViewHeaderHeightDifference}");
54+
}
55+
}

0 commit comments

Comments
 (0)