Skip to content

[Android] Fix for unexpected CurrentItem updates in CarouselView when scroll animation is enabled#31875

Closed
SyedAbdulAzeemSF4852 wants to merge 6 commits into
dotnet:mainfrom
SyedAbdulAzeemSF4852:fix-carouselview-inconsistent-notify
Closed

[Android] Fix for unexpected CurrentItem updates in CarouselView when scroll animation is enabled#31875
SyedAbdulAzeemSF4852 wants to merge 6 commits into
dotnet:mainfrom
SyedAbdulAzeemSF4852:fix-carouselview-inconsistent-notify

Conversation

@SyedAbdulAzeemSF4852
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

  • On Android, when selecting an item in CarouselView with scroll animation enabled (IsScrollAnimated = true), the final item is updated correctly, but unnecessary and incorrect intermediate CurrentItem updates are triggered.

Root Cause

  • When IsScrollAnimated="True" is enabled on the CarouselView, programmatically selecting an item triggers multiple OnScrolled events during the smooth scroll animation. Each of these intermediate scroll events invokes the CarouselViewScrolled method, which updates the selected position based on the CenterItemIndex. As a result, CurrentItem changes multiple times throughout the animation, leading to inconsistent binding updates.

Description of Change

  • Enhanced the CarouselViewOnScrollListener to track whether scrolling is programmatic or user-initiated by introducing _isProgrammaticScrolling, _lastDx, and _lastDy fields, and updating the scroll state handling to process programmatic scrolls only after the animation completes.

Issues Fixed

Fixes #31874

Validated the behaviour in the following platforms

  • Windows
  • Android
  • iOS
  • Mac

Output

Before After
Before.mov
After.mov

@dotnet-policy-service dotnet-policy-service Bot added community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration labels Oct 6, 2025
@SyedAbdulAzeemSF4852 SyedAbdulAzeemSF4852 marked this pull request as ready for review October 7, 2025 12:06
Copilot AI review requested due to automatic review settings October 7, 2025 12:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR addresses an issue where CarouselView on Android generates unexpected intermediate CurrentItem updates during scroll animations. When IsScrollAnimated="True" is enabled and an item is programmatically selected, the smooth scroll animation triggers multiple OnScrolled events that incorrectly update the CurrentItem binding multiple times.

Key changes:

  • Enhanced CarouselViewOnScrollListener to differentiate between programmatic and user-initiated scrolling
  • Added logic to defer CurrentItem updates during programmatic scrolling until animation completes
  • Added comprehensive UI tests to validate the fix

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs Core fix: Added programmatic scroll detection and deferred processing logic
src/Controls/tests/TestCases.HostApp/Issues/Issue31874.cs UI test page demonstrating the issue with CarouselView scroll animation
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31874.cs NUnit test implementation to validate the fix

HeightRequest = 150,
Loop = false,
IsScrollAnimated = true,
ItemsSource = new ObservableCollection<string>{ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11" },
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider using a more readable format for the collection initialization, such as using Enumerable.Range(0, 12).Select(i => i.ToString()) or splitting across multiple lines for better readability.

Copilot uses AI. Check for mistakes.
Comment thread src/Controls/tests/TestCases.HostApp/Issues/Issue31874.cs Outdated
Comment thread src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs Outdated
@jsuarezruiz
Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

@SyedAbdulAzeemSF4852 SyedAbdulAzeemSF4852 force-pushed the fix-carouselview-inconsistent-notify branch from bade20d to 735613a Compare December 8, 2025 19:11
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Dec 8, 2025

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 31875

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 31875"

@rmarinho
Copy link
Copy Markdown
Member

rmarinho commented Feb 18, 2026

🤖 AI Summary

📊 Expand Full Review
🔍 Pre-Flight — Context & Validation
📝 Review SessionUsed constants for item values and removed redundant recyclerView null check · 735613a

Issue: #31874 - [Android] Incorrect Intermediate CurrentItem updates with CarouselView Scroll Animation Enabled
Platforms Affected: Android only
Files Changed: 1 fix file, 2 test files

Issue Summary

On Android, when programmatically selecting a CarouselView item with IsScrollAnimated = true, the CurrentItem property fires multiple intermediate updates (for intermediate scroll positions) before settling on the final target item. This causes incorrect binding notifications. On iOS, the behavior is correct.

Fix Approach

The PR introduces _isProgrammaticScrolling, _lastDx, and _lastDy fields in CarouselViewOnScrollListener. When ScrollStateSettling fires without dragging, it sets _isProgrammaticScrolling = true, suppressing OnScrolled processing until ScrollStateIdle fires, at which point only the final cached scroll data is processed.

Reviewer Discussion (jsuarezruiz)

File:Line Reviewer Says Author Says Status
CarouselViewOnScrollListener.cs:35 "Could incorrectly detect programmatic scroll when user drags and releases — enters Settling with IsDragging=false" "Aligns Android with iOS behavior; prevents intermediate CurrentItem updates during fast drags" ⚠️ INVESTIGATE
CarouselViewOnScrollListener.cs:47 "If CurrentItem changes twice before first animation completes, second overwrites _lastDx/_lastDy losing first scroll" "Introduced queue approach but noted it only captures intermediate deltas, not discrete transitions" ⚠️ INVESTIGATE

Edge Cases from Discussion

  • User drags fast → releases → carousel settles: Could set _isProgrammaticScrolling = true incorrectly
  • Multiple rapid programmatic CurrentItem changes before idle: _lastDx/_lastDy overwritten (queue approach was considered but not implemented)

File Classification

  • Fix files: src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs (+39/-6)
  • Test files: src/Controls/tests/TestCases.HostApp/Issues/Issue31874.cs (+93), src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31874.cs (+27)

Test Type

UI Tests (Appium - TestCases.HostApp + TestCases.Shared.Tests)

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #31875 Track programmatic scroll via _isProgrammaticScrolling flag; suppress OnScrolled during animation; replay at Idle ⏳ PENDING (Gate) CarouselViewOnScrollListener.cs (+39/-6) Original PR

🚦 Gate — Test Verification
📝 Review SessionUsed constants for item values and removed redundant recyclerView null check · 735613a

Result: ✅ PASSED
Platform: android
Mode: Full Verification

  • Tests FAIL without fix ✅
  • Tests PASS with fix ✅

Test: VerifyCurrentItemUpdatesWithScrollAnimation (Issue31874)

Build time: ~1.5 min per run. Both runs completed successfully.
Fix file verified: src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs


🔧 Fix — Analysis & Comparison
📝 Review SessionUsed constants for item values and removed redundant recyclerView null check · 735613a

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #31875 Track _isProgrammaticScrolling flag; defer OnScrolled during Settling state; replay at Idle ✅ PASS (Gate) CarouselViewOnScrollListener.cs (+39/-6) Original PR

Exhausted: No (skipped - environment blocker: 429 rate limit on task agent for all 3 attempts)
Selected Fix: PR's fix - Gate confirmed it works; try-fix exploration was blocked by rate limits.

Skipped Reason

Phase 3 try-fix was blocked by repeated 429 Too Many Requests errors when attempting to invoke the task agent with claude-sonnet-4.5. After 3 retry attempts, proceeded directly to Report per autonomous execution rules.


📋 Report — Final Recommendation
📝 Review SessionUsed constants for item values and removed redundant recyclerView null check · 735613a

✅ Final Recommendation: APPROVE

Summary

PR #31875 fixes an Android-only bug in CarouselViewOnScrollListener where programmatic CurrentItem changes with IsScrollAnimated = true fire multiple intermediate incorrect PropertyChanged notifications during the scroll animation. The fix correctly defers scroll processing until ScrollStateIdle. Gate verification confirmed tests fail without the fix and pass with it.

Root Cause

RecyclerView's smooth scroll animation calls OnScrolled on every frame as it animates through intermediate positions. The CarouselViewScrolled method in the base class uses CenterItemIndex to update CurrentItem — so every intermediate scroll frame updates CurrentItem to an intermediate item, producing spurious notifications. On iOS, there is no equivalent intermediate event.

Fix Quality

The fix introduces _isProgrammaticScrolling, _lastDx, and _lastDy fields. When ScrollStateSettling fires without dragging, it marks the scroll as programmatic and suppresses base.OnScrolled() calls during animation, replaying only once when ScrollStateIdle fires.

Strengths:

  • ✅ Gate passed: tests fail without fix, pass with fix (empirically verified)
  • ✅ Detection logic is sound: !IsDragging during Settling correctly identifies programmatic scrolls since drag state is not cleared until Idle
  • ✅ Loop mode handled correctly: when OnScrollStateChanged replays OnScrolled at Idle with _isProgrammaticScrolling = false, the else branch runs and calls CheckPendingScrollToEvents
  • ✅ Test page uses PlatformAffected.Android, appropriate for Android-only bug
  • ✅ UI test with #if TEST_FAILS_ON_WINDOWS guards appropriately

Concerns (non-blocking):

  • ⚠️ _lastDx/_lastDy overwrite if two rapid programmatic scrolls fire before Idle — acknowledged by reviewer; no fix applied; common case is fine
  • ⚠️ Both test files missing newline at end of file
  • ⚠️ Test doesn't verify that the animation actually triggered (only checks result label)

PR Title Assessment

Current: [Android] Fix for unexpected CurrentItem updates in CarouselView when scroll animation is enabled
Assessment: Acceptable. Could be more concise: [Android] CarouselView: Suppress intermediate CurrentItem notifications during programmatic scroll animation

PR Description Assessment

The description is adequate with NOTE block, root cause, description of change, issues fixed, and platform validation checkboxes. Structure is acceptable.

Phase Results

Phase Status Notes
Pre-Flight ✅ COMPLETE Android-only bug, 3 files changed
Gate ✅ PASSED Tests fail without fix, pass with fix on Android
Fix (try-fix) ⚠️ SKIPPED Rate limit blocked task agent after 3 attempts
Report ✅ COMPLETE

📋 Expand PR Finalization Review
Title: ✅ Good

Current: [Android] Fix for unexpected CurrentItem updates in CarouselView when scroll animation is enabled

Description: ✅ Good

Description needs updates. See details below.

✨ 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!

Issue Details

On Android, when selecting an item in CarouselView with scroll animation enabled (IsScrollAnimated = true), the final item is updated correctly, but unnecessary and incorrect intermediate CurrentItem updates are triggered during the animation. This does not occur when IsScrollAnimated = false, and does not occur on iOS.

Root Cause

When IsScrollAnimated="True" is enabled on the CarouselView, programmatically selecting an item triggers multiple OnScrolled events during the smooth scroll animation. Each intermediate scroll event invokes CarouselViewScrolled, which updates the selected position based on CenterItemIndex. As a result, CurrentItem changes multiple times throughout the animation, leading to inconsistent binding updates.

Description of Change

Enhanced CarouselViewOnScrollListener to distinguish programmatic scrolls from user-initiated scrolls:

  • Added _isProgrammaticScrolling, _lastDx, and _lastDy fields.
  • In OnScrollStateChanged: when ScrollStateSettling fires and the view is not being dragged, the scroll is marked as programmatic. Intermediate OnScrolled events cache dx/dy instead of processing immediately. When ScrollStateIdle is reached, the cached scroll data is processed once, ensuring CurrentItem is only updated after the animation completes.
  • For user-initiated (drag) scrolls, OnScrolled continues to be processed immediately as before.

Known limitation: If CurrentItem is changed programmatically twice in rapid succession before the first animation completes, only the latest _lastDx/_lastDy values are retained. The first programmatic scroll's final position may not be fully resolved. This is an edge case acknowledged during review.

Issues Fixed

Fixes #31874

Platforms Affected

  • Android – fix applies here only
  • iOS/MacCatalyst/Windows – no behavior change (fix is in Android-specific handler Items/Android/CarouselViewOnScrollListener.cs)

Validated the behaviour in the following platforms

  • Windows (test excluded via #if TEST_FAILS_ON_WINDOWS)
  • Android
  • iOS
  • Mac
Code Review: ✅ Passed

Code Review – PR #31875

🟡 Suggestions


1. Programmatic scroll detection can incorrectly fire for fast user drags

File: src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs
Line: ~35

if (state == RecyclerView.ScrollStateSettling && !_carouselView.IsDragging)
{
    _isProgrammaticScrolling = true;
}

Problem: When the user drags the carousel quickly and releases their finger:

  1. IsDragging becomes false (finger lifted)
  2. State transitions to ScrollStateSettling (inertia/snap still happening)
  3. _isProgrammaticScrolling is set to true

This means fast user drags also get treated as programmatic scrolls — OnScrolled is suppressed and CurrentItem only updates once, after the settling completes. This changes the existing behavior for user-initiated drags.

Author's response in review: This is intentional; it aligns Android behavior with iOS (which also only updates CurrentItem after the gesture settles). The author provided comparison videos showing the behaviors match.

Recommendation: If the behavioral alignment with iOS is intentional, document this in a code comment. It changes Android's existing user-drag behavior and reviewers/future developers need to understand it's deliberate:

// Note: this also covers the case where a user drag transitions to settling
// (IsDragging becomes false before ScrollStateSettling fires). This intentionally
// aligns Android behavior with iOS: CurrentItem is only updated once scrolling
// fully completes, not during the settling phase.
if (state == RecyclerView.ScrollStateSettling && !_carouselView.IsDragging)
{
    _isProgrammaticScrolling = true;
}

2. UI test timing: result may be checked before animation completes

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31874.cs
Lines: 20–24

App.WaitForElement("Issue31874ScrollBtn");
App.Tap("Issue31874ScrollBtn");

var resultLabel = App.WaitForElement("Issue31874ResultLabel").GetText();
Assert.That(resultLabel, Is.EqualTo("Success"));

Problem: After tapping the button, the scroll animation is asynchronous. WaitForElement only checks the element exists and has text — it doesn't wait for the animation to complete. The resultStatus label starts as "Success" and only changes to "Failure" if an intermediate CurrentItem change is observed while the animation is in flight. If the UI test reads the label before the animation has progressed far enough to fire intermediate events, it may always read "Success" regardless of whether the fix is in place.

Recommendation: Add a small wait or a more robust assertion approach. One option: check after a short deliberate pause to allow the animation to proceed, or check for a "settled" state indicator. At minimum, this should be noted as a known flakiness risk.


3. #if TEST_FAILS_ON_WINDOWS is incorrect for this Android-only issue

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31874.cs

#if TEST_FAILS_ON_WINDOWS // Issue Link - https://github.com/dotnet/maui/issues/31670

Problem: The test is wrapped in #if TEST_FAILS_ON_WINDOWS, which means it compiles and runs on Android, iOS, and MacCatalyst — but not Windows. The referenced issue (#31670) is unrelated to this bug. The actual issue (#31874) is Android-specific. The test running on iOS and MacCatalyst is harmless (since the bug doesn't exist there, it will always pass), but it's misleading and wastes CI time.

Recommendation: Either:

  • Add a platform guard comment explaining the scope: // Android-only issue; test runs on Android, iOS, Mac (passes on iOS/Mac because bug doesn't exist there)
  • Or reference the correct issue link in the #if comment

4. Known race condition: rapid successive programmatic scrolls

File: src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs
Lines: ~43–50

_lastDx = dx;
_lastDy = dy;

Problem: If CurrentItem is changed programmatically twice before the first animation completes (e.g., rapid successive updates), each OnScrolled call overwrites _lastDx/_lastDy. When ScrollStateIdle fires, only the last-cached delta is processed. The first scroll's final position is never explicitly finalized.

The author explored a queue-based approach but found it captured all incremental deltas rather than only item-transition deltas, which introduced different issues.

Current state: This is acknowledged as a known edge case in the review thread with no resolution yet.

Recommendation: Add a code comment documenting this limitation so future maintainers understand the tradeoff:

// Note: Only the most recent delta is retained. If CurrentItem is changed
// programmatically multiple times during a single animation, earlier intermediate
// positions are not separately finalized. This is an accepted limitation.
_lastDx = dx;
_lastDy = dy;

ℹ️ Nitpicks

  • Missing newline at end of file in both Issue31874.cs test files. Both end with \\ No newline at end of file. Minor but inconsistent with repo conventions.
  • Magic strings in HostApp test ("0" and "4" used inline in label text and string comparisons). The constants InitialItem and TargetItem are defined at the top of the class, but selectedItemLabel text is constructed inline rather than referencing them — a nitpick from the automated review thread.

✅ Looks Good

  • The core fix logic is simple and well-targeted: cache scroll data during animation, process once at idle.
  • CarouselViewLoopManager.CheckPendingScrollToEvents is correctly preserved for user-initiated scrolls.
  • The HostApp test page (Issue31874.cs) includes AutomationId attributes and a clear pass/fail label, following established patterns.
  • The [Category(UITestCategories.CarouselView)] attribute is correctly applied.
  • The PR is Android-specific and correctly touches only the Items/Android/ handler (not Items2/).

@rmarinho rmarinho added s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Feb 18, 2026
@kubaflo kubaflo added s/agent-fix-lose Author adopted the agent's fix and it turned out to be bad s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-win AI found a better alternative fix than the PR s/agent-fix-lose Author adopted the agent's fix and it turned out to be bad labels Feb 20, 2026
@SyedAbdulAzeemSF4852 SyedAbdulAzeemSF4852 force-pushed the fix-carouselview-inconsistent-notify branch from 735613a to dfb7c5a Compare March 2, 2026 17:55
@SyedAbdulAzeemSF4852
Copy link
Copy Markdown
Contributor Author

Addressed concerns raised in the AI summary.

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR and removed s/agent-approved AI agent recommends approval - PR fix is correct and optimal labels Mar 29, 2026
@MauiBot MauiBot removed the s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates label Mar 29, 2026
Copy link
Copy Markdown
Contributor

@kubaflo kubaflo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test failed

@SyedAbdulAzeemSF4852
Copy link
Copy Markdown
Contributor Author

The test failed

@kubaflo, The gate failed, but I verified locally: the test fails without the fix and passes with it.

@MauiBot MauiBot added s/agent-review-incomplete s/agent-changes-requested AI agent recommends changes - found a better alternative or issues and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-review-incomplete labels Mar 30, 2026
@dotnet dotnet deleted a comment from MauiBot Mar 30, 2026
@dotnet dotnet deleted a comment from MauiBot Mar 30, 2026
@dotnet dotnet deleted a comment from MauiBot Mar 30, 2026
@dotnet dotnet deleted a comment from MauiBot Mar 30, 2026
@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented May 3, 2026

🤖 AI Summary

👋 @SyedAbdulAzeemSF4852 — new AI review results are available. Please review the latest session below.

📊 Review Session99d3d84 · Add clarifying comments for scroll detection behavior and known race condition in CarouselView · 2026-05-03 21:05 UTC
🚦 Gate — Test Before & After Fix

Gate Result: ❌ FAILED

Platform: IOS · Base: main · Merge base: 1463c4c5

🩺 Test does not reproduce the bug — ran the same in both states (PASS without fix, PASS with fix). The repro test is not exercising the issue. Strengthen the test before reviewing the fix.

Test Without Fix (expect FAIL) With Fix (expect PASS)
🖥️ Issue31874 Issue31874 ❌ PASS — 233s ✅ PASS — 90s
🔴 Without fix — 🖥️ Issue31874: PASS ❌ · 233s
  Determining projects to restore...
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 530 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 634 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 5.13 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/Foldable/src/Controls.Foldable.csproj (in 5.39 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Xaml/Controls.Xaml.csproj (in 5.38 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 5.39 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj (in 4.21 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 5.4 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/BlazorWebView/src/Maui/Microsoft.AspNetCore.Components.WebView.Maui.csproj (in 5.41 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/Maps/src/Controls.Maps.csproj (in 5.43 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Core/maps/src/Maps.csproj (in 5.44 sec).
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0-ios26.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0-ios26.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0-ios26.0/Microsoft.Maui.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Maps.dll
  Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Controls.Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Maps.dll
  Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Xaml.dll
  Controls.Foldable -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Foldable.dll
  Microsoft.AspNetCore.Components.WebView.Maui -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-ios26.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
  Detected signing identity:
    Code Signing Key: "" (-)
    Provisioning Profile: "" () - no entitlements
    Bundle Id: com.microsoft.maui.uitests
    App Id: com.microsoft.maui.uitests
  Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-ios/iossimulator-arm64/Controls.TestCases.HostApp.dll
  Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
  Optimizing assemblies for size. This process might take a while.

Build succeeded.

/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
    1 Warning(s)
    0 Error(s)

Time Elapsed 00:02:01.11
  Determining projects to restore...
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 662 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/VisualTestUtils/VisualTestUtils.csproj (in 672 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 662 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/CustomAttributes/Controls.CustomAttributes.csproj (in 672 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 662 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Core/UITest.Core.csproj (in 2 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 742 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 393 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.NUnit/UITest.NUnit.csproj (in 1.44 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Appium/UITest.Appium.csproj (in 2.25 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/VisualTestUtils.MagickNet/VisualTestUtils.MagickNet.csproj (in 3.61 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.iOS.Tests/Controls.TestCases.iOS.Tests.csproj (in 3.68 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Analyzers/UITest.Analyzers.csproj (in 2.99 sec).
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Controls.CustomAttributes -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
  Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
  VisualTestUtils -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
  UITest.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
  VisualTestUtils.MagickNet -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
  UITest.NUnit -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
  UITest.Appium -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
  UITest.Analyzers -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
  Controls.TestCases.iOS.Tests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
Test run for /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (arm64)

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.06]   Discovering: Controls.TestCases.iOS.Tests
[xUnit.net 00:00:00.15]   Discovered:  Controls.TestCases.iOS.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 5/3/2026 1:51:57 PM FixtureSetup for Issue31874(iOS)
>>>>> 5/3/2026 1:52:01 PM VerifyCurrentItemUpdatesWithScrollAnimation Start
>>>>> 5/3/2026 1:52:02 PM VerifyCurrentItemUpdatesWithScrollAnimation Stop
  Passed VerifyCurrentItemUpdatesWithScrollAnimation [814 ms]
NUnit Adapter 4.5.0.0: Test execution complete

Test Run Successful.
Total tests: 1
     Passed: 1
 Total time: 1.1242 Minutes

🟢 With fix — 🖥️ Issue31874: PASS ✅ · 90s
  Determining projects to restore...
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 461 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 420 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 481 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 502 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 515 ms).
  6 of 11 projects are up-to-date for restore.
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0-ios26.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0-ios26.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0-ios26.0/Microsoft.Maui.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Maps.dll
  Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Controls.Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Maps.dll
  Microsoft.AspNetCore.Components.WebView.Maui -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-ios26.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
  Controls.Foldable -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Foldable.dll
  Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Xaml.dll
  Detected signing identity:
    Code Signing Key: "" (-)
    Provisioning Profile: "" () - no entitlements
    Bundle Id: com.microsoft.maui.uitests
    App Id: com.microsoft.maui.uitests
  Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-ios/iossimulator-arm64/Controls.TestCases.HostApp.dll
  Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
  Optimizing assemblies for size. This process might take a while.

Build succeeded.

/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
    1 Warning(s)
    0 Error(s)

Time Elapsed 00:00:46.28
  Determining projects to restore...
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 349 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 390 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 391 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 423 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 443 ms).
  8 of 13 projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  Controls.CustomAttributes -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003640
  Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
  UITest.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
  VisualTestUtils -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
  VisualTestUtils.MagickNet -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
  UITest.Appium -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
  UITest.NUnit -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
  UITest.Analyzers -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
  Controls.TestCases.iOS.Tests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
Test run for /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (arm64)

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.07]   Discovering: Controls.TestCases.iOS.Tests
[xUnit.net 00:00:00.16]   Discovered:  Controls.TestCases.iOS.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 5/3/2026 1:53:27 PM FixtureSetup for Issue31874(iOS)
>>>>> 5/3/2026 1:53:31 PM VerifyCurrentItemUpdatesWithScrollAnimation Start
>>>>> 5/3/2026 1:53:32 PM VerifyCurrentItemUpdatesWithScrollAnimation Stop
  Passed VerifyCurrentItemUpdatesWithScrollAnimation [858 ms]
NUnit Adapter 4.5.0.0: Test execution complete

Test Run Successful.
Total tests: 1
     Passed: 1
 Total time: 18.7978 Seconds

⚠️ Failure Details

  • Issue31874 PASSED without fix (should fail) — tests don't catch the bug
📁 Fix files reverted (2 files)
  • eng/pipelines/ci-copilot.yml
  • src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs

🧪 UI Tests — Category Detection

Detected UI test categories: CarouselView,CollectionView


🔍 Regression Cross-Reference

🔍 Regression Cross-Reference

🟢 No regression risks detected. No labeled bug-fix PRs in the last 6 months touched the modified files.


🔍 Pre-Flight — Context & Validation

PR # Pre-Flight Summary31875

PR Overview

Title: [Android] Fix for unexpected CurrentItem updates in CarouselView when scroll animation is enabled
Author: SyedAbdulAzeemSF4852 (Syncfusion partner)
Base branch: main
Status: Open, not draft
Labels: , partner/syncfusion, s/agent-reviewed, s/agent-changes-requested, s/agent-gate-passed, s/agent-fix-win community

Issue Being Fixed

Issue # On Android, when selecting an item in CarouselView with IsScrollAnimated = true, intermediate CurrentItem property-change events fire for items between the start and target position. This does NOT happen on iOS. The fix should make Android match iOS behavior: CurrentItem updates only once, when scrolling fully completes.31874

Changed Files

File Change
src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs adds _isProgrammaticScrolling, _lastDx, _lastDy fields; defers OnScrolled processing until ScrollStateIdle for programmatic scrolls
src/Controls/tests/TestCases.HostApp/Issues/Issue31874.cs HostApp test page
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31874.cs NUnit UI test

Root Cause (as described by PR)

CarouselView.CurrentItem is set by CarouselViewScrolled via CenterItemIndex inside OnScrolled. When IsScrollAnimated=true, programmatic CurrentItem assignment triggers a smooth scroll animation, which fires many OnScrolled callbacks (one per animation frame). Each callback updates CurrentItem, causing spurious intermediate updates.

Fix Approach

The PR adds _isProgrammaticScrolling state tracking. When ScrollStateSettling fires and IsDragging is false, _isProgrammaticScrolling is set to true. While true, OnScrolled callbacks just cache dx/dy. When ScrollStateIdle arrives, the cached values are replayed via a single OnScrolled call.

Known Problems with the Fix

Critical Bug: User-Initiated Fling Misclassified as Programmatic

When a user drags and releases (fling):
SetIsDragging(true)
SetIsDragging(false) (code runs BEFORE the check)
3. The check state == ScrollStateSettling && !_carouselView.IsDragging evaluates to true even for user flings, because IsDragging was just set to false in the block above.

The PR comment acknowledges this but calls it " but it's a behavior regression for user-initiated scroll settling.intentional"

Offset Accumulation Skew

The base RecyclerViewScrollListener maintains _horizontalOffset and _verticalOffset accumulators inside base.OnScrolled(). During programmatic scrolling, all intermediate OnScrolled calls are suppressed. The final replay sends only the last delta, not the cumulative delta. This means ItemsViewScrolledEventArgs.HorizontalOffset/VerticalOffset will be incorrect after a programmatic animated scroll.

Loop Mode Gap

When _isProgrammaticScrolling is true and scroll is in progress, CheckPendingScrollToEvents(recyclerView) is never called. The fix resets _isProgrammaticScrolling to false before calling OnScrolled at idle, so the loop manager's pending events ARE processed then. But there is a timing gap: any pending loop scroll events during the animation are deferred longer than before.

Test Quality Issue

The test is wrapped in #if TEST_FAILS_ON_WINDOWS (referencing issue #31670). However, the bug is Android-only, not a Windows-skipping concern. The test should use #if ANDROID or equivalent. Running on iOS (as the gate did) shows the test passes in both with-fix and without-fix states, confirming the test does NOT reproduce the bug on non-Android platforms.

Gate Result

** Gate ran on iOS. The test Issue31874 passed in both pre-fix and post-fix states (233s and 90s respectively). The gate failure indicates the test does not reliably reproduce the Android-specific bug.FAILED**

Related Context

  • RecyclerViewScrollListener base class: manages _horizontalOffset, _verticalOffset accumulators via base.OnScrolled()
  • CarouselViewLoopManager.CheckPendingScrollToEvents(): called during OnScrolled for loop mode to handle centering
  • _carouselView.IsDragging: set via SetIsDragging() when IsSwipeEnabled is true
  • The IsDragging state is set to false unconditionally for any non-Dragging state BEFORE the programmatic detection check runs

Candidate Generation Strategy

  • pr: The current PR fix as-is
  • pr-plus-reviewer: PR fix with expert reviewer feedback applied (fix the IsDragging detection bug, fix test guard)
  • try-fix-1: State-machine approach tracking drag-started flag to properly distinguish user vs. programmatic
  • try-fix-2: Approach using IsScrollAnimated check to gate the deferral behavior
  • try-fix-3: Approach overriding ScrollTo to set a flag before animation starts
  • try-fix-4: Approach using IsDragging snapshot taken at drag-start time (save flag at Dragging state)

🔧 Fix — Analysis & Comparison

Try-Fix Phase PR #31875Summary

Overview

Four independent candidates were generated to address the broken programmatic-scroll detection in CarouselViewOnScrollListener.cs. All four candidates fix the core bug (IsDragging always-false at Settling). Candidates vary by secondary concerns (test fix, IsScrollAnimated guard, IsSwipeEnabled edge case).


Candidate 1: _wasDraggingBeforeSettle State-Machine Approach

Domain: Android RecyclerView Scroll State Machine

Introduces _wasDraggingBeforeSettle set on ScrollStateDragging, checked at ScrollStateSettling. Also adds explicit _isProgrammaticScrolling = false on ScrollStateDragging to handle user-interrupts-programmatic-scroll scenario.

Key changes:

  • Add bool _wasDraggingBeforeSettle;
  • Set _wasDraggingBeforeSettle = true in ScrollStateDragging block (inside IsSwipeEnabled guard, )but
  • Add ScrollStateDragging arm to if/else if chain: resets _isProgrammaticScrolling = false
    !_wasDraggingBeforeSettle in the Settling check
  • Reset _wasDraggingBeforeSettle = false at ScrollStateIdle

Strengths: Handles programmatic-interrupted-by-user cleanly. Correct for IsSwipeEnabled=true.
Weakness: _wasDraggingBeforeSettle set inside IsSwipeEnabled may not fire with IsSwipeEnabled=false.guard


Candidate 2: _userInitiatedScroll + IsScrollAnimated Guard

Domain: API Surface / IsScrollAnimated property

Adds _userInitiatedScroll (set outside IsSwipeEnabled guard at ScrollStateDragging) AND checks _carouselView.IsScrollAnimated in the Settling guard. The IsScrollAnimated check eliminates an entire class of false positives (non-animated programmatic scrolls skip settling entirely anyway).

Key changes:

  • Add bool _userInitiatedScroll;
    _userInitiatedScroll = true, _isProgrammaticScrolling = false
  • Settling guard: IsScrollAnimated && !_userInitiatedScroll
  • Idle: reset _userInitiatedScroll = false

Strengths: Strongest semantic the extra IsScrollAnimated guard eliminates any scenario where non-animated programmatic scrolls could accidentally trigger deferral. _userInitiatedScroll is set outside IsSwipeEnabled guard. correctness
Weakness: Slightly more complex; reads a MAUI property in a RecyclerView callback.


Candidate 3: _wasDragging Minimal Snapshot

Domain: Handler Lifecycle

The simplest possible one new field _wasDragging, set inside the IsSwipeEnabled guard at Dragging, checked at Settling, reset at Idle.fix

Key changes:

  • Add bool _wasDragging;
    ScrollStateDragging block
    !_wasDragging
  • Reset at Idle

Strengths: Minimal change, easy to review.
Weakness: _wasDragging inside IsSwipeEnabled won't fire with IsSwipeEnabled=false. No explicit cancellation when user interrupts a programmatic scroll.guard


Candidate 4: _wasDraggingBeforeSettle + Test Guard Fix

Domain: Test Quality + Minimal Core Fix

Same scroll listener fix as candidate 1, PLUS fixes the test platform guard and the test's issue comment reference.

Key changes (scroll listener): Same as candidate 1
Key changes (test):
#if TEST_FAILS_ON_WINDOWS && TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST
#31874

Strengths: Most fixes both the logic bug and the test quality issue that caused the gate failure. complete
Weakness: Still doesn't fix offset accumulator skew or timing race in test.


Cross-Candidate Comparison

try-fix-1 try-fix-2 try-fix-3 try-fix-4
Fixes IsDragging detection
Handles partial IsSwipeEnabled= partial false
Handles user-interrupts - programmatic
IsScrollAnimated guard
Fixes test guard
Complexity Low Medium Minimal Low

Gate Results

ran on iOS; test trivially passes on iOS because iOS doesn't use RecyclerView. This does not reflect on the quality of the scroll listener fix. All try-fix candidates address the Android-specific issue; the gate result for iOS is not indicative of their correctness.FAILED Gate


📋 Report — Final Recommendation

Multi-Candidate Comparative PR #31875Report

Gate Status

Gate Platform: iOS. The test Issue31874 passed in both pre-fix and post-fix states on iOS (no bug on iOS). The gate failure is a test quality issue, not a fix quality issue: the test guard #if TEST_FAILS_ON_WINDOWS should restrict the test to Android only, but instead allows it to run on iOS where it trivially passes. This directly explains the gate failure.FAILED


Candidate Summary

Candidate Fixes IsDragging Detection Fixes Test Guard IsScrollAnimated Guard _userInitiatedScroll Outside IsSwipeEnabled Regression Risk
` (critical ( High wrong guard) bug) pr`
(inside guard) Medium ()
(inside guard) Medium (`_
() Low
() Low
(inside guard) Medium-Low ()

Detailed Analysis

Raw PR as Submittedpr

Verdict: Do not use.

Two critical blocking bugs:

  1. IsDragging is always false at ScrollStateSettling (set to false in the unconditional else block above the check). User flings are misclassified as CurrentItem only updates at full stop, regressing normal swipe behavior.programmatic
  2. Base-class _horizontalOffset/_verticalOffset accumulators are corrupted: only the last tiny animation-frame delta is replayed, not the total scroll distance. CarouselView.Scrolled events report wrong offsets.

Test guard is wrong (#if TEST_FAILS_ON_WINDOWS instead of Android-only), and there's a timing race in the test assertion.

Gate result: Would still FAIL on iOS (wrong platform). Would FAIL on Android too (logic regression in user swipe path; test timing race could produce false-positive).


pr-plus- PR + Expert Reviewer Fixes Appliedreviewer

Verdict: Good but not the strongest.

Fixes critical bug #1 (IsDragging detection) using _wasDraggingBeforeSettle. Fixes test guard to TEST_FAILS_ON_WINDOWS && TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST. Adds scroll-completion wait in test assertion.

Remaining gaps:

  • _wasDraggingBeforeSettle is set inside the IsSwipeEnabled won't fire with IsSwipeEnabled=falseguard
  • No IsScrollAnimated guard (non-animated programmatic scrolls never reach ScrollStateSettling anyway, so this is low risk)
  • Offset accumulator skew (critical issue Update README.md #2) not addressed
  • No explicit _isProgrammaticScrolling = false on ScrollStateDragging (user-interrupts-programmatic edge case)

Gate result: Would likely PASS on Android with test guard fix.


try-fix- _wasDraggingBeforeSettle State Machine1

Verdict: Correct core fix, incomplete test.

Fixes IsDragging detection correctly. Has explicit _isProgrammaticScrolling = false on ScrollStateDragging (handles user-interrupts-programmatic). Does NOT fix test guard.

Gate result: Would still FAIL on iOS (no test guard fix). Would likely PASS on Android IF test guard is fixed separately.


try-fix- _userInitiatedScroll+IsScrollAnimated Guard2

Verdict: Strongest logic fix, no test fix.

Most semantically correct scroll listener implementation:
handles IsSwipeEnabled=false correctly
narrows deferral to exactly the scenario that causes the bug
user-interrupts-programmatic handled

Does NOT fix test guard.

Gate result: Would FAIL on iOS (test guard wrong). Would PASS on Android.


try-fix- _userInitiatedScroll (Same as try-fix-2, independently derived)3

Verdict: Strongest logic fix, same gap as try-fix-2.

Functionally identical to try-fix-2. Arrived at the same optimal implementation independently. _userInitiatedScroll outside IsSwipeEnabled guard + IsScrollAnimated check + explicit reset on Dragging.

Gate result: Would FAIL on iOS (no test guard fix). Would PASS on Android.


try-fix- _wasDraggingBeforeSettle + Test Guard Fix4

**Verdict: Best overall fixes logic AND test quality.**candidate

Combines:

The test guard fix is the direct root cause of the gate failure. Fixing it means CI will only run this test on Android where the behavior can actually be validated.

Remaining gaps (shared with most candidates):

  • _wasDraggingBeforeSettle inside IsSwipeEnabled guard ( can be moved in follow-up)minor
  • No IsScrollAnimated guard (low non-animated programmatic scrolls don't reach Settling)risk
  • Offset accumulator skew (medium severity, separate follow-up)
  • Test timing race not addressed

Gate result: Would PASS on Android (test guard restricts to Android where fix is meaningful).


Ranking (Best to Worst)

  1. try-fix- Fixes both the logic bug AND the test quality issue that caused gate failure. Only candidate that addresses the gate root cause directly.4
  2. pr-plus- Fixes logic + test guard + timing race, but _wasDraggingBeforeSettleinsideIsSwipeEnabled guard.reviewer
  3. try-fix-3 / try-fix- Best logic fix quality, but no test guard fix.2
  4. try-fix- Same logic quality as try-fix-4, missing test fix.1
  5. Two critical blocking bugs. Not usable.pr

Winner: try-fix-4

The gate failure is caused by a wrong platform guard in the the test runs on iOS where it always passes, providing no signal. try-fix-4 is the only candidate that simultaneously fixes the detection regression AND addresses the root cause of the gate failure. The scroll listener logic in try-fix-4 is correct and equivalent to try-fix-1/pr-plus-reviewer.test

Note: If a future iteration is possible, incorporating try-fix-2/3's _userInitiatedScroll (outside IsSwipeEnabled guard) + IsScrollAnimated guard on top of try-fix-4's test fix would yield the optimal solution.


📋 Expand PR Finalization Review
Title: ✅ Good

Current: [Android] Fix for unexpected CurrentItem updates in CarouselView when scroll animation is enabled

Description: ✅ Good

Description needs updates. See details below.

✨ 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!

Issue Details

On Android, when selecting an item in CarouselView with scroll animation enabled (IsScrollAnimated = true), the final item is updated correctly, but unnecessary and incorrect intermediate CurrentItem updates are triggered during the animation. This does not occur when IsScrollAnimated = false, and does not occur on iOS.

Root Cause

When IsScrollAnimated="True" is enabled on the CarouselView, programmatically selecting an item triggers multiple OnScrolled events during the smooth scroll animation. Each intermediate scroll event invokes CarouselViewScrolled, which updates the selected position based on CenterItemIndex. As a result, CurrentItem changes multiple times throughout the animation, leading to inconsistent binding updates.

Description of Change

Enhanced CarouselViewOnScrollListener to distinguish programmatic scrolls from user-initiated scrolls:

  • Added _isProgrammaticScrolling, _lastDx, and _lastDy fields.
  • In OnScrollStateChanged: when ScrollStateSettling fires and the view is not being dragged, the scroll is marked as programmatic. Intermediate OnScrolled events cache dx/dy instead of processing immediately. When ScrollStateIdle is reached, the cached scroll data is processed once, ensuring CurrentItem is only updated after the animation completes.
  • For user-initiated (drag) scrolls, OnScrolled continues to be processed immediately as before.

Known limitation: If CurrentItem is changed programmatically twice in rapid succession before the first animation completes, only the latest _lastDx/_lastDy values are retained. The first programmatic scroll's final position may not be fully resolved. This is an edge case acknowledged during review.

Issues Fixed

Fixes #31874

Platforms Affected

  • Android – fix applies here only
  • iOS/MacCatalyst/Windows – no behavior change (fix is in Android-specific handler Items/Android/CarouselViewOnScrollListener.cs)

Validated the behaviour in the following platforms

  • Windows (test excluded via #if TEST_FAILS_ON_WINDOWS)
  • Android
  • iOS
  • Mac
Code Review: ✅ Passed

Code Review – PR #31875

🟡 Suggestions


1. Programmatic scroll detection can incorrectly fire for fast user drags

File: src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs
Line: ~35

if (state == RecyclerView.ScrollStateSettling && !_carouselView.IsDragging)
{
    _isProgrammaticScrolling = true;
}

Problem: When the user drags the carousel quickly and releases their finger:

  1. IsDragging becomes false (finger lifted)
  2. State transitions to ScrollStateSettling (inertia/snap still happening)
  3. _isProgrammaticScrolling is set to true

This means fast user drags also get treated as programmatic scrolls — OnScrolled is suppressed and CurrentItem only updates once, after the settling completes. This changes the existing behavior for user-initiated drags.

Author's response in review: This is intentional; it aligns Android behavior with iOS (which also only updates CurrentItem after the gesture settles). The author provided comparison videos showing the behaviors match.

Recommendation: If the behavioral alignment with iOS is intentional, document this in a code comment. It changes Android's existing user-drag behavior and reviewers/future developers need to understand it's deliberate:

// Note: this also covers the case where a user drag transitions to settling
// (IsDragging becomes false before ScrollStateSettling fires). This intentionally
// aligns Android behavior with iOS: CurrentItem is only updated once scrolling
// fully completes, not during the settling phase.
if (state == RecyclerView.ScrollStateSettling && !_carouselView.IsDragging)
{
    _isProgrammaticScrolling = true;
}

2. UI test timing: result may be checked before animation completes

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31874.cs
Lines: 20–24

App.WaitForElement("Issue31874ScrollBtn");
App.Tap("Issue31874ScrollBtn");

var resultLabel = App.WaitForElement("Issue31874ResultLabel").GetText();
Assert.That(resultLabel, Is.EqualTo("Success"));

Problem: After tapping the button, the scroll animation is asynchronous. WaitForElement only checks the element exists and has text — it doesn't wait for the animation to complete. The resultStatus label starts as "Success" and only changes to "Failure" if an intermediate CurrentItem change is observed while the animation is in flight. If the UI test reads the label before the animation has progressed far enough to fire intermediate events, it may always read "Success" regardless of whether the fix is in place.

Recommendation: Add a small wait or a more robust assertion approach. One option: check after a short deliberate pause to allow the animation to proceed, or check for a "settled" state indicator. At minimum, this should be noted as a known flakiness risk.


3. #if TEST_FAILS_ON_WINDOWS is incorrect for this Android-only issue

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31874.cs

#if TEST_FAILS_ON_WINDOWS // Issue Link - https://github.com/dotnet/maui/issues/31670

Problem: The test is wrapped in #if TEST_FAILS_ON_WINDOWS, which means it compiles and runs on Android, iOS, and MacCatalyst — but not Windows. The referenced issue (#31670) is unrelated to this bug. The actual issue (#31874) is Android-specific. The test running on iOS and MacCatalyst is harmless (since the bug doesn't exist there, it will always pass), but it's misleading and wastes CI time.

Recommendation: Either:

  • Add a platform guard comment explaining the scope: // Android-only issue; test runs on Android, iOS, Mac (passes on iOS/Mac because bug doesn't exist there)
  • Or reference the correct issue link in the #if comment

4. Known race condition: rapid successive programmatic scrolls

File: src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs
Lines: ~43–50

_lastDx = dx;
_lastDy = dy;

Problem: If CurrentItem is changed programmatically twice before the first animation completes (e.g., rapid successive updates), each OnScrolled call overwrites _lastDx/_lastDy. When ScrollStateIdle fires, only the last-cached delta is processed. The first scroll's final position is never explicitly finalized.

The author explored a queue-based approach but found it captured all incremental deltas rather than only item-transition deltas, which introduced different issues.

Current state: This is acknowledged as a known edge case in the review thread with no resolution yet.

Recommendation: Add a code comment documenting this limitation so future maintainers understand the tradeoff:

// Note: Only the most recent delta is retained. If CurrentItem is changed
// programmatically multiple times during a single animation, earlier intermediate
// positions are not separately finalized. This is an accepted limitation.
_lastDx = dx;
_lastDy = dy;

ℹ️ Nitpicks

  • Missing newline at end of file in both Issue31874.cs test files. Both end with \\ No newline at end of file. Minor but inconsistent with repo conventions.
  • Magic strings in HostApp test ("0" and "4" used inline in label text and string comparisons). The constants InitialItem and TargetItem are defined at the top of the class, but selectedItemLabel text is constructed inline rather than referencing them — a nitpick from the automated review thread.

✅ Looks Good

  • The core fix logic is simple and well-targeted: cache scroll data during animation, process once at idle.
  • CarouselViewLoopManager.CheckPendingScrollToEvents is correctly preserved for user-initiated scrolls.
  • The HostApp test page (Issue31874.cs) includes AutomationId attributes and a clear pass/fail label, following established patterns.
  • The [Category(UITestCategories.CarouselView)] attribute is correctly applied.
  • The PR is Android-specific and correctly touches only the Items/Android/ handler (not Items2/).

Copy link
Copy Markdown
Collaborator

@MauiBot MauiBot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #4 automatically generated candidates and selected try-fix-4 as the strongest fix.

Why: try-fix-4 fixes both the critical IsDragging detection bug (user flings were misclassified as programmatic, regressing normal swipe behavior) and the test platform guard that directly caused the gate failure (the test was running on iOS where it always passes trivially, instead of Android where the bug exists). It is the only candidate that simultaneously corrects the scroll listener logic and addresses the gate root cause.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-4`)
diff --git a/src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs b/src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs
--- a/src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs
+++ b/src/Controls/src/Core/Handlers/Items/Android/CarouselViewOnScrollListener.cs
@@ -7,8 +7,10 @@
 readonly CarouselView _carouselView;
 readonly CarouselViewLoopManager _carouselViewLoopManager;
 int _lastDx;
 int _lastDy;
 bool _isProgrammaticScrolling;
+// Carries the user-dragged signal into ScrollStateSettling since IsDragging is reset to false before that state fires.
+bool _wasDraggingBeforeSettle;
 
@@ -26,14 +28,23 @@
 if (_carouselView.IsSwipeEnabled)
 {
 if (state == RecyclerView.ScrollStateDragging)
+{
 _carouselView.SetIsDragging(true);
+_wasDraggingBeforeSettle = true;
+}
 else
 _carouselView.SetIsDragging(false);
 }
 
+if (state == RecyclerView.ScrollStateDragging)
+{
+_isProgrammaticScrolling = false;
+}
 {
 _isProgrammaticScrolling = true;
 }
 else if (state == RecyclerView.ScrollStateIdle)
 {
+_wasDraggingBeforeSettle = false;
 if (_isProgrammaticScrolling)
 {
 _isProgrammaticScrolling = false;
 OnScrolled(recyclerView, _lastDx, _lastDy);
 _lastDx = 0;
 _lastDy = 0;
 }
 }

diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31874.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31874.cs
--- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31874.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31874.cs
@@ -1,4 +1,4 @@
-#if TEST_FAILS_ON_WINDOWS // Issue Link - https://github.com/dotnet/maui/issues/31670
+#if TEST_FAILS_ON_WINDOWS && TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST // https://github.com/dotnet/maui/issues/31874

@MauiBot MauiBot added s/agent-review-incomplete and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels May 3, 2026
Copy link
Copy Markdown
Contributor

@kubaflo kubaflo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please review the AI's suggestions?

@SyedAbdulAzeemSF4852
Copy link
Copy Markdown
Contributor Author

Closing this PR since the issue has already been resolved on the main branch. PR #34570 already addressed the same CarouselView animated-scroll behavior (intermediate/incorrect updates during programmatic scrolling), so these changes are no longer needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration s/agent-fix-win AI found a better alternative fix than the PR s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] Incorrect Intermediate CurrentItem updates with CarouselView Scroll Animation Enabled

8 participants