[Android] Fix CollectionView KeepScrollOffset regressed after PR #29255 - #35946
Merged
kubaflo merged 2 commits intoJun 17, 2026
Merged
Conversation
…net#34527) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Issue Details: Horizontalspacing / Verticalspacing is not not applied to the first column in GridItemLayout using CollectionView on Android platform. ### Root Cause: The grid spacing was not being distributed symmetrically across the active layout implementations, so edge items did not fully participate when spacing changed at runtime. ### Description of Change: - On Android, the fix in MauiRecyclerView.cs changes how RecyclerView padding is handled for GridItemsLayout. Android was already using SpacingItemDecoration, which applies half-spacing on all four sides of each item. Previously, negative RecyclerView padding canceled that spacing at the control edges. The branch keeps that negative-padding behavior for non-grid layouts, but disables it for GridItemsLayout, allowing the grid’s half-spacing to remain visible at the outer perimeter. This makes the first row and first column visually respond when spacing changes, but it also changes the grid behavior from spacing only between items to spacing around the outside edges as well. **Tested the behavior in the following platforms:** - [x] Android - [x] Windows - [ ] iOS - [ ] Mac ### Reference: N/A ### Issues Fixed: Fixes dotnet#34257 ### Screenshots | Before | After | |---------|--------| | <Video src="https://github.com/user-attachments/assets/578dda69-1d60-474c-a6d8-23b3f9d29a50" Width="300" Height="600"> | <Video src="https://github.com/user-attachments/assets/7f3826e6-5922-4b6f-a6b9-de581b7db6c3" Width="300" Height="600"> |
Contributor
|
Hey there @@HarishwaranVijayakumar! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
Contributor
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
Contributor
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
Contributor
Author
|
@kubaflo, This appears to be an existing failure on the Windows InFlight/Current branch, not a failure introduced by this PR. |
kubaflo
approved these changes
Jun 17, 2026
kubaflo
marked this pull request as ready for review
June 17, 2026 11:55
PureWeen
pushed a commit
that referenced
this pull request
Jun 22, 2026
…#29255 (#35946) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details - Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset stops working after ItemsSource is replaced. - After the reassignment, inserting a new item at index 0 leaves the previous top item anchored on screen and hides the new item above the viewport — effectively KeepItemsInView semantics instead of KeepScrollOffset. - Only Android is affected. iOS behaves correctly. ### Root Cause of the issue - MauiRecyclerView.RemoveScrollListener() calls ClearOnScrollListeners(), which removes all scroll listeners registered on the RecyclerView — including the one ScrollHelper self-registered. - ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes stale (true while the listener is actually detached). - Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the stale flag, so ScrollHelper is never re-attached. - With no scroll callbacks reaching ScrollHelper, TrackOffsets() never runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is silently skipped. ### Before PR #29255 - ScrollHelper attached its listener lazily, inside UndoNextScrollAdjustment(), only when an insert actually needed it. - The stale-flag desync caused by ClearOnScrollListeners() was a latent bug, but the lazy re-register path inside UndoNextScrollAdjustment() masked it for the typical user — the listener got attached the next time it was needed, so most KeepScrollOffset scenarios still worked. ### After PR #29255 - Listener registration was moved out of UndoNextScrollAdjustment() and into an explicit ScrollHelper.AddScrollListener() method, called eagerly from MauiRecyclerView.UpdateItemsUpdatingScrollMode(). - Both AddScrollListener() and the old lazy block use the same _maintainingScrollOffsets guard, but the lazy self-heal block in UndoNextScrollAdjustment() was deleted. - Now, when ClearOnScrollListeners() detaches ScrollHelper's listener, the flag stays true, the eager AddScrollListener() no-ops, the lazy safety net is gone, and KeepScrollOffset is permanently broken after the first ItemsSource reassignment. ### Description of Change <!-- Enter description of the fix in this section --> **Bug fix:** * Fixed the scroll listener removal logic in `RemoveScrollListener()` by removing only the specific listener instead of clearing all listeners, which resolves the `KeepScrollOffset` issue when replacing the `ItemsSource` in `CollectionView` on Android. (`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`) **Test coverage:** * Added a new test case page that reproduces the issue and provides buttons to replace the `ItemsSource`, scroll to the top, and insert items at the top, making it easier to manually verify the fix. (`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`) * Introduced an automated UI test (Android only) that verifies the `KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring the regression is caught in the future. (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`) ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #35806 ### Tested the behavior in the following platforms - [ ] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797"> | <video src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> --------- Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
This was
linked to
issues
Jun 25, 2026
kubaflo
pushed a commit
that referenced
this pull request
Jun 25, 2026
…#29255 (#35946) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details - Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset stops working after ItemsSource is replaced. - After the reassignment, inserting a new item at index 0 leaves the previous top item anchored on screen and hides the new item above the viewport — effectively KeepItemsInView semantics instead of KeepScrollOffset. - Only Android is affected. iOS behaves correctly. ### Root Cause of the issue - MauiRecyclerView.RemoveScrollListener() calls ClearOnScrollListeners(), which removes all scroll listeners registered on the RecyclerView — including the one ScrollHelper self-registered. - ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes stale (true while the listener is actually detached). - Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the stale flag, so ScrollHelper is never re-attached. - With no scroll callbacks reaching ScrollHelper, TrackOffsets() never runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is silently skipped. ### Before PR #29255 - ScrollHelper attached its listener lazily, inside UndoNextScrollAdjustment(), only when an insert actually needed it. - The stale-flag desync caused by ClearOnScrollListeners() was a latent bug, but the lazy re-register path inside UndoNextScrollAdjustment() masked it for the typical user — the listener got attached the next time it was needed, so most KeepScrollOffset scenarios still worked. ### After PR #29255 - Listener registration was moved out of UndoNextScrollAdjustment() and into an explicit ScrollHelper.AddScrollListener() method, called eagerly from MauiRecyclerView.UpdateItemsUpdatingScrollMode(). - Both AddScrollListener() and the old lazy block use the same _maintainingScrollOffsets guard, but the lazy self-heal block in UndoNextScrollAdjustment() was deleted. - Now, when ClearOnScrollListeners() detaches ScrollHelper's listener, the flag stays true, the eager AddScrollListener() no-ops, the lazy safety net is gone, and KeepScrollOffset is permanently broken after the first ItemsSource reassignment. ### Description of Change <!-- Enter description of the fix in this section --> **Bug fix:** * Fixed the scroll listener removal logic in `RemoveScrollListener()` by removing only the specific listener instead of clearing all listeners, which resolves the `KeepScrollOffset` issue when replacing the `ItemsSource` in `CollectionView` on Android. (`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`) **Test coverage:** * Added a new test case page that reproduces the issue and provides buttons to replace the `ItemsSource`, scroll to the top, and insert items at the top, making it easier to manually verify the fix. (`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`) * Introduced an automated UI test (Android only) that verifies the `KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring the regression is caught in the future. (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`) ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #35806 ### Tested the behavior in the following platforms - [ ] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797"> | <video src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> --------- Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
kubaflo
pushed a commit
that referenced
this pull request
Jul 3, 2026
…#29255 (#35946) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details - Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset stops working after ItemsSource is replaced. - After the reassignment, inserting a new item at index 0 leaves the previous top item anchored on screen and hides the new item above the viewport — effectively KeepItemsInView semantics instead of KeepScrollOffset. - Only Android is affected. iOS behaves correctly. ### Root Cause of the issue - MauiRecyclerView.RemoveScrollListener() calls ClearOnScrollListeners(), which removes all scroll listeners registered on the RecyclerView — including the one ScrollHelper self-registered. - ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes stale (true while the listener is actually detached). - Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the stale flag, so ScrollHelper is never re-attached. - With no scroll callbacks reaching ScrollHelper, TrackOffsets() never runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is silently skipped. ### Before PR #29255 - ScrollHelper attached its listener lazily, inside UndoNextScrollAdjustment(), only when an insert actually needed it. - The stale-flag desync caused by ClearOnScrollListeners() was a latent bug, but the lazy re-register path inside UndoNextScrollAdjustment() masked it for the typical user — the listener got attached the next time it was needed, so most KeepScrollOffset scenarios still worked. ### After PR #29255 - Listener registration was moved out of UndoNextScrollAdjustment() and into an explicit ScrollHelper.AddScrollListener() method, called eagerly from MauiRecyclerView.UpdateItemsUpdatingScrollMode(). - Both AddScrollListener() and the old lazy block use the same _maintainingScrollOffsets guard, but the lazy self-heal block in UndoNextScrollAdjustment() was deleted. - Now, when ClearOnScrollListeners() detaches ScrollHelper's listener, the flag stays true, the eager AddScrollListener() no-ops, the lazy safety net is gone, and KeepScrollOffset is permanently broken after the first ItemsSource reassignment. ### Description of Change <!-- Enter description of the fix in this section --> **Bug fix:** * Fixed the scroll listener removal logic in `RemoveScrollListener()` by removing only the specific listener instead of clearing all listeners, which resolves the `KeepScrollOffset` issue when replacing the `ItemsSource` in `CollectionView` on Android. (`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`) **Test coverage:** * Added a new test case page that reproduces the issue and provides buttons to replace the `ItemsSource`, scroll to the top, and insert items at the top, making it easier to manually verify the fix. (`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`) * Introduced an automated UI test (Android only) that verifies the `KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring the regression is caught in the future. (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`) ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #35806 ### Tested the behavior in the following platforms - [ ] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797"> | <video src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> --------- Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
Merged
kubaflo
pushed a commit
that referenced
this pull request
Jul 6, 2026
…#29255 (#35946) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details - Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset stops working after ItemsSource is replaced. - After the reassignment, inserting a new item at index 0 leaves the previous top item anchored on screen and hides the new item above the viewport — effectively KeepItemsInView semantics instead of KeepScrollOffset. - Only Android is affected. iOS behaves correctly. ### Root Cause of the issue - MauiRecyclerView.RemoveScrollListener() calls ClearOnScrollListeners(), which removes all scroll listeners registered on the RecyclerView — including the one ScrollHelper self-registered. - ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes stale (true while the listener is actually detached). - Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the stale flag, so ScrollHelper is never re-attached. - With no scroll callbacks reaching ScrollHelper, TrackOffsets() never runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is silently skipped. ### Before PR #29255 - ScrollHelper attached its listener lazily, inside UndoNextScrollAdjustment(), only when an insert actually needed it. - The stale-flag desync caused by ClearOnScrollListeners() was a latent bug, but the lazy re-register path inside UndoNextScrollAdjustment() masked it for the typical user — the listener got attached the next time it was needed, so most KeepScrollOffset scenarios still worked. ### After PR #29255 - Listener registration was moved out of UndoNextScrollAdjustment() and into an explicit ScrollHelper.AddScrollListener() method, called eagerly from MauiRecyclerView.UpdateItemsUpdatingScrollMode(). - Both AddScrollListener() and the old lazy block use the same _maintainingScrollOffsets guard, but the lazy self-heal block in UndoNextScrollAdjustment() was deleted. - Now, when ClearOnScrollListeners() detaches ScrollHelper's listener, the flag stays true, the eager AddScrollListener() no-ops, the lazy safety net is gone, and KeepScrollOffset is permanently broken after the first ItemsSource reassignment. ### Description of Change <!-- Enter description of the fix in this section --> **Bug fix:** * Fixed the scroll listener removal logic in `RemoveScrollListener()` by removing only the specific listener instead of clearing all listeners, which resolves the `KeepScrollOffset` issue when replacing the `ItemsSource` in `CollectionView` on Android. (`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`) **Test coverage:** * Added a new test case page that reproduces the issue and provides buttons to replace the `ItemsSource`, scroll to the top, and insert items at the top, making it easier to manually verify the fix. (`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`) * Introduced an automated UI test (Android only) that verifies the `KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring the regression is caught in the future. (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`) ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #35806 ### Tested the behavior in the following platforms - [ ] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797"> | <video src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> --------- Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
PureWeen
pushed a commit
that referenced
this pull request
Jul 7, 2026
…#29255 (#35946) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details - Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset stops working after ItemsSource is replaced. - After the reassignment, inserting a new item at index 0 leaves the previous top item anchored on screen and hides the new item above the viewport — effectively KeepItemsInView semantics instead of KeepScrollOffset. - Only Android is affected. iOS behaves correctly. ### Root Cause of the issue - MauiRecyclerView.RemoveScrollListener() calls ClearOnScrollListeners(), which removes all scroll listeners registered on the RecyclerView — including the one ScrollHelper self-registered. - ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes stale (true while the listener is actually detached). - Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the stale flag, so ScrollHelper is never re-attached. - With no scroll callbacks reaching ScrollHelper, TrackOffsets() never runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is silently skipped. ### Before PR #29255 - ScrollHelper attached its listener lazily, inside UndoNextScrollAdjustment(), only when an insert actually needed it. - The stale-flag desync caused by ClearOnScrollListeners() was a latent bug, but the lazy re-register path inside UndoNextScrollAdjustment() masked it for the typical user — the listener got attached the next time it was needed, so most KeepScrollOffset scenarios still worked. ### After PR #29255 - Listener registration was moved out of UndoNextScrollAdjustment() and into an explicit ScrollHelper.AddScrollListener() method, called eagerly from MauiRecyclerView.UpdateItemsUpdatingScrollMode(). - Both AddScrollListener() and the old lazy block use the same _maintainingScrollOffsets guard, but the lazy self-heal block in UndoNextScrollAdjustment() was deleted. - Now, when ClearOnScrollListeners() detaches ScrollHelper's listener, the flag stays true, the eager AddScrollListener() no-ops, the lazy safety net is gone, and KeepScrollOffset is permanently broken after the first ItemsSource reassignment. ### Description of Change <!-- Enter description of the fix in this section --> **Bug fix:** * Fixed the scroll listener removal logic in `RemoveScrollListener()` by removing only the specific listener instead of clearing all listeners, which resolves the `KeepScrollOffset` issue when replacing the `ItemsSource` in `CollectionView` on Android. (`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`) **Test coverage:** * Added a new test case page that reproduces the issue and provides buttons to replace the `ItemsSource`, scroll to the top, and insert items at the top, making it easier to manually verify the fix. (`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`) * Introduced an automated UI test (Android only) that verifies the `KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring the regression is caught in the future. (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`) ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #35806 ### Tested the behavior in the following platforms - [ ] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797"> | <video src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> --------- Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
PureWeen
pushed a commit
that referenced
this pull request
Jul 7, 2026
…#29255 (#35946) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details - Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset stops working after ItemsSource is replaced. - After the reassignment, inserting a new item at index 0 leaves the previous top item anchored on screen and hides the new item above the viewport — effectively KeepItemsInView semantics instead of KeepScrollOffset. - Only Android is affected. iOS behaves correctly. ### Root Cause of the issue - MauiRecyclerView.RemoveScrollListener() calls ClearOnScrollListeners(), which removes all scroll listeners registered on the RecyclerView — including the one ScrollHelper self-registered. - ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes stale (true while the listener is actually detached). - Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the stale flag, so ScrollHelper is never re-attached. - With no scroll callbacks reaching ScrollHelper, TrackOffsets() never runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is silently skipped. ### Before PR #29255 - ScrollHelper attached its listener lazily, inside UndoNextScrollAdjustment(), only when an insert actually needed it. - The stale-flag desync caused by ClearOnScrollListeners() was a latent bug, but the lazy re-register path inside UndoNextScrollAdjustment() masked it for the typical user — the listener got attached the next time it was needed, so most KeepScrollOffset scenarios still worked. ### After PR #29255 - Listener registration was moved out of UndoNextScrollAdjustment() and into an explicit ScrollHelper.AddScrollListener() method, called eagerly from MauiRecyclerView.UpdateItemsUpdatingScrollMode(). - Both AddScrollListener() and the old lazy block use the same _maintainingScrollOffsets guard, but the lazy self-heal block in UndoNextScrollAdjustment() was deleted. - Now, when ClearOnScrollListeners() detaches ScrollHelper's listener, the flag stays true, the eager AddScrollListener() no-ops, the lazy safety net is gone, and KeepScrollOffset is permanently broken after the first ItemsSource reassignment. ### Description of Change <!-- Enter description of the fix in this section --> **Bug fix:** * Fixed the scroll listener removal logic in `RemoveScrollListener()` by removing only the specific listener instead of clearing all listeners, which resolves the `KeepScrollOffset` issue when replacing the `ItemsSource` in `CollectionView` on Android. (`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`) **Test coverage:** * Added a new test case page that reproduces the issue and provides buttons to replace the `ItemsSource`, scroll to the top, and insert items at the top, making it easier to manually verify the fix. (`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`) * Introduced an automated UI test (Android only) that verifies the `KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring the regression is caught in the future. (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`) ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #35806 ### Tested the behavior in the following platforms - [ ] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797"> | <video src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> --------- Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
kubaflo
pushed a commit
that referenced
this pull request
Jul 10, 2026
…#29255 (#35946) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details - Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset stops working after ItemsSource is replaced. - After the reassignment, inserting a new item at index 0 leaves the previous top item anchored on screen and hides the new item above the viewport — effectively KeepItemsInView semantics instead of KeepScrollOffset. - Only Android is affected. iOS behaves correctly. ### Root Cause of the issue - MauiRecyclerView.RemoveScrollListener() calls ClearOnScrollListeners(), which removes all scroll listeners registered on the RecyclerView — including the one ScrollHelper self-registered. - ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes stale (true while the listener is actually detached). - Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the stale flag, so ScrollHelper is never re-attached. - With no scroll callbacks reaching ScrollHelper, TrackOffsets() never runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is silently skipped. ### Before PR #29255 - ScrollHelper attached its listener lazily, inside UndoNextScrollAdjustment(), only when an insert actually needed it. - The stale-flag desync caused by ClearOnScrollListeners() was a latent bug, but the lazy re-register path inside UndoNextScrollAdjustment() masked it for the typical user — the listener got attached the next time it was needed, so most KeepScrollOffset scenarios still worked. ### After PR #29255 - Listener registration was moved out of UndoNextScrollAdjustment() and into an explicit ScrollHelper.AddScrollListener() method, called eagerly from MauiRecyclerView.UpdateItemsUpdatingScrollMode(). - Both AddScrollListener() and the old lazy block use the same _maintainingScrollOffsets guard, but the lazy self-heal block in UndoNextScrollAdjustment() was deleted. - Now, when ClearOnScrollListeners() detaches ScrollHelper's listener, the flag stays true, the eager AddScrollListener() no-ops, the lazy safety net is gone, and KeepScrollOffset is permanently broken after the first ItemsSource reassignment. ### Description of Change <!-- Enter description of the fix in this section --> **Bug fix:** * Fixed the scroll listener removal logic in `RemoveScrollListener()` by removing only the specific listener instead of clearing all listeners, which resolves the `KeepScrollOffset` issue when replacing the `ItemsSource` in `CollectionView` on Android. (`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`) **Test coverage:** * Added a new test case page that reproduces the issue and provides buttons to replace the `ItemsSource`, scroll to the top, and insert items at the top, making it easier to manually verify the fix. (`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`) * Introduced an automated UI test (Android only) that verifies the `KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring the regression is caught in the future. (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`) ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #35806 ### Tested the behavior in the following platforms - [ ] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797"> | <video src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> --------- Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
kubaflo
pushed a commit
that referenced
this pull request
Jul 15, 2026
…#29255 (#35946) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details - Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset stops working after ItemsSource is replaced. - After the reassignment, inserting a new item at index 0 leaves the previous top item anchored on screen and hides the new item above the viewport — effectively KeepItemsInView semantics instead of KeepScrollOffset. - Only Android is affected. iOS behaves correctly. ### Root Cause of the issue - MauiRecyclerView.RemoveScrollListener() calls ClearOnScrollListeners(), which removes all scroll listeners registered on the RecyclerView — including the one ScrollHelper self-registered. - ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes stale (true while the listener is actually detached). - Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the stale flag, so ScrollHelper is never re-attached. - With no scroll callbacks reaching ScrollHelper, TrackOffsets() never runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is silently skipped. ### Before PR #29255 - ScrollHelper attached its listener lazily, inside UndoNextScrollAdjustment(), only when an insert actually needed it. - The stale-flag desync caused by ClearOnScrollListeners() was a latent bug, but the lazy re-register path inside UndoNextScrollAdjustment() masked it for the typical user — the listener got attached the next time it was needed, so most KeepScrollOffset scenarios still worked. ### After PR #29255 - Listener registration was moved out of UndoNextScrollAdjustment() and into an explicit ScrollHelper.AddScrollListener() method, called eagerly from MauiRecyclerView.UpdateItemsUpdatingScrollMode(). - Both AddScrollListener() and the old lazy block use the same _maintainingScrollOffsets guard, but the lazy self-heal block in UndoNextScrollAdjustment() was deleted. - Now, when ClearOnScrollListeners() detaches ScrollHelper's listener, the flag stays true, the eager AddScrollListener() no-ops, the lazy safety net is gone, and KeepScrollOffset is permanently broken after the first ItemsSource reassignment. ### Description of Change <!-- Enter description of the fix in this section --> **Bug fix:** * Fixed the scroll listener removal logic in `RemoveScrollListener()` by removing only the specific listener instead of clearing all listeners, which resolves the `KeepScrollOffset` issue when replacing the `ItemsSource` in `CollectionView` on Android. (`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`) **Test coverage:** * Added a new test case page that reproduces the issue and provides buttons to replace the `ItemsSource`, scroll to the top, and insert items at the top, making it easier to manually verify the fix. (`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`) * Introduced an automated UI test (Android only) that verifies the `KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring the regression is caught in the future. (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`) ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #35806 ### Tested the behavior in the following platforms - [ ] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797"> | <video src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> --------- Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
kubaflo
pushed a commit
that referenced
this pull request
Jul 22, 2026
…#29255 (#35946) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details - Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset stops working after ItemsSource is replaced. - After the reassignment, inserting a new item at index 0 leaves the previous top item anchored on screen and hides the new item above the viewport — effectively KeepItemsInView semantics instead of KeepScrollOffset. - Only Android is affected. iOS behaves correctly. ### Root Cause of the issue - MauiRecyclerView.RemoveScrollListener() calls ClearOnScrollListeners(), which removes all scroll listeners registered on the RecyclerView — including the one ScrollHelper self-registered. - ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes stale (true while the listener is actually detached). - Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the stale flag, so ScrollHelper is never re-attached. - With no scroll callbacks reaching ScrollHelper, TrackOffsets() never runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is silently skipped. ### Before PR #29255 - ScrollHelper attached its listener lazily, inside UndoNextScrollAdjustment(), only when an insert actually needed it. - The stale-flag desync caused by ClearOnScrollListeners() was a latent bug, but the lazy re-register path inside UndoNextScrollAdjustment() masked it for the typical user — the listener got attached the next time it was needed, so most KeepScrollOffset scenarios still worked. ### After PR #29255 - Listener registration was moved out of UndoNextScrollAdjustment() and into an explicit ScrollHelper.AddScrollListener() method, called eagerly from MauiRecyclerView.UpdateItemsUpdatingScrollMode(). - Both AddScrollListener() and the old lazy block use the same _maintainingScrollOffsets guard, but the lazy self-heal block in UndoNextScrollAdjustment() was deleted. - Now, when ClearOnScrollListeners() detaches ScrollHelper's listener, the flag stays true, the eager AddScrollListener() no-ops, the lazy safety net is gone, and KeepScrollOffset is permanently broken after the first ItemsSource reassignment. ### Description of Change <!-- Enter description of the fix in this section --> **Bug fix:** * Fixed the scroll listener removal logic in `RemoveScrollListener()` by removing only the specific listener instead of clearing all listeners, which resolves the `KeepScrollOffset` issue when replacing the `ItemsSource` in `CollectionView` on Android. (`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`) **Test coverage:** * Added a new test case page that reproduces the issue and provides buttons to replace the `ItemsSource`, scroll to the top, and insert items at the top, making it easier to manually verify the fix. (`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`) * Introduced an automated UI test (Android only) that verifies the `KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring the regression is caught in the future. (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`) ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #35806 ### Tested the behavior in the following platforms - [ ] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797"> | <video src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> --------- Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue Details
Root Cause of the issue
Before PR #29255
After PR #29255
Description of Change
Bug fix:
RemoveScrollListener()by removing only the specific listener instead of clearing all listeners, which resolves theKeepScrollOffsetissue when replacing theItemsSourceinCollectionViewon Android. (src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs)Test coverage:
ItemsSource, scroll to the top, and insert items at the top, making it easier to manually verify the fix. (src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs)KeepScrollOffsetbehavior after replacing theItemsSource, ensuring the regression is caught in the future. (src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs)Issues Fixed
Fixes #35806
Tested the behavior in the following platforms
Output
Before-35806.mov
After-35806.mov