[Android] Fix for CollectionView Scrolled event is triggered on the initial app load.#33558
Conversation
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
This PR fixes an Android-specific issue where the CollectionView Scrolled event was incorrectly firing on initial app load. The root cause is that Android's RecyclerView natively calls OnScrolled(0, 0) during initial layout, which MAUI was treating as a user scroll event.
Changes:
- Introduces a
_isInitialLayoutflag to filter out the firstOnScrolled(0, 0)callback during initial layout - Resets the flag when the adapter is updated to handle subsequent layout cycles correctly
- Adds UI tests to verify the fix
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs |
Implements the fix by adding _isInitialLayout flag and filtering logic in OnScrolled method |
src/Controls/tests/TestCases.HostApp/Issues/Issue33333.cs |
Creates HostApp test page that demonstrates the issue with a scroll event counter |
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33333.cs |
Adds NUnit test to verify Scrolled event doesn't fire on initial load |
src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs
Outdated
Show resolved
Hide resolved
| public void CollectionViewScrolledEventShouldNotFireOnInitialLoad() | ||
| { | ||
| App.WaitForElement("ScrollCountLabel"); | ||
|
|
||
| var scrollCountText = App.FindElement("ScrollCountLabel").GetText(); | ||
| Assert.That(scrollCountText, Is.EqualTo("Scrolled Event Count: 0"), | ||
| "Scrolled event should not fire on initial load without user interaction"); | ||
| } |
There was a problem hiding this comment.
The test only verifies that the Scrolled event doesn't fire on initial load, but doesn't validate that subsequent scrolling events work correctly after the initial load. Consider adding a second test case or extending this test to perform an actual scroll and verify that the event fires properly (e.g., scroll count should be 1 after user scrolls).
|
/rebase |
818e3ed to
2c6d458
Compare
✅ PR Review Complete: #33558Details### Final Recommendation: **APPROVE** with Minor Suggestion📋 Executive SummaryPR #33558 correctly fixes an Android-specific issue where CollectionView's Scrolled event incorrectly fires during initial app load. The root cause was properly identified (RecyclerView natively calls ✅ What's Good
|
| Attempt | Approach | Result |
|---|---|---|
| #1 | Skip ALL OnScrolled(0, 0) callbacks |
❌ Too broad - blocks legitimate layout updates |
| #2 | Track _hasScrolled, skip first only |
❌ Incomplete - missing adapter update reset |
| #3 | Track _hasScrolled + reset on adapter update |
✅ Equivalent to PR |
Conclusion: PR's approach is optimal. Alternative #3 works but PR's naming (_isInitialLayout) is more descriptive.
📊 Test Coverage Assessment
Current coverage: ✅ Adequate for the fix
- Validates event doesn't fire on initial load
- Android-focused (matching issue scope)
Reviewer feedback noted tests don't validate subsequent scrolling works - this is acceptable because:
- Subsequent scroll functionality is pre-existing behavior (already working)
- Fix only touches initial layout filter - doesn't modify scroll event logic
- Test focuses on the specific bug being fixed
🎯 Final Verdict
APPROVE - The fix is correct, well-tested, and the optimal solution. The minor redundancy at line 55 is non-blocking and could be addressed in a follow-up if desired.
�[37mTotal usage est: �[39m1 Premium request
�[37mAPI time spent: �[39m8m 20.944s
�[37mTotal session time: �[39m11m 44.641s
�[37mTotal code changes: �[32m+0�[39m �[31m-0�[39m
�[37mBreakdown by AI model:�[39m
claude-sonnet-4.5 4.5m in, 18.7k out, 4.4m cached (Est. 1 Premium request)
|
/rebase |
…ent during the initial RecyclerView layout on Android.
…ent on Android page load while preserving scroll events for ItemsSource changes.
b50236a to
1e3107f
Compare
🤖 AI Summary📊 Expand Full Review🔍 Pre-Flight — Context & Validation📝 Review Session — fix-33333-Changes committed. ·
|
| File:Line | Reviewer Says | Status |
|---|---|---|
RecyclerViewScrollListener.cs |
Copilot: Redundant flag clearing (applies to old code) | Outdated - doesn't apply to current implementation |
Issue33333.cs:22 |
Copilot: Test doesn't validate subsequent scroll works |
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #33558 | _hasCompletedFirstLayout flag + !recyclerView.IsLaidOut check to filter initial layout callback |
⏳ PENDING (Gate) | RecyclerViewScrollListener.cs (+14) |
Current PR |
🚦 Gate — Test Verification
📝 Review Session — fix-33333-Changes committed. · b009ee4
Result: ✅ PASSED
Platform: android
Mode: Full Verification (verify-tests-fail-without-fix skill)
| Check | Expected | Actual | Result |
|---|---|---|---|
| Tests WITHOUT fix | FAIL | FAIL | ✅ |
| Tests WITH fix | PASS | PASS | ✅ |
- Tests FAIL without fix ✅ (bug is present and detectable)
- Tests PASS with fix ✅ (fix correctly resolves the issue)
Test: CollectionViewScrolledEventShouldNotFireOnInitialLoad in Issue33333
🔧 Fix — Analysis & Comparison
📝 Review Session — fix-33333-Changes committed. · b009ee4
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-sonnet-4.5) | OnScrollStateChanged override + _userHasScrolled flag (set on non-IDLE state) |
✅ PASS | RecyclerViewScrollListener.cs, PublicAPI.Unshipped.txt |
Requires new public API override; semantically clean |
| 2 | try-fix (claude-opus-4.6) | Stateless: dx==0 && dy==0 && _horizontalOffset==0 && _verticalOffset==0 |
✅ PASS | RecyclerViewScrollListener.cs |
No new state; but edge case if offsets accumulate back to 0 |
| 3 | try-fix (gpt-5.2) | recyclerView.ScrollState == ScrollStateIdle && dx==0 && dy==0 |
✅ PASS | RecyclerViewScrollListener.cs |
One-liner; could suppress idle-state programmatic scrolls |
| 4 | try-fix (gpt-5.2-codex) | recyclerView.IsComputingLayout && dx==0 && dy==0 |
❌ FAIL | RecyclerViewScrollListener.cs |
Initial callback fires AFTER layout computation, not during |
| 5 | try-fix (gemini-3-pro-preview) | Strict dx==0 && dy==0 guard (always) |
✅ PASS | RecyclerViewScrollListener.cs |
Too broad; PR author explicitly rejected this approach |
| 6 | try-fix (gemini-3-pro-preview) | Latch: _hasFiredNonZeroScroll flag, suppress zero-delta until first non-zero |
✅ PASS | RecyclerViewScrollListener.cs |
Similar to PR but without IsLaidOut check |
| PR | PR #33558 | _hasCompletedFirstLayout boolean + !recyclerView.IsLaidOut + dx==0 && dy==0 |
✅ PASS (Gate) | RecyclerViewScrollListener.cs (+14) |
Most targeted: specifically filters pre-layout zero-delta callbacks |
Exhausted: Yes — Round 3 cross-pollination: claude-opus-4.6 confirmed NO NEW IDEAS; remaining models suggested variations already covered.
Selected Fix: PR's fix — The !recyclerView.IsLaidOut condition is the most precise, targeting specifically the pre-layout state. After the first layout completes, IsLaidOut = true and the guard never triggers again. This is more precise than alternatives 3 (suppresses all idle-state zero-delta) or 5 (suppresses all zero-delta permanently). Alternatives 1 and 2 also work, but Attempt 1 requires a new public API surface, and Attempt 2 has a theoretical edge case. The PR's fix is the best balance of precision and simplicity.
Cross-Pollination Summary
| Round | claude-sonnet-4.5 | claude-opus-4.6 | gpt-5.2 | gpt-5.2-codex | gemini-3-pro-preview |
|---|---|---|---|---|---|
| 2 | NEW IDEAS (5) | NO NEW IDEAS | NEW IDEA (ViewTreeObserver) | NEW IDEA (touch listener) | NEW IDEA → ran as Attempt 6 |
| 3 | NEW IDEAS | NO NEW IDEAS | (rate limited) | NEW IDEA (Post()-based) | NEW IDEA (ChildCount) |
📋 Report — Final Recommendation
📝 Review Session — fix-33333-Changes committed. · b009ee4
✅ Final Recommendation: APPROVE
Summary
PR #33558 correctly fixes issue #33333 where Android's CollectionView Scrolled event incorrectly fires on initial app load without user interaction. The fix is sound, well-targeted, and independently validated by multi-model try-fix exploration (5 of 6 alternative approaches also passed tests). Gate verified tests properly catch the bug.
Root Cause
Android's RecyclerView natively calls OnScrolled(0, 0) during initial layout as a notification that visible item positions may have changed. This is Android's internal layout mechanism, NOT a user scroll event. MAUI's RecyclerViewScrollListener.OnScrolled was treating this layout callback identically to a real user scroll event, causing CollectionView.Scrolled to fire on page load — inconsistent with iOS behavior.
Fix Quality Assessment
Approach: Boolean flag _hasCompletedFirstLayout combined with !recyclerView.IsLaidOut + zero-delta check.
Why this approach is correct:
recyclerView.IsLaidOutisfalseonly before the first layout completes — precisely the window when the spurious callback occurs- After first layout,
IsLaidOut = truepermanently, so the guard never triggers again for real scrolls - Most targeted of all alternatives: specifically filters pre-layout state rather than broader conditions like scroll state
Comparison with alternatives found via try-fix:
| Approach | Correctness | Simplicity | Risk |
|---|---|---|---|
PR's fix: !IsLaidOut + dx==0&&dy==0 |
✅ Most precise | Medium | Low |
Attempt 1: OnScrollStateChanged |
✅ Semantic | Medium | Adds public API |
| Attempt 2: Stateless offsets==0 | ✅ Works | High | Theoretical edge case |
Attempt 3: ScrollState==Idle |
✅ Works | High | Could suppress programmatic |
Attempt 5: dx==0&&dy==0 only |
✅ Works | High | Too broad (rejected by PR author) |
The PR's fix is the best balance of precision and robustness.
Minor Issues (Non-Blocking)
-
⚠️ Description mentions old variable name: PR body says "introduces a boolean flag (_isInitialLayout)" but the code uses_hasCompletedFirstLayout. The description is from an older iteration. -
⚠️ Comment accuracy inUpdateAdapter: Comment says "Reset flag when adapter changes to handle ItemsSource updates" but after initial layout,recyclerView.IsLaidOut = true, so resetting_hasCompletedFirstLayouttofalsedoesn't re-trigger the guard. The reset is harmless but the comment overstates its effect. -
⚠️ Test coverage gap: Test only verifies event count = 0 on initial load. Does not verify that actual scrolling DOES fire the event. This was noted by copilot review but is not blocking. -
⚠️ Missing newline at end of file:Issue33333.cs(tests) is missing final newline.
Title Recommendation
Current: [Android] Fix for CollectionView Scrolled event is triggered on the initial app load.
Recommended: [Android] CollectionView: Fix Scrolled event firing on initial load
Reasons: "Fix for CollectionView Scrolled event is triggered" is grammatically awkward; trailing period is not standard.
Code Review Findings
✅ Looks Good
- Fix is appropriately scoped to Android only
_hasCompletedFirstLayoutnaming clearly communicates intent- Test page has proper
AutomationIdvalues and[Issue]attribute - Test class uses
[Category(UITestCategories.CollectionView)]correctly _horizontalOffsetand_verticalOffsetstill accumulate correctly (update happens before guard)
🟡 Minor Suggestions (Non-Blocking)
- Initialize:
bool _hasCompletedFirstLayout;(remove= false, it's the default) - Update PR description to reflect current variable name
_hasCompletedFirstLayout - Add newline at end of
TestCases.Shared.Tests/Tests/Issues/Issue33333.cs - Consider adding a second test case verifying scrolling DOES fire the event after initial load
📋 Expand PR Finalization Review
Title: ✅ Good
Current: [Android] Fix for CollectionView Scrolled event is triggered on the initial app load.
Description: ✅ Good
- "Fix for ... is triggered" is awkward passive phrasing
- Trailing period (not standard for PR titles)
- "on the initial app load" is slightly verbose
✨ Suggested PR Description
[!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!
Root Cause
Android's RecyclerView natively invokes OnScrolled(0, 0) during the initial layout pass to notify the listener of the initial visible item positions, even when no actual scrolling has occurred. The .NET MAUI CollectionView handler's RecyclerViewScrollListener was treating this layout callback as a user scroll event, causing the Scrolled event to fire when the page first loaded. This was inconsistent with iOS behavior and violated the expectation that Scrolled should only fire in response to genuine user interaction.
Description of Change
Introduces a boolean flag _hasCompletedFirstLayout in RecyclerViewScrollListener<TItemsView, TItemsViewSource> to suppress the spurious initial OnScrolled callback.
Guard condition in OnScrolled:
if (!_hasCompletedFirstLayout && !recyclerView.IsLaidOut && dx == 0 && dy == 0)
{
return;
}
_hasCompletedFirstLayout = true;This ensures only the very first layout callback (where the view is not yet laid out and no delta exists) is suppressed. All subsequent OnScrolled invocations proceed normally.
Flag reset in UpdateAdapter:
_hasCompletedFirstLayout is reset to false when the adapter is updated, so that a fresh ItemsSource swap correctly suppresses the next initial layout callback too.
Files changed:
src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs— fixsrc/Controls/tests/TestCases.HostApp/Issues/Issue33333.cs— UI test host pagesrc/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33333.cs— UI test
Issues Fixed
Fixes #33333
Platforms Tested
- Android
- iOS (Android-only fix)
- Windows (Android-only fix)
- Mac (Android-only fix)
Code Review: ⚠️ Issues Found
Code Review — PR #33558
🔴 Critical Issues
None.
🟡 Suggestions
1. Description references stale flag name
File: PR description (not code)
Problem: The description says "introduces a boolean flag (_isInitialLayout)" but the actual field in the code is _hasCompletedFirstLayout. This is a leftover from a prior iteration of the implementation and will confuse future readers of the git history.
Recommendation: Update the description to reference _hasCompletedFirstLayout and clarify that the condition also checks !recyclerView.IsLaidOut.
2. UI test does not verify scroll events fire after initial load
File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33333.cs
Problem: The test only verifies the scroll count is 0 on initial load. It does not verify that the Scrolled event fires correctly after a real user scroll. This means the test would pass even if the flag accidentally suppressed all scroll events, not just the initial one.
Recommendation: Add a second assertion that performs a scroll and verifies the count increments. Example:
// Verify event fires on real scroll
App.ScrollDown("TestCollectionView", ScrollStrategy.Gesture, 0.5);
App.WaitForElement("ScrollCountLabel");
var afterScrollText = App.FindElement("ScrollCountLabel").GetText();
Assert.That(afterScrollText, Is.Not.EqualTo("Scrolled Event Count: 0"),
"Scrolled event should fire when user scrolls");3. StackLayout used instead of VerticalStackLayout
File: src/Controls/tests/TestCases.HostApp/Issues/Issue33333.cs, line ~61
Problem: StackLayout is the legacy layout control. VerticalStackLayout is preferred in new code.
Recommendation: Replace StackLayout with VerticalStackLayout.
4. Missing newline at end of test file
File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33333.cs
Problem: File ends without a trailing newline (\ No newline at end of file).
Recommendation: Add a trailing newline.
5. Platforms Tested checklist inaccuracy
File: PR description
Problem: The description checks off Windows and Mac as tested, but the fix is Android-only (changes only Android code in Handlers/Items/Android/). This is misleading.
Recommendation: Only check Android, and add a note that the fix is Android-only.
✅ Looks Good
- Core fix logic is correct: The condition
!_hasCompletedFirstLayout && !recyclerView.IsLaidOut && dx == 0 && dy == 0precisely identifies the initial layout callback. Using!recyclerView.IsLaidOutas an additional guard (beyond just zero deltas) makes the filter robust — a legitimate scroll of(0, 0)is less likely to be mistakenly suppressed. - Flag reset in
UpdateAdapteris correct: Resetting_hasCompletedFirstLayout = falsewhen the adapter changes ensures that ItemsSource swaps also benefit from suppression of the initial layout callback. - Offset accumulation before the guard is harmless:
dxanddyare both 0 at that point so_horizontalOffsetand_verticalOffsetare not incorrectly modified. - No impact on other platforms: The change is in an Android-specific file (
Handlers/Items/Android/). [Issue]attribute is correct: Platform is correctly set toPlatformAffected.Android.- Test category is correct:
[Category(UITestCategories.CollectionView)].
PureWeen
left a comment
There was a problem hiding this comment.
let me know if you agree with the agents summary #33558 (comment)
…firing on initial page load on Android.
@PureWeen, The Zero-Delta Filter suggestion is not working properly. Using a simple condition such as if (dx == 0 && dy == 0) return; would permanently suppress all (0, 0) scroll events and could break valid scenarios like ItemsSource updates. Instead, I used alternative approach 5 with the !IsLaidOut check, which suppresses the initial layout event while allowing subsequent events. This resolves the issue with a single file change and no cross-class impact. |
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
…nitial app load. (#33558) <!-- 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! ### Root cause The issue occurs because Android’s RecyclerView natively invokes OnScrolled(0, 0) during the initial layout to notify changes in visible item positions, even when no actual scrolling has occurred. The .NET MAUI CollectionView handler was treating this layout callback as a user scroll event, causing the Scrolled event to fire when the page loaded. This resulted in behavior that was inconsistent with iOS and violated the expectation that the Scrolled event should only be raised in response to actual user interaction. ### Description of Issue Fix The fix introduces a boolean flag (_isInitialLayout) to filter out the first OnScrolled() invocation when both deltas are zero, identifying it as the initial layout callback. This flag is reset when the adapter is updated to correctly handle subsequent layout cycles. As a result, the Scrolled event is now raised only for genuine scroll activity, ensuring consistent behavior across Android and iOS platforms. Tested the behavior in the following platforms. - [x] Windows - [x] Mac - [x] iOS - [x] Android ### 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 #33333 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> ### Output Before Issue Fix | After Issue Fix | |----------|----------| |<video width="100" height="100" alt="Before Fix" src="https://github.com/user-attachments/assets/d48418bd-679e-413a-8d73-8e85fd190c41">|<video width="100" height="100" alt="After Fix" src="https://github.com/user-attachments/assets/fa07fb58-895c-49de-b86c-4243c0e73d64">|
…nitial app load. (#33558) <!-- 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! ### Root cause The issue occurs because Android’s RecyclerView natively invokes OnScrolled(0, 0) during the initial layout to notify changes in visible item positions, even when no actual scrolling has occurred. The .NET MAUI CollectionView handler was treating this layout callback as a user scroll event, causing the Scrolled event to fire when the page loaded. This resulted in behavior that was inconsistent with iOS and violated the expectation that the Scrolled event should only be raised in response to actual user interaction. ### Description of Issue Fix The fix introduces a boolean flag (_isInitialLayout) to filter out the first OnScrolled() invocation when both deltas are zero, identifying it as the initial layout callback. This flag is reset when the adapter is updated to correctly handle subsequent layout cycles. As a result, the Scrolled event is now raised only for genuine scroll activity, ensuring consistent behavior across Android and iOS platforms. Tested the behavior in the following platforms. - [x] Windows - [x] Mac - [x] iOS - [x] Android ### 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 #33333 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> ### Output Before Issue Fix | After Issue Fix | |----------|----------| |<video width="100" height="100" alt="Before Fix" src="https://github.com/user-attachments/assets/d48418bd-679e-413a-8d73-8e85fd190c41">|<video width="100" height="100" alt="After Fix" src="https://github.com/user-attachments/assets/fa07fb58-895c-49de-b86c-4243c0e73d64">|
…nitial app load. (#33558) <!-- 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! ### Root cause The issue occurs because Android’s RecyclerView natively invokes OnScrolled(0, 0) during the initial layout to notify changes in visible item positions, even when no actual scrolling has occurred. The .NET MAUI CollectionView handler was treating this layout callback as a user scroll event, causing the Scrolled event to fire when the page loaded. This resulted in behavior that was inconsistent with iOS and violated the expectation that the Scrolled event should only be raised in response to actual user interaction. ### Description of Issue Fix The fix introduces a boolean flag (_isInitialLayout) to filter out the first OnScrolled() invocation when both deltas are zero, identifying it as the initial layout callback. This flag is reset when the adapter is updated to correctly handle subsequent layout cycles. As a result, the Scrolled event is now raised only for genuine scroll activity, ensuring consistent behavior across Android and iOS platforms. Tested the behavior in the following platforms. - [x] Windows - [x] Mac - [x] iOS - [x] Android ### 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 #33333 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> ### Output Before Issue Fix | After Issue Fix | |----------|----------| |<video width="100" height="100" alt="Before Fix" src="https://github.com/user-attachments/assets/d48418bd-679e-413a-8d73-8e85fd190c41">|<video width="100" height="100" alt="After Fix" src="https://github.com/user-attachments/assets/fa07fb58-895c-49de-b86c-4243c0e73d64">|
…nitial app load. (#33558) <!-- 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! ### Root cause The issue occurs because Android’s RecyclerView natively invokes OnScrolled(0, 0) during the initial layout to notify changes in visible item positions, even when no actual scrolling has occurred. The .NET MAUI CollectionView handler was treating this layout callback as a user scroll event, causing the Scrolled event to fire when the page loaded. This resulted in behavior that was inconsistent with iOS and violated the expectation that the Scrolled event should only be raised in response to actual user interaction. ### Description of Issue Fix The fix introduces a boolean flag (_isInitialLayout) to filter out the first OnScrolled() invocation when both deltas are zero, identifying it as the initial layout callback. This flag is reset when the adapter is updated to correctly handle subsequent layout cycles. As a result, the Scrolled event is now raised only for genuine scroll activity, ensuring consistent behavior across Android and iOS platforms. Tested the behavior in the following platforms. - [x] Windows - [x] Mac - [x] iOS - [x] Android ### 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 #33333 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> ### Output Before Issue Fix | After Issue Fix | |----------|----------| |<video width="100" height="100" alt="Before Fix" src="https://github.com/user-attachments/assets/d48418bd-679e-413a-8d73-8e85fd190c41">|<video width="100" height="100" alt="After Fix" src="https://github.com/user-attachments/assets/fa07fb58-895c-49de-b86c-4243c0e73d64">|
…nitial app load. (dotnet#33558) <!-- 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! ### Root cause The issue occurs because Android’s RecyclerView natively invokes OnScrolled(0, 0) during the initial layout to notify changes in visible item positions, even when no actual scrolling has occurred. The .NET MAUI CollectionView handler was treating this layout callback as a user scroll event, causing the Scrolled event to fire when the page loaded. This resulted in behavior that was inconsistent with iOS and violated the expectation that the Scrolled event should only be raised in response to actual user interaction. ### Description of Issue Fix The fix introduces a boolean flag (_isInitialLayout) to filter out the first OnScrolled() invocation when both deltas are zero, identifying it as the initial layout callback. This flag is reset when the adapter is updated to correctly handle subsequent layout cycles. As a result, the Scrolled event is now raised only for genuine scroll activity, ensuring consistent behavior across Android and iOS platforms. Tested the behavior in the following platforms. - [x] Windows - [x] Mac - [x] iOS - [x] Android ### 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 dotnet#33333 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> ### Output Before Issue Fix | After Issue Fix | |----------|----------| |<video width="100" height="100" alt="Before Fix" src="https://github.com/user-attachments/assets/d48418bd-679e-413a-8d73-8e85fd190c41">|<video width="100" height="100" alt="After Fix" src="https://github.com/user-attachments/assets/fa07fb58-895c-49de-b86c-4243c0e73d64">|
…nitial app load. (#33558) <!-- 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! ### Root cause The issue occurs because Android’s RecyclerView natively invokes OnScrolled(0, 0) during the initial layout to notify changes in visible item positions, even when no actual scrolling has occurred. The .NET MAUI CollectionView handler was treating this layout callback as a user scroll event, causing the Scrolled event to fire when the page loaded. This resulted in behavior that was inconsistent with iOS and violated the expectation that the Scrolled event should only be raised in response to actual user interaction. ### Description of Issue Fix The fix introduces a boolean flag (_isInitialLayout) to filter out the first OnScrolled() invocation when both deltas are zero, identifying it as the initial layout callback. This flag is reset when the adapter is updated to correctly handle subsequent layout cycles. As a result, the Scrolled event is now raised only for genuine scroll activity, ensuring consistent behavior across Android and iOS platforms. Tested the behavior in the following platforms. - [x] Windows - [x] Mac - [x] iOS - [x] Android ### 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 #33333 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> ### Output Before Issue Fix | After Issue Fix | |----------|----------| |<video width="100" height="100" alt="Before Fix" src="https://github.com/user-attachments/assets/d48418bd-679e-413a-8d73-8e85fd190c41">|<video width="100" height="100" alt="After Fix" src="https://github.com/user-attachments/assets/fa07fb58-895c-49de-b86c-4243c0e73d64">|
## What's Coming .NET MAUI inflight/candidate introduces significant improvements across all platforms with focus on quality, performance, and developer experience. This release includes 46 commits with various improvements, bug fixes, and enhancements. ## Button - [Android] Implemented material3 support for Button by @Dhivya-SF4094 in #33173 <details> <summary>🔧 Fixes</summary> - [Implement Material3 support for Button](#33172) </details> ## CollectionView - [Android] Fix RemainingItemsThresholdReachedCommand not firing when CollectionView has Header and Footer both defined by @SuthiYuvaraj in #29618 <details> <summary>🔧 Fixes</summary> - [Android : RemainingItemsThresholdReachedCommand not firing when CollectionVew has Header and Footer both defined](#29588) </details> - [iOS/MacCatalyst] Fix CollectionView ScrollTo for horizontal layouts by @Shalini-Ashokan in #33853 <details> <summary>🔧 Fixes</summary> - [[iOS/MacCatalyst] CollectionView ScrollTo does not work with horizontal Layout](#33852) </details> - [iOS & Mac] Fixed IndicatorView Size doesnt update dynamically by @SubhikshaSf4851 in #31129 <details> <summary>🔧 Fixes</summary> - [[iOS, Catalyst] IndicatorView.IndicatorSize does not update dynamically at runtime](#31064) </details> - [Android] Fix for CollectionView Scrolled event is triggered on the initial app load. by @BagavathiPerumal in #33558 <details> <summary>🔧 Fixes</summary> - [[Android] CollectionView Scrolled event is triggered on the initial app load.](#33333) </details> - [iOS, Android] Fix for CollectionView IsEnabled=false allows touch interactions by @praveenkumarkarunanithi in #31403 <details> <summary>🔧 Fixes</summary> - [More issues with CollectionView IsEnabled, InputTransparent, Opacity via Styles and code behind](#19771) </details> - [iOS] Fix VerticalOffset Update When Modifying CollectionView.ItemsSource While Scrolled by @devanathan-vaithiyanathan in #34153 <details> <summary>🔧 Fixes</summary> - [[iOS]VerticalOffset Not Reset to Zero After Clearing ItemSource in CollectionView](#26798) </details> ## DateTimePicker - [Android] Fix DatePicker MinimumDate/MaximumDate not updating dynamically by @HarishwaranVijayakumar in #33687 <details> <summary>🔧 Fixes</summary> - [[regression/8.0.3] [Android] DatePicker control minimum date issue](#19256) - [[Android] DatePicker does not update MinimumDate / MaximumDate in the Popup when set in the viewmodel after first opening](#33583) </details> ## Drawing - Android drawable perf by @albyrock87 in #31567 ## Editor - [Android] Implemented material3 support for Editor by @SyedAbdulAzeemSF4852 in #33478 <details> <summary>🔧 Fixes</summary> - [Implement Material3 Support for Editor](#33476) </details> ## Entry - [iOS, Mac] Fix for CursorPosition not updating when typing into Entry control by @SyedAbdulAzeemSF4852 in #30505 <details> <summary>🔧 Fixes</summary> - [Entry control CursorPosition does not update on TextChanged event [iOS Maui 8.0.7] ](#20911) - [CursorPosition not calculated correctly on behaviors events for iOS devices](#32483) </details> ## Flyoutpage - [Android, Windows] Fix for FlyoutPage toolbar button not updating on orientation change by @praveenkumarkarunanithi in #31962 <details> <summary>🔧 Fixes</summary> - [Flyout page in Android does not show flyout button (burger) consistently](#24468) </details> - Fix for First Item in CollectionView Overlaps in FlyoutPage.Flyout on iOS by @praveenkumarkarunanithi in #29265 <details> <summary>🔧 Fixes</summary> - [[iOS] CollectionView not rendering first item correctly in FlyoutPage.Flyout](#29170) </details> ## Image - [Android] Fix excessive memory usage for stream and resource-based image loading by @Shalini-Ashokan in #33590 <details> <summary>🔧 Fixes</summary> - [[Android] Unexpected high Bitmap.ByteCount when loading image via ImageSource.FromResource() or ImageSource.FromStream() in .NET MAUI](#33239) </details> - [Android] Fix for Resize method returns an image that has already been disposed by @SyedAbdulAzeemSF4852 in #29964 <details> <summary>🔧 Fixes</summary> - [In GraphicsView, the Resize method returns an image that has already been disposed](#29961) - [IIMage.Resize bugged behaviour](#31103) </details> ## Label - Fixed Label Span font property inheritance when applied via Style by @SubhikshaSf4851 in #34110 <details> <summary>🔧 Fixes</summary> - [`Span` does not inherit text styling from `Label` if that styling is applied using `Style` ](#21326) </details> - [Android] Implemented material3 support for Label by @SyedAbdulAzeemSF4852 in #33599 <details> <summary>🔧 Fixes</summary> - [Implement Material3 Support for Label](#33598) </details> ## Map - [Android] Fix Circle Stroke color is incorrectly updated as Fill color. by @NirmalKumarYuvaraj in #33643 <details> <summary>🔧 Fixes</summary> - [[Android] Circle Stroke color is incorrectly updated as Fill color.](#33642) </details> ## Mediapicker - [iOS] Fix: invoke MediaPicker completion handler after DismissViewController by @yuriikyry4enko in #34250 <details> <summary>🔧 Fixes</summary> - [[iOS] Media Picker UIImagePickerController closing issue](#21996) </details> ## Navigation - Fix ContentPage memory leak on Android when using NavigationPage modally (fixes #33918) by @brunck in #34117 <details> <summary>🔧 Fixes</summary> - [[Android] Modal TabbedPage whose tabs are NavigationPage(ContentPage) is retained after PopModalAsync()](#33918) </details> ## Picker - [Android] Implement material3 support for TimePicker by @HarishwaranVijayakumar in #33646 <details> <summary>🔧 Fixes</summary> - [Implement Material3 support for TimePicker](#33645) </details> - [Android] Implemented Material3 support for Picker by @SyedAbdulAzeemSF4852 in #33668 <details> <summary>🔧 Fixes</summary> - [Implement Material3 support for Picker](#33665) </details> ## RadioButton - [Android] Implemented material3 support for RadioButton by @SyedAbdulAzeemSF4852 in #33468 <details> <summary>🔧 Fixes</summary> - [Implement Material3 Support for RadioButton](#33467) </details> ## Setup - Clarify MA003 error message by @jeremy-visionaid in #34067 <details> <summary>🔧 Fixes</summary> - [MA003 false positive with 9.0.21](#26599) </details> ## Shell - [Android] Fix TabBar FlowDirection not updating dynamically by @SubhikshaSf4851 in #33091 <details> <summary>🔧 Fixes</summary> - [[Android, iOS] FlowDirection RTL is not updated dynamically on Shell TabBar](#32993) </details> - [Android] Fix page not disposed on Shell replace navigation by @Vignesh-SF3580 in #33426 <details> <summary>🔧 Fixes</summary> - [[Android] [Shell] replace navigation leaks current page](#25134) </details> - [Android] Fixed Shell flyout does not disable scrolling when FlyoutVerticalScrollMode is set to Disabled by @NanthiniMahalingam in #32734 <details> <summary>🔧 Fixes</summary> - [[Android] Shell.FlyoutVerticalScrollMode="Disabled" does not disable scrolling](#32477) </details> ## Single Project - Fix: Throw a clear error when an SVG lacks dimensions instead of a NullReferenceException by @Shalini-Ashokan in #33194 <details> <summary>🔧 Fixes</summary> - [MAUI Fails To Convert Valid SVG Files Into PNG Files (Object reference not set to an instance of an object)](#32460) </details> ## SwipeView - [iOS] Fix SwipeView stays open on iOS after updating content by @devanathan-vaithiyanathan in #31248 <details> <summary>🔧 Fixes</summary> - [[iOS] - Swipeview with collectionview issue](#19541) </details> ## TabbedPage - [Windows] Fixed IsEnabled Property not works on Tabs by @NirmalKumarYuvaraj in #26728 <details> <summary>🔧 Fixes</summary> - [ShellContent IsEnabledProperty does not work](#5161) - [[Windows] Shell Tab IsEnabled Not Working](#32996) </details> - [Android] Fix NavigationBar overlapping StatusBar when NavigationBar visibility changes by @Vignesh-SF3580 in #33359 <details> <summary>🔧 Fixes</summary> - [[Android] NavigationBar overlaps with StatusBar when mixing HasNavigationBar=true/false in TabbedPage on Android 15 (API 35)](#33340) </details> ## Templates - Fix for unable to open task using keyboard navigation on windows platform by @SuthiYuvaraj in #33647 <details> <summary>🔧 Fixes</summary> - [Unable to open task using keyboard: A11y_.NET maui_User can get all the insights of Dashboard_Keyboard](#30787) </details> ## TitleView - Fix for NavigationPage.TitleView does not expand with host window in iPadOS 26+ by @SuthiYuvaraj in #33088 ## Toolbar - [iOS] Fix toolbar items ignoring BarTextColor on iOS/MacCatalyst 26+ by @Shalini-Ashokan in #34036 <details> <summary>🔧 Fixes</summary> - [[iOS 26] ToolbarItem color with custom BarTextColor not working](#33970) </details> - [Android] Fix for ToolbarItem retaining the icon from the previous page on Android when using NavigationPage. by @BagavathiPerumal in #32311 <details> <summary>🔧 Fixes</summary> - [Toolbaritem keeps the icon of the previous page on Android, using NavigationPage (not shell)](#31727) </details> ## WebView - [Android] Fix WebView in a grid expands beyond it's cell by @devanathan-vaithiyanathan in #32145 <details> <summary>🔧 Fixes</summary> - [Android - WebView in a grid expands beyond it's cell](#32030) </details> ## Xaml - ContentPresenter: Propagate binding context to children with explicit TemplateBinding by @HarishwaranVijayakumar in #30880 <details> <summary>🔧 Fixes</summary> - [Binding context in ContentPresenter](#23797) </details> <details> <summary>🔧 Infrastructure (1)</summary> - [Revert] ContentPresenter: Propagate binding context to children with explicit TemplateBinding by @Ahamed-Ali in #34332 </details> <details> <summary>🧪 Testing (6)</summary> - [Testing] Feature Matrix UITest Cases for Shell Flyout Page by @NafeelaNazhir in #32525 - [Testing] Feature Matrix UITest Cases for Brushes by @LogishaSelvarajSF4525 in #31833 - [Testing] Feature Matrix UITest Cases for BindableLayout by @LogishaSelvarajSF4525 in #33108 - [Android] Add UI tests for Material 3 CheckBox by @HarishwaranVijayakumar in #34126 <details> <summary>🔧 Fixes</summary> - [[Android] Add UI tests for Material 3 CheckBox](#34125) </details> - [Testing] Feature Matrix UITest Cases for Shell Tabbed Page by @NafeelaNazhir in #33159 - [Testing] Fixed Test case failure in PR 34294 - [03/2/2026] Candidate - 1 by @TamilarasanSF4853 in #34334 </details> <details> <summary>📦 Other (2)</summary> - Bumps Syncfusion.Maui.Toolkit dependency to version 1.0.9 by @PaulAndersonS in #34178 - Fix crash when closing Windows based app when using TitleBar by @MFinkBK in #34032 <details> <summary>🔧 Fixes</summary> - [Unhandled exception "Value does not fall within the expected range" when closing Windows app](#32194) </details> </details> **Full Changelog**: main...inflight/candidate
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!
Root cause
The issue occurs because Android’s RecyclerView natively invokes OnScrolled(0, 0) during the initial layout to notify changes in visible item positions, even when no actual scrolling has occurred. The .NET MAUI CollectionView handler was treating this layout callback as a user scroll event, causing the Scrolled event to fire when the page loaded. This resulted in behavior that was inconsistent with iOS and violated the expectation that the Scrolled event should only be raised in response to actual user interaction.
Description of Issue Fix
The fix introduces a boolean flag (_isInitialLayout) to filter out the first OnScrolled() invocation when both deltas are zero, identifying it as the initial layout callback. This flag is reset when the adapter is updated to correctly handle subsequent layout cycles. As a result, the Scrolled event is now raised only for genuine scroll activity, ensuring consistent behavior across Android and iOS platforms.
Tested the behavior in the following platforms.
Issues Fixed
Fixes #33333
Output
33333-BeforeFix.mov
33333-AfterFix.mov