Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public static void MapItemsUpdatingScrollMode(ItemsViewHandler<TItemsView> handl
protected virtual void UpdateLayout()
{
_layout = SelectLayout();
MapItemsUpdatingScrollMode(this, ItemsView);
Controller?.UpdateLayout(_layout);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,25 +156,26 @@ protected override UICollectionViewLayout SelectLayout()
}

var itemSizingStrategy = ItemsView.ItemSizingStrategy;
var itemsUpdatingScrollMode = ItemsView.ItemsUpdatingScrollMode;
var itemsLayout = ItemsView.ItemsLayout;

SubscribeToItemsLayoutPropertyChanged(itemsLayout);

if (itemsLayout is GridItemsLayout gridItemsLayout)
{
return LayoutFactory2.CreateGrid(gridItemsLayout, groupInfo, headerFooterInfo);
return LayoutFactory2.CreateGrid(gridItemsLayout, groupInfo, headerFooterInfo, itemsUpdatingScrollMode);
}

if (itemsLayout is LinearItemsLayout listItemsLayout)
{
return LayoutFactory2.CreateList(listItemsLayout, groupInfo, headerFooterInfo);
return LayoutFactory2.CreateList(listItemsLayout, groupInfo, headerFooterInfo, itemsUpdatingScrollMode);
}

// Fall back to vertical list
var fallbackItemsLayout = new LinearItemsLayout(ItemsLayoutOrientation.Vertical);
// Manually setting the value to ensure the property changed event is properly wired..
ItemsView.ItemsLayout = fallbackItemsLayout;
return LayoutFactory2.CreateList(fallbackItemsLayout, groupInfo, headerFooterInfo);
return LayoutFactory2.CreateList(fallbackItemsLayout, groupInfo, headerFooterInfo, itemsUpdatingScrollMode);
}

public static void MapHeaderTemplate(CollectionViewHandler2 handler, StructuredItemsView itemsView)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public static void MapIsVisible(ItemsViewHandler2<TItemsView> handler, ItemsView

public static void MapItemsUpdatingScrollMode(ItemsViewHandler2<TItemsView> handler, ItemsView itemsView)
{
// TODO: Fix handler._layout.ItemsUpdatingScrollMode = itemsView.ItemsUpdatingScrollMode;
handler.UpdateLayout();
}

//TODO: this is being called 2 times on startup, one from OnCreatePlatformView and otehr from the mapper for the layout
Expand Down
83 changes: 64 additions & 19 deletions src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ namespace Microsoft.Maui.Controls.Handlers.Items2;
internal static class LayoutFactory2
{
public static UICollectionViewLayout CreateList(LinearItemsLayout linearItemsLayout,
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo)
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo, ItemsUpdatingScrollMode itemsUpdatingScrollMode)
=> linearItemsLayout.Orientation == ItemsLayoutOrientation.Vertical
? CreateVerticalList(linearItemsLayout, groupingInfo, headerFooterInfo)
: CreateHorizontalList(linearItemsLayout, groupingInfo, headerFooterInfo);
? CreateVerticalList(linearItemsLayout, groupingInfo, headerFooterInfo, itemsUpdatingScrollMode)
: CreateHorizontalList(linearItemsLayout, groupingInfo, headerFooterInfo, itemsUpdatingScrollMode);

public static UICollectionViewLayout CreateGrid(GridItemsLayout gridItemsLayout,
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo)
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo, ItemsUpdatingScrollMode itemsUpdatingScrollMode)
=> gridItemsLayout.Orientation == ItemsLayoutOrientation.Vertical
? CreateVerticalGrid(gridItemsLayout, groupingInfo, headerFooterInfo)
: CreateHorizontalGrid(gridItemsLayout, groupingInfo, headerFooterInfo);
? CreateVerticalGrid(gridItemsLayout, groupingInfo, headerFooterInfo, itemsUpdatingScrollMode)
: CreateHorizontalGrid(gridItemsLayout, groupingInfo, headerFooterInfo, itemsUpdatingScrollMode);

static NSCollectionLayoutBoundarySupplementaryItem[] CreateSupplementaryItems(LayoutGroupingInfo? groupingInfo, LayoutHeaderFooterInfo? layoutHeaderFooterInfo,
UICollectionViewScrollDirection scrollDirection, NSCollectionLayoutDimension width, NSCollectionLayoutDimension height)
Expand Down Expand Up @@ -83,7 +83,7 @@ static NSCollectionLayoutBoundarySupplementaryItem[] CreateSupplementaryItems(La
return [];
}

static UICollectionViewLayout CreateListLayout(UICollectionViewScrollDirection scrollDirection, LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo layoutHeaderFooterInfo, LayoutSnapInfo snapInfo, NSCollectionLayoutDimension itemWidth, NSCollectionLayoutDimension itemHeight, NSCollectionLayoutDimension groupWidth, NSCollectionLayoutDimension groupHeight, double itemSpacing, Func<Thickness>? peekAreaInsetsFunc)
static UICollectionViewLayout CreateListLayout(UICollectionViewScrollDirection scrollDirection, LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo layoutHeaderFooterInfo, LayoutSnapInfo snapInfo, NSCollectionLayoutDimension itemWidth, NSCollectionLayoutDimension itemHeight, NSCollectionLayoutDimension groupWidth, NSCollectionLayoutDimension groupHeight, double itemSpacing, Func<Thickness>? peekAreaInsetsFunc, ItemsUpdatingScrollMode itemsUpdatingScrollMode)
{
var layoutConfiguration = new UICollectionViewCompositionalLayoutConfiguration();
layoutConfiguration.ScrollDirection = scrollDirection;
Expand Down Expand Up @@ -137,14 +137,14 @@ static UICollectionViewLayout CreateListLayout(UICollectionViewScrollDirection s
groupHeight);

return section;
}, layoutConfiguration);
}, layoutConfiguration, itemsUpdatingScrollMode);

return layout;
}



static UICollectionViewLayout CreateGridLayout(UICollectionViewScrollDirection scrollDirection, LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo, LayoutSnapInfo snapInfo, NSCollectionLayoutDimension itemWidth, NSCollectionLayoutDimension itemHeight, NSCollectionLayoutDimension groupWidth, NSCollectionLayoutDimension groupHeight, double verticalItemSpacing, double horizontalItemSpacing, int columns)
static UICollectionViewLayout CreateGridLayout(UICollectionViewScrollDirection scrollDirection, LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo, LayoutSnapInfo snapInfo, NSCollectionLayoutDimension itemWidth, NSCollectionLayoutDimension itemHeight, NSCollectionLayoutDimension groupWidth, NSCollectionLayoutDimension groupHeight, double verticalItemSpacing, double horizontalItemSpacing, int columns, ItemsUpdatingScrollMode itemsUpdatingScrollMode)
{
var layoutConfiguration = new UICollectionViewCompositionalLayoutConfiguration();
layoutConfiguration.ScrollDirection = scrollDirection;
Expand Down Expand Up @@ -189,13 +189,13 @@ static UICollectionViewLayout CreateGridLayout(UICollectionViewScrollDirection s
groupHeight);

return section;
}, layoutConfiguration);
}, layoutConfiguration, itemsUpdatingScrollMode);

return layout;
}

public static UICollectionViewLayout CreateVerticalList(LinearItemsLayout linearItemsLayout,
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo)
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo, ItemsUpdatingScrollMode itemsUpdatingScrollMode)
=> CreateListLayout(UICollectionViewScrollDirection.Vertical,
groupingInfo,
headerFooterInfo,
Expand All @@ -207,11 +207,12 @@ public static UICollectionViewLayout CreateVerticalList(LinearItemsLayout linear
NSCollectionLayoutDimension.CreateFractionalWidth(1f),
NSCollectionLayoutDimension.CreateEstimated(30f),
linearItemsLayout.ItemSpacing,
null);
null,
itemsUpdatingScrollMode);


public static UICollectionViewLayout CreateHorizontalList(LinearItemsLayout linearItemsLayout,
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo)
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo, ItemsUpdatingScrollMode itemsUpdatingScrollMode)
=> CreateListLayout(UICollectionViewScrollDirection.Horizontal,
groupingInfo,
headerFooterInfo,
Expand All @@ -223,10 +224,11 @@ public static UICollectionViewLayout CreateHorizontalList(LinearItemsLayout line
NSCollectionLayoutDimension.CreateEstimated(30f),
NSCollectionLayoutDimension.CreateFractionalHeight(1f),
linearItemsLayout.ItemSpacing,
null);
null,
itemsUpdatingScrollMode);

public static UICollectionViewLayout CreateVerticalGrid(GridItemsLayout gridItemsLayout,
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo)
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo, ItemsUpdatingScrollMode itemsUpdatingScrollMode)
=> CreateGridLayout(UICollectionViewScrollDirection.Vertical,
groupingInfo,
headerFooterInfo,
Expand All @@ -241,11 +243,12 @@ public static UICollectionViewLayout CreateVerticalGrid(GridItemsLayout gridItem
NSCollectionLayoutDimension.CreateEstimated(30f),
gridItemsLayout.VerticalItemSpacing,
gridItemsLayout.HorizontalItemSpacing,
gridItemsLayout.Span);
gridItemsLayout.Span,
itemsUpdatingScrollMode);


public static UICollectionViewLayout CreateHorizontalGrid(GridItemsLayout gridItemsLayout,
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo)
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo, ItemsUpdatingScrollMode itemsUpdatingScrollMode)
=> CreateGridLayout(UICollectionViewScrollDirection.Horizontal,
groupingInfo,
headerFooterInfo,
Expand All @@ -260,7 +263,8 @@ public static UICollectionViewLayout CreateHorizontalGrid(GridItemsLayout gridIt
NSCollectionLayoutDimension.CreateFractionalHeight(1f),
gridItemsLayout.VerticalItemSpacing,
gridItemsLayout.HorizontalItemSpacing,
gridItemsLayout.Span);
gridItemsLayout.Span,
itemsUpdatingScrollMode);


#nullable disable
Expand Down Expand Up @@ -396,9 +400,50 @@ public static UICollectionViewLayout CreateCarouselLayout(
class CustomUICollectionViewCompositionalLayout : UICollectionViewCompositionalLayout
{
LayoutSnapInfo _snapInfo;
public CustomUICollectionViewCompositionalLayout(LayoutSnapInfo snapInfo, UICollectionViewCompositionalLayoutSectionProvider sectionProvider, UICollectionViewCompositionalLayoutConfiguration configuration) : base(sectionProvider, configuration)
ItemsUpdatingScrollMode _itemsUpdatingScrollMode;

public CustomUICollectionViewCompositionalLayout(LayoutSnapInfo snapInfo, UICollectionViewCompositionalLayoutSectionProvider sectionProvider, UICollectionViewCompositionalLayoutConfiguration configuration, ItemsUpdatingScrollMode itemsUpdatingScrollMode) : base(sectionProvider, configuration)
{
_snapInfo = snapInfo;
_itemsUpdatingScrollMode = itemsUpdatingScrollMode;
}

public override void FinalizeCollectionViewUpdates()
{
base.FinalizeCollectionViewUpdates();

if (_itemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView)
Copy link
Preview

Copilot AI Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overriding FinalizeCollectionViewUpdates to scroll to the last item is a significant behavior change; please ensure tests verify the accuracy of this scrolling behavior.

Copilot uses AI. Check for mistakes.

{
ForceScrollToLastItem(CollectionView);
}
}

void ForceScrollToLastItem(UICollectionView collectionView)
{
var sections = (int)collectionView.NumberOfSections();

if (sections == 0)
{
return;
}

for (int section = sections - 1; section >= 0; section--)
{
var itemCount = collectionView.NumberOfItemsInSection(section);
if (itemCount > 0)
{
var lastIndexPath = NSIndexPath.FromItemSection(itemCount - 1, section);
if (Configuration.ScrollDirection == UICollectionViewScrollDirection.Vertical)
{
collectionView.ScrollToItem(lastIndexPath, UICollectionViewScrollPosition.Bottom, true);
}
else
{
collectionView.ScrollToItem(lastIndexPath, UICollectionViewScrollPosition.Right, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should look at RTL here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that the current implementation already supports RTL. I've included it in the UITest to be sure

}
return;
}
}
}

public override CGPoint TargetContentOffset(CGPoint proposedContentOffset, CGPoint scrollingVelocity)
Expand Down
49 changes: 49 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue28720.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue28720">
<Grid RowDefinitions="Auto,100,100,100">
<Button Text="Add Item"
Command="{Binding AddItemCommand}"
AutomationId="AddItemButton"
HorizontalOptions="Center"
VerticalOptions="Center"/>
<CollectionView
ItemsUpdatingScrollMode="KeepLastItemInView"
ItemsSource="{Binding Items}"
Grid.Row="1">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding . ,StringFormat='{0}cv1'}"/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<CollectionView
ItemsUpdatingScrollMode="KeepLastItemInView"
FlowDirection="RightToLeft"
ItemsSource="{Binding Items}"
Grid.Row="2">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding . ,StringFormat='{0}cv2'}"/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<CollectionView
ItemsUpdatingScrollMode="KeepLastItemInView"
ItemsSource="{Binding Items}"
Grid.Row="3">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding . ,StringFormat='{0}cv3'}"/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
33 changes: 33 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue28720.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Collections.ObjectModel;

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 28720, "Support for KeepLastItemInView for CV2", PlatformAffected.iOS)]
public partial class Issue28720 : ContentPage
{
private ObservableCollection<string> _items;
public ObservableCollection<string> Items
{
get => _items;
set
{
if (_items != value)
{
_items = value;
OnPropertyChanged();
}
}
}

public Command AddItemCommand => new(() =>
{
Items.Add($"Item{_items.Count}");
});

public Issue28720()
{
InitializeComponent();
Items = [.. Enumerable.Range(0, 20).Select(x => $"Item{x}")];
BindingContext = this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ public async Task LayoutPassesShouldNotIncrease()
var arrangePasses = int.Parse(match.Groups[2].Value);

#if IOS
var maxMeasurePasses = 225;
var maxArrangePasses = 247;
var maxMeasurePasses = 263;
var maxArrangePasses = 267;

if (App.FindElement("HeadingLabel").GetText() == "CollectionViewHandler2")
{
maxMeasurePasses = 380;
maxArrangePasses = 295;
maxMeasurePasses = 604;
maxArrangePasses = 588;
}
#elif ANDROID
const int maxMeasurePasses = 353;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;
public class Issue28720 : _IssuesUITest
{
public Issue28720(TestDevice testDevice) : base(testDevice)
{
}

public override string Issue => "Support for KeepLastItemInView for CV2";

[Test]
[Category(UITestCategories.CollectionView)]
public void KeepLastItemInViewShouldWork()
{
App.WaitForElement("AddItemButton");
App.Click("AddItemButton");
App.WaitForElement("Item20cv1");
App.WaitForElement("Item20cv2");
App.WaitForElement("Item20cv3");
}
}