diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs index 937902e8e722..0ca0ec26110b 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs @@ -1060,8 +1060,8 @@ void RemoveScrollListener() if (RecyclerViewScrollListener == null) return; + RemoveOnScrollListener(RecyclerViewScrollListener); RecyclerViewScrollListener.Dispose(); - ClearOnScrollListeners(); RecyclerViewScrollListener = null; } } diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs new file mode 100644 index 000000000000..387a8079853e --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs @@ -0,0 +1,92 @@ +using System.Collections.ObjectModel; +using Maui.Controls.Sample.Issues; + +namespace Controls.TestCases.HostApp.Issues; + +[Issue(IssueTracker.Github, 35806, "Android CollectionView KeepScrollOffset stops working after replacing ItemsSource", PlatformAffected.Android)] +public class Issue35806 : TestContentPage +{ + ObservableCollection items; + CollectionView collectionView; + int sourceVersion = 1; + + protected override void Init() + { + items = CreateItemsSource(sourceVersion); + + collectionView = new CollectionView + { + AutomationId = "CollectionView35806", + ItemsSource = items, + ItemsUpdatingScrollMode = ItemsUpdatingScrollMode.KeepScrollOffset, + ItemTemplate = new DataTemplate(() => + { + var label = new Label(); + label.SetBinding(Label.TextProperty, "."); + return new Border + { + Content = label, + Padding = 10, + Margin = new Thickness(5), + BackgroundColor = Colors.LightGray, + }; + }) + }; + + var replaceSourceButton = new Button + { + Text = "Replace ItemsSource", + AutomationId = "ReplaceSourceButton", + Command = new Command(() => + { + sourceVersion++; + items = CreateItemsSource(sourceVersion); + collectionView.ItemsSource = items; + }) + }; + + var scrollToTopButton = new Button + { + Text = "Scroll To Top", + AutomationId = "ScrollToTopButton", + Command = new Command(() => + { + collectionView.ScrollTo(0, position: ScrollToPosition.Start, animate: false); + }) + }; + + var insertAtTopButton = new Button + { + Text = "Insert At Top", + AutomationId = "InsertAtTopButton", + Command = new Command(() => + { + items.Insert(0, $"Inserted-{items.Count + 1}"); + }) + }; + + var grid = new Grid + { + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Star } + }, + RowSpacing = 5 + }; + grid.Add(replaceSourceButton, 0, 0); + grid.Add(scrollToTopButton, 0, 1); + grid.Add(insertAtTopButton, 0, 2); + grid.Add(collectionView, 0, 3); + + Content = grid; + } + + static ObservableCollection CreateItemsSource(int version) + { + return new ObservableCollection( + Enumerable.Range(1, 30).Select(i => $"v{version}-Item {i}")); + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs new file mode 100644 index 000000000000..469c29468fbc --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs @@ -0,0 +1,43 @@ +#if ANDROID // This regression is Android-only: https://github.com/dotnet/maui/pull/29255 +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue35806 : _IssuesUITest +{ + public Issue35806(TestDevice device) : base(device) { } + + public override string Issue => "Android CollectionView KeepScrollOffset stops working after replacing ItemsSource"; + + [Test] + [Category(UITestCategories.CollectionView)] + public void KeepScrollOffsetWorksAfterReplacingItemsSource() + { + App.WaitForElement("CollectionView35806"); + + // Verify initial source loaded + App.WaitForElement("v1-Item 1"); + + // Replace source, scroll to top, insert at top — KeepScrollOffset should keep position + App.Click("ReplaceSourceButton"); + App.WaitForElement("v2-Item 1"); + App.Click("ScrollToTopButton"); + App.Click("InsertAtTopButton"); + + // With KeepScrollOffset at position 0, the inserted item should be visible at the top. + // Without the fix, "Inserted-31" is hidden above the viewport (broken KeepItemsInView behavior). + App.WaitForElement("Inserted-31"); + + // Replace source again to verify it still works on subsequent replacements + App.Click("ReplaceSourceButton"); + App.WaitForElement("v3-Item 1"); + App.Click("ScrollToTopButton"); + App.Click("InsertAtTopButton"); + + // After second replacement (30 items in new source), inserted item text is "Inserted-31" + App.WaitForElement("Inserted-31"); + } +} +#endif