Skip to content

Commit 79b2a24

Browse files
committed
Fix CarouselView layout SR6 regressions
1 parent 5f986dd commit 79b2a24

File tree

14 files changed

+294
-48
lines changed

14 files changed

+294
-48
lines changed

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ public override void ConstrainTo(CGSize constraint)
3232

3333
public override CGSize Measure()
3434
{
35-
return new CGSize(_constraint.Width, _constraint.Height);
35+
// Go through the measure pass even if the constraints are fixed
36+
// to ensure arrange pass has the appropriate desired size in place.
37+
PlatformHandler.VirtualView.Measure(_constraint.Width, _constraint.Height);
38+
return _constraint;
3639
}
3740

3841
protected override (bool, Size) NeedsContentSizeUpdate(Size currentSize)
@@ -42,7 +45,7 @@ protected override (bool, Size) NeedsContentSizeUpdate(Size currentSize)
4245

4346
protected override bool AttributesConsistentWithConstrainedDimension(UICollectionViewLayoutAttributes attributes)
4447
{
45-
return false;
48+
return _constraint.IsCloseTo(attributes.Frame.Size);
4649
}
4750
}
4851
}

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

+3
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ protected override string DetermineCellReuseId(NSIndexPath indexPath)
199199
return base.DetermineCellReuseId(NSIndexPath.FromItemSection(itemIndex, 0));
200200
}
201201

202+
private protected override (Type CellType, string CellTypeReuseId) DetermineTemplatedCellType()
203+
=> (typeof(CarouselTemplatedCell), "maui_carousel");
204+
202205
protected override void RegisterViewTypes()
203206
{
204207
CollectionView.RegisterClassForCell(typeof(CarouselTemplatedCell), CarouselTemplatedCell.ReuseId);

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -503,9 +503,9 @@ protected virtual string DetermineCellReuseId(NSIndexPath indexPath)
503503
var dataTemplate = ItemsView.ItemTemplate.SelectDataTemplate(item, ItemsView);
504504

505505
var cellOrientation = ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Vertical ? "v" : "h";
506-
var cellType = ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Vertical ? typeof(VerticalCell) : typeof(HorizontalCell);
506+
(Type cellType, var cellTypeReuseId) = DetermineTemplatedCellType();
507507

508-
var reuseId = $"_maui_{cellOrientation}_{dataTemplate.Id}";
508+
var reuseId = $"_{cellTypeReuseId}_{cellOrientation}_{dataTemplate.Id}";
509509

510510
if (!_cellReuseIds.Contains(reuseId))
511511
{
@@ -521,6 +521,11 @@ protected virtual string DetermineCellReuseId(NSIndexPath indexPath)
521521
: VerticalDefaultCell.ReuseId;
522522
}
523523

524+
private protected virtual (Type CellType, string CellTypeReuseId) DetermineTemplatedCellType()
525+
{
526+
return (ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Vertical ? typeof(VerticalCell) : typeof(HorizontalCell), "maui");
527+
}
528+
524529
[Obsolete("Use DetermineCellReuseId(NSIndexPath indexPath) instead.")]
525530
protected virtual string DetermineCellReuseId()
526531
{

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

+5-6
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ protected override CarouselViewController2 CreateController(CarouselView newElem
3838

3939
protected override UICollectionViewLayout SelectLayout()
4040
{
41-
bool IsHorizontal = VirtualView.ItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal;
42-
UICollectionViewScrollDirection scrollDirection = IsHorizontal ? UICollectionViewScrollDirection.Horizontal : UICollectionViewScrollDirection.Vertical;
41+
bool isHorizontal = VirtualView.ItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal;
4342

4443
NSCollectionLayoutDimension itemWidth = NSCollectionLayoutDimension.CreateFractionalWidth(1);
4544
NSCollectionLayoutDimension itemHeight = NSCollectionLayoutDimension.CreateFractionalHeight(1);
@@ -55,7 +54,7 @@ protected override UICollectionViewLayout SelectLayout()
5554
return null;
5655
}
5756
double sectionMargin = 0.0;
58-
if (!IsHorizontal)
57+
if (!isHorizontal)
5958
{
6059
sectionMargin = VirtualView.PeekAreaInsets.VerticalThickness / 2;
6160
var newGroupHeight = environment.Container.ContentSize.Height - VirtualView.PeekAreaInsets.VerticalThickness;
@@ -81,19 +80,19 @@ protected override UICollectionViewLayout SelectLayout()
8180

8281
if (OperatingSystem.IsIOSVersionAtLeast(16))
8382
{
84-
group = IsHorizontal ? NSCollectionLayoutGroup.GetHorizontalGroup(groupSize, item, 1) :
83+
group = isHorizontal ? NSCollectionLayoutGroup.GetHorizontalGroup(groupSize, item, 1) :
8584
NSCollectionLayoutGroup.GetVerticalGroup(groupSize, item, 1);
8685
}
8786
else
8887
{
89-
group = IsHorizontal ? NSCollectionLayoutGroup.CreateHorizontal(groupSize, item, 1) :
88+
group = isHorizontal ? NSCollectionLayoutGroup.CreateHorizontal(groupSize, item, 1) :
9089
NSCollectionLayoutGroup.CreateVertical(groupSize, item, 1);
9190
}
9291

9392
// Create our section layout
9493
var section = NSCollectionLayoutSection.Create(group: group);
9594
section.InterGroupSpacing = itemSpacing;
96-
section.OrthogonalScrollingBehavior = IsHorizontal ? UICollectionLayoutSectionOrthogonalScrollingBehavior.GroupPagingCentered : UICollectionLayoutSectionOrthogonalScrollingBehavior.None;
95+
section.OrthogonalScrollingBehavior = isHorizontal ? UICollectionLayoutSectionOrthogonalScrollingBehavior.GroupPagingCentered : UICollectionLayoutSectionOrthogonalScrollingBehavior.None;
9796
section.VisibleItemsInvalidationHandler = (items, offset, env) =>
9897
{
9998
//This will allow us to SetPosition when we are scrolling the items
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#nullable disable
2+
using CoreGraphics;
3+
using Foundation;
4+
using Microsoft.Maui.Graphics;
5+
using UIKit;
6+
7+
namespace Microsoft.Maui.Controls.Handlers.Items2
8+
{
9+
internal sealed class CarouselTemplatedCell2 : TemplatedCell2
10+
{
11+
internal new const string ReuseId = "Microsoft.Maui.Controls.CarouselTemplatedCell2";
12+
13+
[Export("initWithFrame:")]
14+
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
15+
public CarouselTemplatedCell2(CGRect frame) : base(frame)
16+
{
17+
}
18+
19+
private protected override Size GetMeasureConstraints(UICollectionViewLayoutAttributes preferredAttributes)
20+
=> preferredAttributes.Size.ToSize();
21+
}
22+
}

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

+24-26
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ protected override string DetermineCellReuseId(NSIndexPath indexPath)
117117
return base.DetermineCellReuseId(itemIndex);
118118
}
119119

120+
private protected override (Type CellType, string CellTypeReuseId) DetermineTemplatedCellType()
121+
=> (typeof(CarouselTemplatedCell2), CarouselTemplatedCell2.ReuseId);
122+
120123
protected override Items.IItemsViewSource CreateItemsViewSource()
121124
{
122125
var itemsSource = ItemsSourceFactory2.CreateForCarouselView(ItemsView.ItemsSource, this, ItemsView.Loop);
@@ -506,54 +509,49 @@ async Task UpdateInitialPosition()
506509
return;
507510
}
508511

509-
int position = carousel.Position;
510-
var currentItem = carousel.CurrentItem;
511-
512-
if (currentItem != null)
513-
{
514-
// Sometimes the item could be just being removed while we navigate back to the CarouselView
515-
var positionCurrentItem = ItemsSource.GetIndexForItem(currentItem).Row;
516-
if (positionCurrentItem != -1)
517-
{
518-
position = positionCurrentItem;
519-
}
520-
}
521-
522-
var projectedPosition = NSIndexPath.FromItemSection(position, _section);
523-
524-
if (LoopItemsSource.Loop)
525-
{
526-
//We need to set the position to the correct position since we added 1 item at the beginning
527-
projectedPosition = GetScrollToIndexPath(position);
528-
}
529-
530-
var uICollectionViewScrollPosition = IsHorizontal ? UICollectionViewScrollPosition.CenteredHorizontally : UICollectionViewScrollPosition.CenteredVertically;
531-
532-
await Task.Delay(100).ContinueWith((t) =>
512+
await Task.Delay(100).ContinueWith(_ =>
533513
{
534514
MainThread.BeginInvokeOnMainThread(() =>
535515
{
536516
if (!IsViewLoaded)
537517
{
538518
return;
539519
}
520+
540521
InitialPositionSet = true;
541522

542523
if (ItemsSource is null || ItemsSource.ItemCount == 0)
543524
{
544525
return;
545526
}
546527

528+
int position = carousel.Position;
529+
var currentItem = carousel.CurrentItem;
530+
531+
if (currentItem != null)
532+
{
533+
// Sometimes the item could be just being removed while we navigate back to the CarouselView
534+
var positionCurrentItem = ItemsSource.GetIndexForItem(currentItem).Row;
535+
if (positionCurrentItem != -1)
536+
{
537+
position = positionCurrentItem;
538+
}
539+
}
540+
541+
var projectedPosition = LoopItemsSource.Loop
542+
? GetScrollToIndexPath(position) // We need to set the position to the correct position since we added 1 item at the beginning
543+
: NSIndexPath.FromItemSection(position, _section);
544+
545+
var uICollectionViewScrollPosition = IsHorizontal ? UICollectionViewScrollPosition.CenteredHorizontally : UICollectionViewScrollPosition.CenteredVertically;
546+
547547
CollectionView.ScrollToItem(projectedPosition, uICollectionViewScrollPosition, false);
548548

549549
//Set the position on VirtualView to update the CurrentItem also
550550
SetPosition(position);
551551

552552
UpdateVisualStates();
553553
});
554-
555554
});
556-
557555
}
558556

559557
void UpdateVisualStates()

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ public void UpdateLayout(UICollectionViewLayout newLayout)
6161

6262
if (newLayout is UICollectionViewCompositionalLayout compositionalLayout)
6363
{
64+
// Note: on carousel layout, the scroll direction is always vertical to achieve horizontal paging with snapping.
65+
// Thanks to it, we can use OrthogonalScrollingBehavior.GroupPagingCentered to scroll the section horizontally.
66+
// And even if CarouselView is vertically oriented, each section scrolls horizontally — which results in the carousel-style behavior.
6467
ScrollDirection = compositionalLayout.Configuration.ScrollDirection;
6568
}
6669

@@ -330,10 +333,9 @@ protected virtual string DetermineCellReuseId(NSIndexPath indexPath)
330333

331334
var dataTemplate = ItemsView.ItemTemplate.SelectDataTemplate(item, ItemsView);
332335

333-
var cellType = typeof(TemplatedCell2);
334-
335336
var orientation = ScrollDirection == UICollectionViewScrollDirection.Horizontal ? "Horizontal" : "Vertical";
336-
var reuseId = $"{TemplatedCell2.ReuseId}.{orientation}.{dataTemplate.Id}";
337+
(Type cellType, var cellTypeReuseId) = DetermineTemplatedCellType();
338+
var reuseId = $"{cellTypeReuseId}.{orientation}.{dataTemplate.Id}";
337339

338340
if (!_cellReuseIds.Contains(reuseId))
339341
{
@@ -347,6 +349,9 @@ protected virtual string DetermineCellReuseId(NSIndexPath indexPath)
347349
return ScrollDirection == UICollectionViewScrollDirection.Horizontal ? HorizontalDefaultCell2.ReuseId : VerticalDefaultCell2.ReuseId;
348350
}
349351

352+
private protected virtual (Type CellType, string CellTypeReuseId) DetermineTemplatedCellType()
353+
=> (typeof(TemplatedCell2), TemplatedCell2.ReuseId);
354+
350355
protected virtual void RegisterViewTypes()
351356
{
352357
CollectionView.RegisterClassForCell(typeof(HorizontalDefaultCell2), HorizontalDefaultCell2.ReuseId);

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

+15-6
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,7 @@ public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittin
8686

8787
if (PlatformHandler?.VirtualView is { } virtualView)
8888
{
89-
var constraints = ScrollDirection == UICollectionViewScrollDirection.Vertical
90-
? new Size(preferredAttributes.Size.Width, double.PositiveInfinity)
91-
: new Size(double.PositiveInfinity, preferredAttributes.Size.Height);
89+
var constraints = GetMeasureConstraints(preferredAttributes);
9290

9391
if (_measureInvalidated || _cachedConstraints != constraints)
9492
{
@@ -98,9 +96,12 @@ public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittin
9896
_needsArrange = true;
9997
}
10098

101-
var size = ScrollDirection == UICollectionViewScrollDirection.Vertical
102-
? new Size(preferredAttributes.Size.Width, _measuredSize.Height)
103-
: new Size(_measuredSize.Width, preferredAttributes.Size.Height);
99+
var preferredSize = preferredAttributes.Size;
100+
// Use measured size only when unconstrained
101+
var size = new Size(
102+
double.IsPositiveInfinity(constraints.Width) ? _measuredSize.Width : preferredSize.Width,
103+
double.IsPositiveInfinity(constraints.Height) ? _measuredSize.Height : preferredSize.Height
104+
);
104105

105106
preferredAttributes.Frame = new CGRect(preferredAttributes.Frame.Location, size);
106107
preferredAttributes.ZIndex = 2;
@@ -111,6 +112,14 @@ public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittin
111112
return preferredAttributes;
112113
}
113114

115+
private protected virtual Size GetMeasureConstraints(UICollectionViewLayoutAttributes preferredAttributes)
116+
{
117+
var constraints = ScrollDirection == UICollectionViewScrollDirection.Vertical
118+
? new Size(preferredAttributes.Size.Width, double.PositiveInfinity)
119+
: new Size(double.PositiveInfinity, preferredAttributes.Size.Height);
120+
return constraints;
121+
}
122+
114123
public override void LayoutSubviews()
115124
{
116125
base.LayoutSubviews();

src/Controls/tests/TestCases.HostApp/Issues/CarouselViewLoopNoFreeze.cs

-2
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,6 @@ void ExecuteRemoveItemsCommand()
100100
while (Items.Count > 0)
101101
{
102102
Items.Remove(Items.Last());
103-
Items.Remove(Items.Last());
104-
Items.Remove(Items.Last());
105103
}
106104
RemoveAllItemsCommand.ChangeCanExecute();
107105
RemoveLastItemCommand.ChangeCanExecute();

0 commit comments

Comments
 (0)