Skip to content

Fix horizontal ScrollView resting past its left edge after a fling on Android 14 - #59020

Merged
meta-codesync[bot] merged 2 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-horizontal-fling-into-pulled-edge
Nov 13, 2026
Merged

meta-codesync[bot] merged 2 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-horizontal-fling-into-pulled-edge

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

On Android 14, a horizontal ScrollView resting at its left edge, dragged further left and released while moving, comes to rest 6 dp past that edge and stays there until it is touched again: the content shows a gap and the view reports a negative offset. In right-to-left layout the left edge is the reading end, so a list flung toward its end sticks the same way.

ReactHorizontalScrollView.fling() always starts its own overscroll fling (minX = 0, overX = width / 2), where the platform's HorizontalScrollView.fling() absorbs a fling into an edge effect that is still pulled. computeScroll() clamps the overscroll to mOverflingDistance and springs back, but Android 14's consumeFlingInStretch() feeds every spring-back step into the stretch the drag is still pulling, so the scroller finishes at 0 and the view stays at −6 dp. Android 15 adds the guard that prevents it (if (scrollX < 0 || scrollX > getScrollRange()) return unconsumed;), and Android 13's HorizontalScrollView has no consumeFlingInStretch.

From Android 12, where the platform's fling absorbs into a pulled edge, this hands a fling that heads into the left edge the view rests at to super.fling(), so the platform absorbs it into the edge effect as it does for a plain HorizontalScrollView. Every other fling, and every fling before Android 12, keeps React Native's own path. The vertical ScrollView is not affected: ScrollView.onTouchEvent releases through flingWithNestedDispatch, which absorbs a fling into an edge the view rests at and never calls fling().

Changelog:

[ANDROID] [FIXED] - Fix horizontal ScrollView resting past its left edge after a fling on Android 14

Test Plan:

  • New ReactHorizontalScrollViewFlingTest (Robolectric, sdk = 34) drags a horizontal ScrollView at its left edge further left and releases it. On main it rests at scrollX = -6 (expected:<[0]> but was:<[-6]>); with this change at 0. A second case flings into the content, so the drag is shown to produce a real fling. Robolectric's ShadowEdgeEffect turns edge effects off on API 31+, so the test supplies a shadow that keeps the stretch a device uses.
  • ReactHorizontalScrollViewFlingTest, ReactScrollViewTouchCancelTest and ReactScrollViewPropertyTest pass under ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest -PenableWarningsAsErrors=true (16 tests), and ktfmt, run with the flags scripts/format-kotlin.js passes it, leaves both files unchanged.
  • RNTester on an Android 14 (API 34) emulator: after the same fling, Item 0 rests 18 px right of where it sat without this change and where it sat with it — before/after.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 12, 2026
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — flung toward its left edge from rest, the strip stops 6 dp past that edge and stays there.

FIXED — the same fling leaves the strip at its edge.

before — main after — with this PR

RNTester's ScrollViewExample/horizontalScrollTo on an Android 14 (API 34) emulator, 1140×2400 at density 480, built from the same commit with and without this change. After a fast swipe to the right from rest, Item 0's left edge reads 48 px without the change and 30 px with it, where it sat at rest, at 3 s and again at 6 s.

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 12, 2026 23:10
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 12, 2026
}

if (pagingEnabled) {
if (scrollX <= 0 && correctedVelocityX < 0) {

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.

Should we guard this specifically to Android 14?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd rather guard on Android 12 than 14. From 12, HorizontalScrollView.fling() absorbs a fling into an edge the drag is still pulling, and the override skips that on every release since; 14 is just where skipping it strands the view, because its consumeFlingInStretch lacks the overscroll check 15 added. Below 12 there's no stretch to absorb into, so I've limited the branch to Build.VERSION_CODES.S and up in 9478215.

… Android 14

A horizontal ScrollView resting at its left edge, dragged further left and
released while moving, came to rest 6 dp past that edge on Android 14.
ReactHorizontalScrollView.fling() starts its own overscroll fling where the
platform absorbs a fling into the edge effect the drag is still pulling, and
Android 14's consumeFlingInStretch() feeds the spring-back into that stretch,
so the view never moves back.

Hand a fling into the left edge the view rests at to super.fling().
The platform's fling absorbs into an edge the drag is still pulling from Android 12, the release that introduced the stretch; below it there is no stretch to absorb into, so React Native's own fling stays.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/android-horizontal-fling-into-pulled-edge branch from 9478215 to c1abf3c Compare November 11, 2026 17:05
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

@javache your question on an Android 14 guard is answered in its thread: 9478215 limits the left-edge super.fling() to Android 12 and up.

@meta-codesync

meta-codesync Bot commented Nov 13, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D131751400.

@cortinico cortinico 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.

Review automatically exported from Phabricator review in Meta.

@meta-codesync
meta-codesync Bot merged commit 718b615 into react:main Nov 13, 2026
54 of 55 checks passed
@react-native-bot

Copy link
Copy Markdown
Collaborator

This pull request was successfully merged by @YevheniiKotyrlo in 718b615

When will my fix make it into a release? | How to file a pick request?

@react-native-bot react-native-bot added the Merged This PR has been merged. label Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants