Fix NestedScrollView fling, overscroll, and animateTo with unified offset model#182327
Fix NestedScrollView fling, overscroll, and animateTo with unified offset model#182327pervanluka wants to merge 5 commits intoflutter:masterfrom
Conversation
|
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. |
There was a problem hiding this comment.
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.
080e225 to
6049d4d
Compare
|
Can someone check this? cc: @gaaclarke |
|
It was a holiday weekend in the states, the framework team will get around to it. |
Piinks
left a comment
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Nit wrt naming - this might be mistaken with the ScrollableDetails class.
d90355d to
c398e19
Compare
|
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:
I've tested these changes locally and can confirm everything works as expected. |
Piinks
left a comment
There was a problem hiding this comment.
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?
| if (_currentUnifiedActivity is RetargetableScrollActivity) { | ||
| (_currentUnifiedActivity! as RetargetableScrollActivity).retarget( |
There was a problem hiding this comment.
I am not sure I follow this, when would this be true in the current implementation?
There was a problem hiding this comment.
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):
- iOS fling causes NestedScrollView inner and outer scrollables get out of sync #13496, NestedScrollView scrolls inner view without scrolling outer view #136199, NestedScrollView inertia scroll cause body scrollable widget scroll #59440, NestedScrollView instantaneous jump when body starts to scroll #84128 — fling momentum and scroll continuity across outer/inner boundary
Overscroll clamping in setPixels + anyOutOfRange independent ballistic fallback in goBallistic:
- NestedScrollView extreme scroll jank with BouncingScrollPhysics #73864, NestedScrollView jank when scrolling on iOS #52233, NestedScrollView SliverAppBar TabBarView listview combination physics:BouncingScrollPhysics Scroll abnormal #33367, NestedScrollView failed assertion
'extra >= 0.0': is not trueon short ballistic drags with BouncingScrollPhysics #79062, NestedScrollView scroll doesn't work as intended on iOS #129119 — BouncingScrollPhysics jank/overscroll issues shouldIgnorePointer handling in _beginUnifiedActivity: - When using nested scroll views, make it possible for outer scroller to over scroll #82150 — shouldIgnorePointer during bounce-back
Unified animateTo via single DrivenScrollActivity on coordinator:
- NestedScrollView does not scroll beyond header when scrolling with ScrollController.animateTo #93818 — animateTo across outer+inner boundary pointerScroll rewrite using unified offset:
- NestedScrollView jumps when using trackpad or mouse wheel to scroll #63948, NestedScrollView is NOT scrolling the header first and then body at all times [web] #63978 — trackpad/mouse wheel jumps
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
c398e19 to
e6b9130
Compare
Remove ScrollInputSource enum, RetargetableScrollActivity, and ScrollPhysics.createScrollActivity that are not exercised by any current code path. Simplify pointerScroll to direct delta application.
e6b9130 to
1cae825
Compare
Piinks
left a comment
There was a problem hiding this comment.
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.
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:
UnifiedNestedScrollActivity+UnifiedNestedBallisticScrollActivitythat scroll outer and inner as one continuous surface using combined min/max/pixelsanimateTowork across the outer→inner boundary seamlesslyBouncingScrollPhysics.decelerationRate(default 0.998) to control overscroll decayScrollDragControllerto apply physics clamping during drags, preventing over-reporting of deltaIssues Fixed
Fling momentum not continuing between outer/inner:
Overscroll bounce janky/too large:
'extra >= 0.0': is not trueon short ballistic drags with BouncingScrollPhysics #79062 — BouncingScrollPhysics overscroll on inneranimateTo broken across outer+inner:
Jump/jank at header-body transition:
Test plan
nested_scroll_view_test.darttests pass (updated expectations for unified behavior)animateTospanning outer+inner rangeBouncingScrollPhysics.decelerationRatescroll_activity_test.dartandscroll_physics_test.dartpass