From 7afd085da14c692d428e83dd632ea72b5a7fdd9c Mon Sep 17 00:00:00 2001 From: prakashKannanSf3972 <127308739+prakashKannanSf3972@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:18:10 +0530 Subject: [PATCH 1/4] Fixed-KeepScrollOffset-Issue --- .../Items/Android/MauiRecyclerView.cs | 9 ++ .../Handlers/Items/Android/ScrollHelper.cs | 41 ++++++--- .../TestCases.HostApp/Issues/Issue29131.cs | 85 +++++++++++++++++++ .../Tests/Issues/Issue29131.cs | 31 +++++++ 4 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs create mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs index 145fd020ced3..e4910808cc07 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs @@ -382,6 +382,15 @@ protected virtual void UpdateItemsUpdatingScrollMode() if (ItemsViewAdapter == null || ItemsView == null) return; + if (ItemsView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepScrollOffset) + { + ScrollHelper.AddScrollListener(); + } + else + { + ScrollHelper.RemoveScrollListener(); + } + if (ItemsView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView) { // Keeping the current items in view is the default, so we don't need to watch for data changes diff --git a/src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs b/src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs index 9c20d6dd6ffd..07f8b110f806 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs @@ -12,7 +12,7 @@ internal class ScrollHelper : RecyclerView.OnScrollListener bool _undoNextScrollAdjustment; bool _maintainingScrollOffsets; - + bool isFirstItemReached = true; int _lastScrollX; int _lastScrollY; int _lastDeltaX; @@ -26,13 +26,6 @@ public ScrollHelper(RecyclerView recyclerView) // Used by the renderer to maintain scroll offset when using ItemsUpdatingScrollMode KeepScrollOffset public void UndoNextScrollAdjustment() { - // Don't start tracking the scroll offsets until we really need to - if (!_maintainingScrollOffsets) - { - _maintainingScrollOffsets = true; - _recyclerView.AddOnScrollListener(this); - } - _undoNextScrollAdjustment = true; _lastScrollX = _recyclerView.ComputeHorizontalScrollOffset(); @@ -212,12 +205,20 @@ void TrackOffsets() // offset to shift; since the ItemsUpdatingScrollMode is set to KeepScrollOffset; we need to undo // that shift and stay where we were before the item was added - _undoNextScrollAdjustment = false; - _recyclerView.ScrollBy(-_lastDeltaX, -_lastDeltaY); + if (isFirstItemReached) + { + _recyclerView.ScrollBy(-_lastDeltaX, -_lastDeltaY); + } + _undoNextScrollAdjustment = false; _lastDeltaX = 0; _lastDeltaY = 0; } + else + { + isFirstItemReached = newXOffset == 0 + && newYOffset == 0; + } } public override void OnScrolled(RecyclerView recyclerView, int dx, int dy) @@ -225,5 +226,25 @@ public override void OnScrolled(RecyclerView recyclerView, int dx, int dy) base.OnScrolled(recyclerView, dx, dy); TrackOffsets(); } + + internal void AddScrollListener() + { + // Set up scroll listener to track the scroll offsets when we're using KeepScrollOffset. + if (!_maintainingScrollOffsets) + { + _maintainingScrollOffsets = true; + _recyclerView.AddOnScrollListener(this); + } + } + + internal void RemoveScrollListener() + { + // Remove the scroll listener when we're done and no longer need to track the offsets. + if (_maintainingScrollOffsets) + { + _maintainingScrollOffsets = false; + _recyclerView.RemoveOnScrollListener(this); + } + } } } diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs new file mode 100644 index 000000000000..f2c967cf06df --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs @@ -0,0 +1,85 @@ +using System.Collections.ObjectModel; +using Maui.Controls.Sample.Issues; + +namespace Controls.TestCases.HostApp.Issues; + +[Issue(IssueTracker.Github, 29131, "Android - KeepScrollOffset doesn't not works as expected when new items are added in CollectionView", PlatformAffected.Android)] +public class Issue29131 : TestContentPage +{ + ObservableCollection items; + CollectionView collectionView; + int count = 1; + + protected override void Init() + { + items = new ObservableCollection(Enumerable.Range(1, 30).Select(i => $"Item {i}")); + + Button keepScrollOffsetButton = CreateButton("KeepScrollOffset", "KeepScrollOffsetButton", OnKeepScrollOffsetClicked); + Button addButton = CreateButton("Add Item to Top", "AddNewItem", OnAddItemClicked); + Button scrollButton = CreateButton("Scroll CollectionView", "ScrollButton", OnScrollButtonClicked); + + collectionView = new CollectionView + { + AutomationId = "CollectionView", + ItemsSource = items, + 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, + }; + }) + }; + + Grid 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(keepScrollOffsetButton, 0, 0); + grid.Add(addButton, 0, 1); + grid.Add(scrollButton, 0, 2); + grid.Add(collectionView, 0, 3); + + Content = grid; + } + + Button CreateButton(string text, string automationId, EventHandler onClick) + { + return new Button + { + Text = text, + AutomationId = automationId, + Command = new Command(_ => onClick(this, EventArgs.Empty)) + }; + } + + void OnKeepScrollOffsetClicked(object sender, EventArgs e) + { + collectionView.ItemsUpdatingScrollMode = ItemsUpdatingScrollMode.KeepScrollOffset; + } + + void OnScrollButtonClicked(object sender, EventArgs e) + { + int index = (count % 2 == 0) ? 0 : items.Count - 1; + var position = (count % 2 == 0) ? ScrollToPosition.Start : ScrollToPosition.End; + collectionView.ScrollTo(index, position: position, animate: true); + count++; + } + + void OnAddItemClicked(object sender, EventArgs e) + { + items.Insert(0, $"Item {items.Count + 1}"); + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs new file mode 100644 index 000000000000..f482ea01c6ce --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue29131 : _IssuesUITest +{ + public Issue29131(TestDevice device) : base(device) { } + + public override string Issue => "Android - KeepScrollOffset doesn't not works as expected when new items are added in CollectionView"; + const string AddNewItem = "AddNewItem"; + const string ScrollButton = "ScrollButton"; + + [Test] + [Category(UITestCategories.CollectionView)] + public void KeepScrollOffSetShouldWork() + { + App.WaitForElement("CollectionView"); + App.Click("KeepScrollOffsetButton"); + App.Click(ScrollButton); + App.Click(AddNewItem); + App.WaitForElement("Item 30"); + App.Click(ScrollButton); + App.Click(AddNewItem); + App.WaitForElement("Item 32"); + App.Click(ScrollButton); + App.Click(AddNewItem); + App.WaitForElement("Item 30"); + } +} \ No newline at end of file From 5d662a4f1ecd40c1ce5413ca3c30cdac64466f5e Mon Sep 17 00:00:00 2001 From: prakashKannanSf3972 <127308739+prakashKannanSf3972@users.noreply.github.com> Date: Wed, 30 Apr 2025 20:34:11 +0530 Subject: [PATCH 2/4] Updated-Test --- ...nViewUITests.CollectionViewItemsUpdatingScrollMode.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs index 09a8d93c8444..8327468b3214 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs @@ -47,8 +47,13 @@ public void KeepScrollOffset() App.WaitForElement("ScrollToMiddle"); App.Click("ScrollToMiddle"); App.WaitForElement("Vegetables.jpg, 10"); - App.Click("AddItemAbove"); - App.WaitForElement("photo.jpg, 9"); + + for (int i = 0; i < 5; i++) + { + App.Click("AddItemAbove"); + } + + App.WaitForElement("FlowerBuds.jpg, 12"); } // KeepLastItemInView(src\Compatibility\ControlGallery\src\Issues.Shared\CollectionViewItemsUpdatingScrollMode.cs) From 6ba935b9a12fe64a68aa6e929cd9ee93e120a5c9 Mon Sep 17 00:00:00 2001 From: HarishwaranVijayakumar Date: Wed, 11 Mar 2026 16:15:06 +0530 Subject: [PATCH 3/4] Address AI concern --- .../src/Core/Handlers/Items/Android/ScrollHelper.cs | 8 ++++---- src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs | 2 +- ...onViewUITests.CollectionViewItemsUpdatingScrollMode.cs | 4 ++++ .../TestCases.Shared.Tests/Tests/Issues/Issue29131.cs | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs b/src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs index 07f8b110f806..6961491ce28a 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs @@ -12,7 +12,7 @@ internal class ScrollHelper : RecyclerView.OnScrollListener bool _undoNextScrollAdjustment; bool _maintainingScrollOffsets; - bool isFirstItemReached = true; + bool _isAtScrollOrigin = true; int _lastScrollX; int _lastScrollY; int _lastDeltaX; @@ -205,7 +205,7 @@ void TrackOffsets() // offset to shift; since the ItemsUpdatingScrollMode is set to KeepScrollOffset; we need to undo // that shift and stay where we were before the item was added - if (isFirstItemReached) + if (_isAtScrollOrigin) { _recyclerView.ScrollBy(-_lastDeltaX, -_lastDeltaY); } @@ -216,8 +216,8 @@ void TrackOffsets() } else { - isFirstItemReached = newXOffset == 0 - && newYOffset == 0; + _isAtScrollOrigin = newXOffset == 0 + && newYOffset == 0; } } diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs index f2c967cf06df..201a8cdc5817 100644 --- a/src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs @@ -82,4 +82,4 @@ void OnAddItemClicked(object sender, EventArgs e) { items.Insert(0, $"Item {items.Count + 1}"); } -} \ No newline at end of file +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs index 8327468b3214..54ecddc436c1 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs @@ -36,6 +36,10 @@ public void KeepItemsInView() #if TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST // The test fails on iOS and macOS because Appium is unable to locate the Picker control elements resulting in a TimeoutException. For more information, see: https://github.com/dotnet/maui/issues/28024 // KeepScrollOffset (src\Compatibility\ControlGallery\src\Issues.Shared\CollectionViewItemsUpdatingScrollMode.cs) + // After scrolling to the middle, adding items above should not cause the view to scroll. + // Previously, the view would automatically scroll to show newly added items, which broke + // KeepScrollOffset semantics. With the fix, the visible items remain stable; adding 5 items + // above shifts indices so the previously visible "Vegetables.jpg, 10" becomes "FlowerBuds.jpg, 12". [Test] [Category(UITestCategories.CollectionView)] public void KeepScrollOffset() diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs index f482ea01c6ce..2c056a8b956d 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs @@ -28,4 +28,4 @@ public void KeepScrollOffSetShouldWork() App.Click(AddNewItem); App.WaitForElement("Item 30"); } -} \ No newline at end of file +} From f1750ff9f6cef158bbdf8e6cf6e0580e2439545c Mon Sep 17 00:00:00 2001 From: HarishwaranVijayakumar Date: Wed, 29 Apr 2026 11:26:24 +0530 Subject: [PATCH 4/4] Address AI summary --- .../src/Core/Handlers/Items/Android/MauiRecyclerView.cs | 3 +-- src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs | 2 +- .../tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs index e4910808cc07..bd34113ce656 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs @@ -370,10 +370,9 @@ public virtual void UpdateItemsSource() UpdateAdapter(); // Set up any properties which require observing data changes in the adapter - UpdateItemsUpdatingScrollMode(); - UpdateEmptyView(); AddOrUpdateScrollListener(); + UpdateItemsUpdatingScrollMode(); UpdateSnapBehavior(); } diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs index 201a8cdc5817..872207e690f8 100644 --- a/src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs @@ -3,7 +3,7 @@ namespace Controls.TestCases.HostApp.Issues; -[Issue(IssueTracker.Github, 29131, "Android - KeepScrollOffset doesn't not works as expected when new items are added in CollectionView", PlatformAffected.Android)] +[Issue(IssueTracker.Github, 29131, "Android - KeepScrollOffset does not work as expected when new items are added in CollectionView", PlatformAffected.Android)] public class Issue29131 : TestContentPage { ObservableCollection items; diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs index 2c056a8b956d..6c38ae037ae7 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs @@ -8,13 +8,13 @@ public class Issue29131 : _IssuesUITest { public Issue29131(TestDevice device) : base(device) { } - public override string Issue => "Android - KeepScrollOffset doesn't not works as expected when new items are added in CollectionView"; + public override string Issue => "Android - KeepScrollOffset does not work as expected when new items are added in CollectionView"; const string AddNewItem = "AddNewItem"; const string ScrollButton = "ScrollButton"; [Test] [Category(UITestCategories.CollectionView)] - public void KeepScrollOffSetShouldWork() + public void KeepScrollOffsetShouldWork() { App.WaitForElement("CollectionView"); App.Click("KeepScrollOffsetButton");