Skip to content

Fix NestedScrollView fling, overscroll, and animateTo with unified offset model#182327

Open
pervanluka wants to merge 5 commits intoflutter:masterfrom
pervanluka:nested-scroll-unified-offset-fixes
Open

Fix NestedScrollView fling, overscroll, and animateTo with unified offset model#182327
pervanluka wants to merge 5 commits intoflutter:masterfrom
pervanluka:nested-scroll-unified-offset-fixes

Conversation

@pervanluka
Copy link

Summary

Fixes NestedScrollView's core scrolling issues by introducing a unified offset model that treats outer + inner as a single continuous scroll coordinate. This replaces the previous NestedBallisticScrollActivity (which ran independent simulations that drifted apart) with coordinated activities driven by a single simulation over the combined offset.

Key changes:

  • Added UnifiedNestedScrollActivity + UnifiedNestedBallisticScrollActivity that scroll outer and inner as one continuous surface using combined min/max/pixels
  • Made animateTo work across the outer→inner boundary seamlessly
  • Added BouncingScrollPhysics.decelerationRate (default 0.998) to control overscroll decay
  • Fixed ScrollDragController to apply physics clamping during drags, preventing over-reporting of delta

Issues Fixed

Fling momentum not continuing between outer/inner:

Overscroll bounce janky/too large:

animateTo broken across outer+inner:

Jump/jank at header-body transition:

Test plan

  • All existing nested_scroll_view_test.dart tests pass (updated expectations for unified behavior)
  • New tests for unified fling across outer→inner boundary
  • New tests for animateTo spanning outer+inner range
  • New tests for overscroll clamping with BouncingScrollPhysics
  • New tests for BouncingScrollPhysics.decelerationRate
  • scroll_activity_test.dart and scroll_physics_test.dart pass

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: scrolling Viewports, list views, slivers, etc. labels Feb 12, 2026
@google-cla
Copy link

google-cla bot commented Feb 12, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and welcome refactoring of NestedScrollView's scrolling logic by implementing a unified offset model. This change addresses numerous long-standing issues related to fling momentum, overscroll behavior, and animations between the outer and inner scroll views. The new approach, which treats the outer and inner scrollables as a single continuous surface, greatly simplifies the coordinator's logic, removing the complex and drift-prone independent ballistic activities. The changes to goBallistic and animateTo are much cleaner and more robust. Additionally, the introduction of RetargetableScrollActivity and ScrollDetails improves how pointer-based scrolling is handled. The new test suite is comprehensive and provides good coverage for the fixes. Overall, this is an excellent improvement for the framework. I have one minor suggestion to improve code conciseness.

@pervanluka pervanluka force-pushed the nested-scroll-unified-offset-fixes branch 5 times, most recently from 080e225 to 6049d4d Compare February 13, 2026 08:21
@pervanluka
Copy link
Author

Can someone check this? cc: @gaaclarke

@gaaclarke
Copy link
Member

It was a holiday weekend in the states, the framework team will get around to it.

@justinmc justinmc requested a review from Piinks February 17, 2026 23:21
Copy link
Contributor

@Piinks Piinks left a comment

Choose a reason for hiding this comment

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

Hi @pervanluka thanks for the ambitious PR! I will need to look through this a bit more carefully. I really appreciate you digging into this, it is a very complicated widget, and so far it appears to not be a breaking change and it passes all of the existing tests. 🥳
I left some initial comments from a first pass looking for any obvious issues, I'll provide a more detailed review in a bit.

///
/// Used by [RetargetableScrollActivity] and [ScrollPhysics.createScrollActivity]
/// to determine how to handle the scroll input.
sealed class ScrollDetails {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit wrt naming - this might be mistaken with the ScrollableDetails class.

@pervanluka pervanluka force-pushed the nested-scroll-unified-offset-fixes branch from d90355d to c398e19 Compare February 21, 2026 23:06
@pervanluka pervanluka requested a review from Piinks February 23, 2026 09:01
@pervanluka
Copy link
Author

pervanluka commented Feb 23, 2026

Just a note for you @Piinks, I did some research across the reported issues and found a design doc referenced in one of them. This PR implements the NestedScrollView-specific parts of the https://docs.google.com/document/d/136wxl8He3PF2UMOT51lPfeqKE2TWGQuQ1u1b-ZDgErM/edit?tab=t.0#heading=h.vuf4ow52ej6k design doc (#159146) authored by @kszczek , specifically:

  • Unified offset model for NestedScrollView (doc's "Unification approach" section) — treating outer+inner as a single continuous scroll surface with setPixels distributing to outer first, then inner
  • RetargetableScrollActivity — allowing running scroll activities to be retargeted with new deltas
  • ScrollInputSource (simplified from the doc's ScrollDetails sealed class per reviewer feedback — now an enum) — identifying the source of scroll input
  • ScrollPhysics.createScrollActivity — letting physics provide default scroll activities
  • Executing scroll activities on the NestedScrollView as a whole rather than on individual inner/outer positions, as outlined in the doc's NestedScrollView section

I've tested these changes locally and can confirm everything works as expected.

Copy link
Contributor

@Piinks Piinks left a comment

Choose a reason for hiding this comment

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

Thanks for the reference to the old design document.
I have some higher level questions, and in particular, this PR lists many issues it is looking to address. I would like to better understand what change is addressing which issue. If we are able to move forward with this, the best course of action will be to split this up into atomic changes.
Can you tell me which part of this change addresses each issue you listed?

From the PR description:

Fixed ScrollDragController to apply physics clamping during drags, preventing over-reporting of delta
Can you tell me where this change is made?

Comment on lines +952 to +953
if (_currentUnifiedActivity is RetargetableScrollActivity) {
(_currentUnifiedActivity! as RetargetableScrollActivity).retarget(
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure I follow this, when would this be true in the current implementation?

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the detailed review! Here's the issue-to-change mapping:

Unified offset model + goBallistic rewrite (the core change — _unifiedPixels, _unifiedMaxScrollExtent, setPixels distributing to outer first then inner, single BallisticScrollActivity on coordinator):

Overscroll clamping in setPixels + anyOutOfRange independent ballistic fallback in goBallistic:

Unified animateTo via single DrivenScrollActivity on coordinator:

Regarding ScrollDragController — that line in the PR description is incorrect, there are no changes to ScrollDragController in this PR. I'll update the description.

Regarding the RetargetableScrollActivity check in pointerScroll — you're right, this is never true in the current implementation. It's forward-looking infrastructure for the smooth scrolling feature from the design doc (#159146).
I can remove it to keep this PR focused and add it in a follow-up when that feature lands.

…fset model

Rewrites the NestedScrollView coordinator to use a single unified scroll
offset distributed across outer and inner positions, replacing the
previous independent-position model that caused discontinuities at
the outer/inner boundary.

Key changes:
- Unified offset model: setPixels distributes delta to outer first,
  then inner, ensuring continuous momentum across the boundary.
- BouncingScrollPhysics: overscroll is applied only to the active
  endpoint (top of outer or bottom of inner), eliminating the
  double-overscroll that caused extreme jank.
- animateTo: drives a single animation on the coordinator position,
  distributing each frame's delta across outer+inner so animations
  can cross the header/body boundary smoothly.
- Fling continuity: ballistic simulations now carry momentum from
  outer into inner and vice versa without velocity loss.
- ScrollActivity: adds NestedBallisticScrollActivity and
  NestedDrivenScrollActivity to support coordinator-driven motion.
- ScrollPhysics: adds toleranceFor() to expose Tolerance for a
  given ScrollMetrics without requiring a full simulation.

Fixes flutter#13496, flutter#136199, flutter#59440, flutter#73864, flutter#52233, flutter#33367, flutter#79062,
Replace sealed class hierarchy (ScrollDetails, PointerScrollDetails,
KeyboardScrollDetails, ProgrammaticScrollDetails) with a simple
ScrollInputSource enum per review feedback.

- Rename ScrollDetails → ScrollInputSource to avoid confusion with
  existing ScrollableDetails class
- Replace `is` type checks with enum values (Flutter style guide)
- Update all usages in scroll_activity.dart, scroll_physics.dart,
  nested_scroll_view.dart
- Update tests in scroll_activity_test.dart and scroll_physics_test.dart
@pervanluka pervanluka force-pushed the nested-scroll-unified-offset-fixes branch from c398e19 to e6b9130 Compare March 10, 2026 09:47
Remove ScrollInputSource enum, RetargetableScrollActivity, and
ScrollPhysics.createScrollActivity that are not exercised by any
current code path. Simplify pointerScroll to direct delta application.
@pervanluka pervanluka force-pushed the nested-scroll-unified-offset-fixes branch from e6b9130 to 1cae825 Compare March 10, 2026 09:49
@pervanluka pervanluka requested a review from Piinks March 17, 2026 14:06
Copy link
Contributor

@Piinks Piinks left a comment

Choose a reason for hiding this comment

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

Thanks for the continued work on this. I know some of this is based on a previous design document. That being said, this should really have an updated design document that details the (updated) implementation and how it resolves the issues listed. And for the issues that are already resolved, why they are not and need this change.

It is also likely that in order to land this, it may need to be split up. Typically we don't try to fix a bunch of issues in one PR. If there is a regression from one part of the PR, we would have to revert all of it rather than just the part that may create an issue.

The design document template is here: https://github.com/flutter/flutter/blob/master/docs/contributing/Design-Documents.md
I would be happy to circulate it with the team to collect feedback in order to make more progress here.

Fix regression where individual positions reported zero velocity during
flings and animations. _CoordinatedScrollActivity now delegates velocity
to the unified activity, restoring recommendDeferredLoading behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment