Skip to content

[Android] Fix CollectionView KeepScrollOffset regressed after PR #29255 - #35946

Merged
kubaflo merged 2 commits into
dotnet:inflight/currentfrom
HarishwaranVijayakumar:fix-35806
Jun 17, 2026
Merged

[Android] Fix CollectionView KeepScrollOffset regressed after PR #29255#35946
kubaflo merged 2 commits into
dotnet:inflight/currentfrom
HarishwaranVijayakumar:fix-35806

Conversation

@HarishwaranVijayakumar

Copy link
Copy Markdown
Contributor

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

  • 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

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

Fixes #35806

Tested the behavior in the following platforms

  • Windows
  • Android
  • iOS
  • Mac

Output

Before After
Before-35806.mov
After-35806.mov

KarthikRajaKalaimani and others added 2 commits June 16, 2026 17:55
…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"> |
@dotnet-policy-service dotnet-policy-service Bot added the community ✨ Community Contribution label Jun 16, 2026
@dotnet-policy-service

Copy link
Copy Markdown
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.

@dotnet-policy-service dotnet-policy-service Bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Jun 16, 2026
@github-actions github-actions Bot added area-controls-collectionview CollectionView, CarouselView, IndicatorView platform/android labels Jun 16, 2026
@kubaflo

kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kubaflo

kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@HarishwaranVijayakumar

Copy link
Copy Markdown
Contributor Author

@kubaflo, This appears to be an existing failure on the Windows InFlight/Current branch, not a failure introduced by this PR.

@kubaflo
kubaflo marked this pull request as ready for review June 17, 2026 11:55
@kubaflo
kubaflo merged commit 229560d into dotnet:inflight/current Jun 17, 2026
10 of 30 checks passed
@github-actions github-actions Bot added this to the .NET 10 SR9 milestone Jun 17, 2026
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>
@sheiksyedm sheiksyedm changed the title [WIP] [Android] Fix CollectionView KeepScrollOffset regressed after PR #29255 [Android] Fix CollectionView KeepScrollOffset regressed after PR #29255 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>
@kubaflo kubaflo mentioned this pull request Jul 6, 2026
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>
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-controls-collectionview CollectionView, CarouselView, IndicatorView community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/android

Projects

None yet

4 participants