Skip to content

fix(android): report settling until a swiped pager comes to rest - #1201

Open
mozzius wants to merge 1 commit into
callstack:mainfrom
mozzius:fix/android-settling-scroll-state
Open

mozzius wants to merge 1 commit into
callstack:mainfrom
mozzius:fix/android-settling-scroll-state

Conversation

@mozzius

@mozzius mozzius commented Oct 26, 2026

Copy link
Copy Markdown

Summary

Fixes #1199.

On Android, a swipe reports onPageScrollStateChanged idle as soon as the finger lifts, while the page is still snapping. It never reports settling. onPageScroll keeps firing for another ~150–300 ms after idle. Anything that treats idle as "the pager has stopped" acts too early.

In Bluesky, the feed tab bar stops following the pager at idle. With 9.x it froze wherever the finger was lifted (videos in bluesky-social/social-app#12195).

What happened

  • ComposePagerView decided between settling and idle on DragInteraction.Stop, using pagerState.isScrollInProgress.
  • Compose ends the drag's scroll {} before the fling that snaps the pager starts its own. DragGestureNode returns from drag {}, emits DragInteraction.Stop, and only then does ScrollableNode.onDragStopped launch the fling in a new coroutine. So isScrollInProgress is always false when the Stop is collected, and every swipe reports idle.
  • When the fling ends, the isScrollInProgress collector sends idle again, but it's dropped as a duplicate.
  • The same collector can also land in that gap, and send onPageScroll(settledPage, 0) and idle mid-gesture.
  • This is the same in Compose foundation 1.7.x (the library's default) and 1.12.

Fix

  • DragInteraction.Stop and Cancel report settling. A drag always ends in a fling that snaps to a page, even when released at rest.
  • When isScrollInProgress turns false, the collector waits one frame. It sends the final onPageScroll(settledPage, 0) and idle only if nothing is scrolling by then, so the gap between drag and fling no longer counts as coming to rest.
  • Swipes now report dragging → settling → idle, like 8.x did. setPage still reports settling → idle, with idle one frame later than before.
  • A drag that starts during the snap reports settling → dragging.
  • onPageSelected is unchanged.

Test Plan

What's required for testing (prerequisites)?

Android emulator or device.

What are the steps to reproduce (after prerequisites)?

This adds Issue #1199 Scroll State Repro to the example app, and e2e/tests/issues/issue-1199-scroll-state.e2e.ts. The status line lists the states since the previous idle, and whether the last onPageScroll offset was 0 when idle arrived.

  1. Tap setPage(1): settling > idle | idle at rest: yes. This is a control and is the same before and after.
  2. Swipe to page 1. Before: dragging > idle | idle at rest: no. After: dragging > settling > idle | idle at rest: yes.

Verified

  • typecheck, format:check, lint (no new warnings) and the e2e typecheck pass.
  • On main, the swipe test fails with dragging > idle | idle at rest: no and the control passes. With the fix, both pass. Pixel 9 emulator, Android 16 (API 36), arm64, release build.
  • Full Android e2e suite with the fix: 28/28 tests pass.
  • In Bluesky (RN 0.88, Compose foundation 1.12.1, this change carried as a patch), on a Pixel 7a and a Pixel 8 emulator (API 34). Gestures: slow swipes, quick flicks, short drags that snap back, rapid swipes started mid-snap, and setPage.
    • Every swipe reported dragging → settling → idle, and nothing was left in settling.
    • No onPageScroll with a fractional offset arrived after idle.
    • On main, settling never appeared once in dozens of swipes.

iOS is untouched.

Compatibility

OS Implemented
iOS n/a
Android ✅

Checklist

  • I have tested this on a device and a simulator

🤖 Generated with Claude Code

Compose ends a drag's scroll before the snap fling starts its own, so the
pager saw isScrollInProgress false on DragInteraction.Stop and reported idle
as the finger lifted. Report settling on every drag stop, and idle only once
nothing is scrolling a frame later.

Fixes callstack#1199

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 26, 2026 11:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The frame-dependent state transitions warrant final human validation of cancellation and interrupted-gesture behavior.

No open issues 🎉

What changed in this PR

Fixes #1199 by adjusting Android pager scroll-state reporting so a swipe remains settling until the page stops, rather than reporting idle when the finger lifts. iOS is unchanged.

Changes:

  • Report settling when a drag ends and defer the final rest check by one frame.
  • Add an example screen displaying state transitions and the scroll offset at idle.
  • Add Android regression tests for swipe and programmatic setPage transitions.
File Description
example/​src/​gh-issues/​Issue1199ScrollStateRepro.tsx Adds an interactive scroll-state reproduction screen.
example/​src/​App.tsx Registers the new reproduction screen.
e2e/​tests/​issues/​issue-1199-scroll-state.e2e.ts Tests swipe and setPage state sequences.
android/​src/​main/​java/​com/​reactnativepagerview/​ComposePagerView.kt Adjusts drag-end reporting and delays idle detection.

Review effort: Balanced

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] v9: a swipe reports idle when the finger lifts, before the page settles (settling is never sent)

2 participants