Skip to content

Scroll protocol assumes ballistic scroll physics, and ClampingScrollPhysics isn't ballistic #120338

@gnprice

Description

@gnprice

Scrolling in a Flutter list on Android and other platforms often feels much slower and less responsive than it should be. This issue is a major reason why: it can make a fling go about 15% less far than it should. This issue has also gotten in the way of previous attempts to fix some other reasons why.

Background

The framework's approach to scrolling assumes that each ScrollPhysics implementation is restartable, or ballistic. (*) The simulation is frequently restarted, and the ScrollPhysics is responsible for not letting that cause problems.

This is a lot like how the framework may frequently rebuild widgets, and if that affects behavior then that's a bug in the widget.

In the case of scrolling, this assumption is very natural because it will automatically be satisfied by any scroll physics so long as it obeys a reasonable ("ballistic") physical metaphor:

  • The thing that's scrolling is a physical object in motion.
  • Its velocity changes due to physical forces on it, according to Newton's second law.
  • Those forces depend only on (a) its current velocity and (b) where it is in its environment.

This metaphor works great for a wide variety of springs, frictional forces, clamping barriers, and other forms of scrolling physics one could dream up. Modulo small bugs (#109675, and another which I'll file shortly #120340 which affects macOS scrolling), it works great for all of the framework's scroll physics…

((*) This assumption doesn't seem to be clearly documented; I'll file a separate issue for that (→#120341), and see about writing a docs PR for it. Fundamentally it stems from the fact that ScrollPhysics.createBallisticAnimation takes only (a) a velocity and (b) a ScrollMetrics, where the latter encodes the current position plus some facts like ScrollMetrics.maxScrollExtent that describe the physical environment.)

Problem

… all, that is, except one. The trouble is that one scroll physics, ClampingScrollPhysics, breaks this assumption.

(That's the scroll physics we use by default on Android and all other non-Apple platforms.)

If you restart it — specifically if you restart it in the middle of a fling, where it uses ClampingScrollSimulation — then it slows down more.

The effect is that scrolling is often much slower and less responsive-feeling than it should be. In the repro below, a fling goes about 20% less far than it does for a native Android list. Part of that is due to #119875, but most of it (a 15% reduction) is due to this issue.

Related issues

Steps to Reproduce

  1. Run the platform_tests/scroll_overlay app on an Android device.

  2. Scroll with a fling gesture: swipe rapidly upward and release. Wait a couple of seconds for the scroll to come to a stop.

  3. Observe how far the scroll went, as shown by the numbered item "Flutter n" in the Flutter-driven scrolling list.

  4. Edit lib/main.dart to delete the line itemCount: 1000,. Reload, and scroll back to the top.

  5. Repeat steps 2 and 3.

Expected results:

The list should go the same distance, reaching the same numbered item, regardless of whether the list is infinite or merely practically infinite. (With 1000 items, we never get close to the end of the list.)

Actual results:

The list goes much less far with itemCount: 1000 present than it does without it.

Specifically, when I try this on the current version of scroll_overlay (at flutter/platform_tests@1d797b8), I see "Flutter 81" at the top of the screen when the fling is complete. With itemCount: 1000, removed, I see "Flutter 90".

(Ideally the list should also go the same distance that the Android list does: I see "Android 94" at the top of the screen. With itemCount removed, the remaining discrepancy is #119875.)

This means the total distance travelled with itemCount present is about 15% less than without it, and about 20% less than the Android list. (Just dividing (90-81)/90 would give too low an estimate, because the later items are bigger: item N is 40+N logical pixels tall. The 15% and 20% are my calculations using those actual heights.)

How the repro works:

This repro uses the fact that passing a non-null itemCount to the ListView constructor causes frequent restarts of the scroll's ballistic simulation, #11599, and an itemCount of null suppresses them. That isolates the effect of this issue from #119875 and others.

Details of why `itemCount` affects the repro

When itemCount is non-null, the underlying SliverList will estimate a max scroll offset based on the children it's laid out, so this estimate constantly changes and each ScrollPosition.applyContentDimensions call causes an applyNewDimensions call, which leads to goBallistic and restarting the simulation. With a null itemCount, the estimate is always infinite (from SliverMultiBoxAdaptorElement.estimateMaxScrollOffset), so it never changes and applyNewDimensions isn't called.

Logs
$ flutter analyze
Analyzing scroll_overlay...                                             
No issues found! (ran in 0.8s)
$ flutter doctor -v
[✓] Flutter (Channel main, 3.8.0-8.0.pre.13, on Debian GNU/Linux 10 (buster) 4.19.0-23-amd64, locale en_US.UTF-8)
    • Flutter version 3.8.0-8.0.pre.13 on channel main at /home/greg/n/flutter/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 2303f42250 (3 hours ago), 2023-02-08 14:24:12 -0500
    • Engine revision cc4ca6a06a
    • Dart version 3.0.0 (build 3.0.0-215.0.dev)
    • DevTools version 2.21.1

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
    • Android SDK at /home/greg/Android/Sdk
    • Platform android-33, build-tools 33.0.1
    • Java binary at: /home/greg/lib/android-studio-2020.3.1.24/jbr/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)
    • All Android licenses accepted.

[✓] Chrome - develop for the web
    • Chrome at google-chrome

[✓] Linux toolchain - develop for Linux desktop
    • clang version 7.0.1-8+deb10u2 (tags/RELEASE_701/final)
    • cmake version 3.13.4
    • ninja version 1.8.2
    • pkg-config version 0.29

[✓] Android Studio (version 2022.1)
    • Android Studio at /home/greg/lib/android-studio-2020.3.1.24
    • Flutter plugin version 71.2.4
    • Dart plugin version 221.6096
    • Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)

[✓] Android Studio (version 2021.2)
    • Android Studio at /opt/android-studio
    • Flutter plugin version 71.1.2
    • Dart plugin version 212.5744
    • Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)

[✓] VS Code (version 1.75.0)
    • VS Code at /usr/share/code
    • Flutter extension can be installed from:
      🔨 https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter

[✓] Connected device (3 available)
    • Pixel 5 (mobile) • 08171FDD40025F • android-arm64  • Android 13 (API 33)
    • Linux (desktop)  • linux          • linux-x64      • Debian GNU/Linux 10 (buster) 4.19.0-23-amd64
    • Chrome (web)     • chrome         • web-javascript • Google Chrome 109.0.5414.119

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

Android's scroll physics isn't ballistic either

When PR #77497 was merged in order to switch ClampingScrollSimulation to use the native Android physics (which would fix #113424 and #119875 and thereby some of the causes of #16371), it had to be reverted because of a variation on this issue. That was:

The root cause of those is that it turns out that the native Android scroll physics is also not restartable/ballistic, but in the opposite direction. Restarting the current ClampingScrollSimulation makes it slow down more, but restarting the Android scroll physics makes it keep going more, i.e. makes it slow down less.

Specifically, the Android physics starts at a deceleration of zero, and gradually decelerates more later. (See the curve at #119875 (comment) , the blue Android curve in the lower-left graph showing velocity, and note that the velocity at the beginning is flat.) So if you restart it, then it returns to not decelerating — which, when it happens again and again, produces the extremely long gliding behavior seen in #83632.

(That issue title says the scroll "regains velocity", but I think that is an illusion. My eyes see that same illusion too; but when I repro the issue and log the velocity, it always gets slower, not faster. It's just that it gets slower very slowly.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listf: scrollingViewports, list views, slivers, etc.found in release: 3.7Found to occur in 3.7found in release: 3.8Found to occur in 3.8frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-androidAndroid applications specificallyr: fixedIssue is closed as already fixed in a newer version

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions