diff --git a/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml b/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml
index 7363d18deadd..c3025222f6a5 100644
--- a/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml
+++ b/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml
@@ -2,4 +2,73 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.MainPage"
xmlns:local="clr-namespace:Maui.Controls.Sample">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml.cs b/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml.cs
index b7744fa262ea..2eb5019c23d4 100644
--- a/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml.cs
+++ b/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml.cs
@@ -1,9 +1,111 @@
-namespace Maui.Controls.Sample;
+using System.Collections.ObjectModel;
+
+namespace Maui.Controls.Sample;
public partial class MainPage : ContentPage
{
+ private ObservableCollection _items;
+ private int _scrollCount = 0;
+
public MainPage()
{
InitializeComponent();
+
+ // Initialize data source
+ _items = new ObservableCollection
+ {
+ "Item 1: Baboon",
+ "Item 2: Capuchin Monkey",
+ "Item 3: Blue Monkey",
+ "Item 4: Squirrel Monkey",
+ "Item 5: Golden Lion Tamarin"
+ };
+
+ TestCarouselView.ItemsSource = _items;
+ TestCarouselView.CurrentItem = _items[0];
+
+ // Subscribe to CurrentItemChanged event
+ TestCarouselView.CurrentItemChanged += OnCurrentItemChanged;
+
+ // Display handler info after layout
+ TestCarouselView.Loaded += (s, e) =>
+ {
+ var handlerType = TestCarouselView.Handler?.GetType().Name ?? "Unknown";
+ HandlerLabel.Text = $"Handler: {handlerType}";
+
+ Console.WriteLine("========== VERTICAL CAROUSELVIEW CENTERING TEST ==========");
+ Console.WriteLine($"Handler Type: {handlerType}");
+ Console.WriteLine($"Initial CurrentItem: {TestCarouselView.CurrentItem}");
+ Console.WriteLine($"Initial Position: {TestCarouselView.Position}");
+ Console.WriteLine($"Total Items: {_items.Count}");
+ Console.WriteLine($"Orientation: Vertical");
+ Console.WriteLine($"Loop: {TestCarouselView.Loop}");
+ Console.WriteLine("");
+ Console.WriteLine("INSTRUCTIONS:");
+ Console.WriteLine("1. Tap 'Scroll to Next Item' button repeatedly");
+ Console.WriteLine("2. Test scrolling past last item (Item 5) to see if it loops to Item 1");
+ Console.WriteLine("3. Watch CurrentItem updates in loop scenario");
+ Console.WriteLine("==========================================================");
+ };
+ }
+
+ private void OnScrollButtonClicked(object sender, EventArgs e)
+ {
+ _scrollCount++;
+ var targetIndex = _scrollCount % _items.Count;
+
+ Console.WriteLine("");
+ Console.WriteLine($"========== SCROLL #{_scrollCount} ==========");
+ Console.WriteLine($"Current Position: {TestCarouselView.Position}");
+ Console.WriteLine($"Current Item: {TestCarouselView.CurrentItem}");
+ Console.WriteLine($"Scrolling to index: {targetIndex} ({_items[targetIndex]})");
+
+ if (_scrollCount > _items.Count)
+ {
+ Console.WriteLine("⚠️ TESTING LOOP BEHAVIOR - scrolling past end of collection");
+ }
+
+ TestCarouselView.ScrollTo(targetIndex, position: ScrollToPosition.Center, animate: true);
+
+ // Wait for scroll animation and log results
+ Task.Run(async () =>
+ {
+ await Task.Delay(1500);
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ Console.WriteLine($"After scroll - Position: {TestCarouselView.Position}, CurrentItem: {TestCarouselView.CurrentItem}");
+
+ if (TestCarouselView.CurrentItem?.ToString() == _items[targetIndex])
+ {
+ Console.WriteLine("✅ CurrentItem updated correctly");
+ }
+ else
+ {
+ Console.WriteLine($"❌ CurrentItem mismatch! Expected: {_items[targetIndex]}, Got: {TestCarouselView.CurrentItem}");
+ }
+ Console.WriteLine("==========================================");
+ });
+ });
+ }
+
+ private void OnCurrentItemChanged(object? sender, CurrentItemChangedEventArgs e)
+ {
+ Console.WriteLine("");
+ Console.WriteLine("========== CurrentItemChanged EVENT ==========");
+ Console.WriteLine($"Previous: {e.PreviousItem ?? "null"}");
+ Console.WriteLine($"Current: {e.CurrentItem ?? "null"}");
+ Console.WriteLine($"Position: {TestCarouselView.Position}");
+ Console.WriteLine("==============================================");
+
+ if (e.CurrentItem != null)
+ {
+ CurrentItemLabel.Text = $"CurrentItem: {e.CurrentItem}";
+ Console.WriteLine($"✅ CurrentItem updated to: {e.CurrentItem}");
+ }
+ else
+ {
+ CurrentItemLabel.Text = "CurrentItem: (null)";
+ Console.WriteLine("⚠️ WARNING: CurrentItem is null");
+ }
}
}
\ No newline at end of file
diff --git a/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs b/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs
index d4941633bbd8..f0433a641837 100644
--- a/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs
+++ b/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs
@@ -1,7 +1,12 @@
-namespace Maui.Controls.Sample;
+namespace Maui.Controls.Sample;
public static class MauiProgram
{
+ // Toggle this to test different handlers
+ // true = Use Handler2 (CURRENT DEFAULT - the one being fixed in PR)
+ // false = Use Handler1 (LEGACY - to compare behavior)
+ private static bool UseHandler2 = true;
+
public static MauiApp CreateMauiApp() =>
MauiApp
.CreateBuilder()
@@ -9,6 +14,21 @@ public static MauiApp CreateMauiApp() =>
.UseMauiMaps()
#endif
.UseMauiApp()
+ .ConfigureMauiHandlers(handlers =>
+ {
+#if IOS || MACCATALYST
+ if (!UseHandler2)
+ {
+ // Force use of legacy Handler1 by overriding default
+ handlers.AddHandler();
+ Console.WriteLine("✅ Forcing CarouselViewHandler1 (legacy)");
+ }
+ else
+ {
+ Console.WriteLine("✅ Using CarouselViewHandler2 (current default)");
+ }
+#endif
+ })
.ConfigureFonts(fonts =>
{
fonts.AddFont("Dokdo-Regular.ttf", "Dokdo");
diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs
index 311d660d7249..a7f198e580f2 100644
--- a/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs
+++ b/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs
@@ -361,23 +361,24 @@ public static UICollectionViewLayout CreateCarouselLayout(
return;
}
- // Calculate page index accounting for ItemSpacing
+ // Calculate page index accounting for ItemSpacing on the active axis.
var itemSpacing = itemsView.ItemsLayout is LinearItemsLayout linearLayout ? linearLayout.ItemSpacing : 0;
+ var effectivePageSize = (isHorizontal
+ ? env.Container.ContentSize.Width
+ : env.Container.ContentSize.Height) - sectionMargin * 2 + itemSpacing;
- var effectiveItemWidth = env.Container.ContentSize.Width - sectionMargin * 2 + itemSpacing;
-
- if (effectiveItemWidth <= 0)
+ if (effectivePageSize <= 0)
{
return;
}
var pageOffset = isHorizontal ? offset.X : offset.Y;
- var pageSize = isHorizontal
- ? env.Container.ContentSize.Width
- : env.Container.ContentSize.Height;
- double page = (pageOffset + sectionMargin) / effectiveItemWidth;
+ double page = (pageOffset + sectionMargin) / effectivePageSize;
- if (Math.Abs(page % 1) > (double.Epsilon * 100) || cv2Controller.ItemsSource.ItemCount <= 0)
+ // Vertical: Scroll stops wherever touch is released (no auto-centering), so use 0.1 (10%) threshold
+ // to accept positions near page boundaries where scroll naturally settles
+ double pageThreshold = isHorizontal ? (double.Epsilon * 100) : 0.1;
+ if (Math.Abs(page % 1) > pageThreshold || cv2Controller.ItemsSource.ItemCount <= 0)
{
return;
}
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue32136.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue32136.cs
new file mode 100644
index 000000000000..647e720bdd67
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue32136.cs
@@ -0,0 +1,76 @@
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, 32136, "CarouselView CurrentItem Not Updating with Vertical LinearItemsLayout", PlatformAffected.iOS)]
+public class Issue32136 : ContentPage
+{
+ public Issue32136()
+ {
+ CarouselView2 carouselView = new CarouselView2
+ {
+ HeightRequest = 400,
+ Loop = false,
+ BackgroundColor = Colors.LightGray,
+ AutomationId = "TestCarouselView",
+ ItemsLayout = new LinearItemsLayout(ItemsLayoutOrientation.Vertical),
+ ItemTemplate = new DataTemplate(() =>
+ {
+ Label label = new Label
+ {
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.Center
+ };
+ label.SetBinding(Label.TextProperty, ".");
+
+ return new Grid
+ {
+ Children = { label }
+ };
+ }),
+ ItemsSource = new string[]
+ {
+ "Baboon",
+ "Capuchin Monkey",
+ "Blue Monkey",
+ "Squirrel Monkey",
+ "Golden Lion Tamarin"
+ }
+ };
+
+ Label currentItemLabel = new Label();
+ currentItemLabel.AutomationId = "CurrentItemLabel";
+ currentItemLabel.Text = "CurrentItem = Baboon";
+
+ Button button = new Button
+ {
+ Text = "Next Item",
+ AutomationId = "ScrollButton"
+ };
+ button.Clicked += (s, e) =>
+ {
+ currentItemLabel.Text = "Button was clicked";
+ carouselView.ScrollTo(carouselView.Position + 1, position: ScrollToPosition.Center, animate: true);
+ };
+
+ carouselView.CurrentItemChanged += (s, e) =>
+ {
+ currentItemLabel.Text = $"CurrentItem = {e.CurrentItem}";
+ };
+
+ Grid grid = new Grid
+ {
+ Padding = 25,
+ RowSpacing = 10,
+ RowDefinitions =
+ {
+ new RowDefinition(GridLength.Auto),
+ new RowDefinition(GridLength.Auto),
+ new RowDefinition(GridLength.Auto)
+ }
+ };
+
+ grid.Add(carouselView);
+ grid.Add(currentItemLabel, row: 1);
+ grid.Add(button, row: 2);
+ Content = grid;
+ }
+}
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32136.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32136.cs
new file mode 100644
index 000000000000..c958d69d5688
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32136.cs
@@ -0,0 +1,24 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+
+public class Issue32136 : _IssuesUITest
+{
+ public Issue32136(TestDevice device) : base(device)
+ {
+ }
+
+ public override string Issue => "CarouselView CurrentItem Not Updating with Vertical LinearItemsLayout";
+
+ [Test]
+ [Category(UITestCategories.CarouselView)]
+ public void CurrentItemShouldUpdateWhenScrollingVerticalCarouselView()
+ {
+ App.WaitForElement("ScrollButton");
+ App.Tap("ScrollButton");
+ var currentItemText = App.WaitForElement("CurrentItemLabel").GetText();
+ Assert.That(currentItemText, Is.EqualTo("CurrentItem = Capuchin Monkey"));
+ }
+}
\ No newline at end of file