New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fling-scrolling is slow unless you swipe precisely vertically #120345
Comments
DiagnosisWhat's going on here is:
Those two different versions of clamping to a maximum velocity are: AndroidAndroid takes the velocity in each axis, x and y, and clamps each of them independently. From RecyclerView.java: velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
mViewFlinger.fling(velocityX, velocityY); A few lines before that, in fact, it looks at which axes the view can scroll in, and zeroes out the velocity in any axis that won't scroll: if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
velocityX = 0;
}
if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
velocityY = 0;
}
if (velocityX == 0 && velocityY == 0) {
return false;
} For the case of a view that scrolls in only one axis, this produces the desirable behavior seen above. FlutterThe current version of Flutter takes the velocity as a two-dimensional vector, and clamps that to a maximum length. From DragGestureRecognizer._checkEnd (if I've correctly traced this up the call graph): final VelocityEstimate? estimate = tracker.getVelocityEstimate();
if (estimate != null && isFlingGesture(estimate, tracker.kind)) {
final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
.clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
details = DragEndDetails(
velocity: velocity,
primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond),
); Here Then the call to In the case of something that can scroll in two dimensions, this logic makes more sense than the Android logic and probably produces more natural behavior — it means that the maximum speed is always the same 8000 px/sec, in every direction, whereas the Android logic would have it range from 8000 px/sec up to sqrt(2) times that, or about 11300 px/sec, when travelling at a 45-degree angle to the axes. But when the view is only scrolling in one axis, the resulting behavior is not physically natural — it's as if you had a physical object mounted on a linear rail, but when you pushed it hard along the rail, it started resisting you much harder and moving more slowly if you also pushed it somewhat to the side. You could build a physical system like that, but it means one with a lot of friction, which is not the metaphor one wants for a good scrolling experience. More concretely, this behavior produces the user perception of sluggishness described above. And it differs from Android's, which is a fidelity issue. Toward a solutionInstead, when we're in a I haven't yet looked into what a good way to do that in the code would look like. It may require a change to the API implemented by |
Hello @gnprice. Apologies if I do not understand the issue completely, but could you highlight what this issue adds that isn't already in the issues you mentioned? There's a lot of information you provide - thanks :) - but I'm having a tough time sifting through it. Also, the steps that you provided to reproduce this issue, I'm afraid that could be hindered by human error. Testing it, it looks like the scroll view sort of follows the finger. Flinging the scroll view, I am not sure if it's Flutter influencing the scroll speed or whether I was slower that time flinging it. I have also seen that sometimes, flinging at an angle scrolls further than flinging vertically, though not by much which is probably because of human error. Is there a consistent way we can test this behavior? Can, for example, tests help us here to reproduce a consistent scrolling behavior? This would help verify the behavior and provide a definite way to check if the issue is fixed in the future. |
Fixes flutter#120338. Fixes flutter#119875. Fixes flutter#113424. Fixes most of flutter#16371, as the combined symptom of those three. (There's still flutter#120345, and possibly other contributing factors, remaining.) This replaces the implementation of ClampingScrollSimulation with a new version that better matches the Android scroll physics it's intended to match. The new implementation is a version of the Android curve which has been adjusted so that it behaves in a physically reasonable way, in order to satisfy an invariant which the Flutter scroll protocol relies on. Because the old ClampingScrollSimulation didn't satisfy that invariant either, it suffered from a bug (flutter#120338) where restarting the simulation with new metrics would have the side effect of adding friction, making a fling go about 15% less far. So this fixes flutter#120338. The new implementation also goes the same total distance as the Android scroll physics, whereas the existing version would go about 7% less far (stacking with the 15% reduction when the other issue was triggered.) That fixes flutter#119875. Finally, the new implementation comes to a smooth stop, like Android does, whereas the existing version would speed slightly up before abruptly stopping. That fixes flutter#113424. The new implementation's curve is the unique curve that both goes the same total distance as Android's for every possible starting velocity, and meets the Flutter scroll protocol's ballistic invariant. So in that sense this is the canonical way of fixing up the Android behavior to something physically reasonable, or something that works correctly in Flutter.
Upon consulting with a colleague of mine, I've decided to label this issue. I've sat down and and reproduced this behavior over and over and I'd say around 80 - 90% of the time I can reproduce this behavior. The other 10 - 20% I'd consider error on my end. Hope to get further insights from the team on this issue :) |
Thanks!
Sure. The key part that's distinct about this issue, from a user-visible perspective, is that it specifically affects the scrolling when you do the swipe at an angle, and not when it's close to precisely vertical. This issue makes swiping rapidly at an angle produce a slower fling-scroll, no matter how fast you swipe, than you get when swiping rapidly straight upward. The resulting fling-scroll also goes less far before stopping. When you swipe straight up, this issue doesn't come into play. The scroll may still be slower than it should be, and slower than the native behavior, due to the other issues. When you swipe at an angle, the other issues are still present but this one slows the scroll down still more, and widens the gap from the native behavior.
Cool. Yeah, I find that trying to swipe at a particular angle is fairly imprecise for me too. But an 80-90% reproduction rate is definitely reliable enough for investigating an issue and manually testing a fix. And I definitely agree we'll want unit tests to accompany any fix. Fortunately those can be a lot more precise than a human finger on a screen, so they should be very reliable. |
Fixes flutter#120345. The main change here is that when a single-axis DragGestureRecognizer (say, a VerticalDragGestureRecognizer) sees the end of a drag, it now projects the drag's velocity onto its own single axis, and then clamps that. Previously it would clamp the velocity's magnitude as a 2D vector, and then consumers would project *that*, which resulted in clamping too hard when the motion wasn't precisely on-axis. For PanGestureRecognizer, the behavior is unchanged: we continue clamping the velocity as a 2D vector, which is the geometrically nice behavior when we're not trying to focus on a single axis. When I'd first looked at this code, I was concerned that fixing this issue might require a breaking change. But happily I believe it doesn't, for two reasons: * This base class DragGestureRecognizer is effectively sealed (even though Dart doesn't quite yet have that feature), because it has private abstract methods. Dart won't actually stop you extending it, but that seems like a bug in Dart (*); when the class's own method implementations go try to call those methods, they'll produce runtime errors. So it's not possible to subclass it in any useful way from outside the library, which means there are likely no such subclasses anywhere. That means we can freely alter the API between the base class and its subclasses. (*) In fact, it's in the Dart tracker: dart-lang/sdk#25462 * We're also changing the behavior of the `onEnd` callback: previously it would return a DragEndDetails with a two-dimensional velocity, even for a single-axis recognizer, and now it returns a velocity that's always zero in the cross axis. In principle that could be a breaking change; it's a bit odd for a VerticalDragGestureRecognizer to be telling you there was horizontal as well as vertical motion, but someone could nevertheless be depending on that. Fortunately, it turns out that the `onUpdate` callback's behavior already agrees with the view that that is an odd thing that shouldn't happen: it already returns an axis-locked velocity from single-axis recognizers. So the existing `onEnd` behavior is inconsistent with that; and whatever someone might try to do with cross-axis velocity from `onEnd`, it seems like it'd be hard for them to be doing it successfully already when `onUpdate` isn't going along. That makes me hopeful that nobody is depending on that behavior and we can freely clean it up. Also add tests, not only for the changes but for the existing behavior: it turns out that the fancy 2-D clamping, which we keep for PanGestureRecognizer because it's helpful there, wasn't tested at all.
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of |
When trying to scroll rapidly through a list, the fastest you can go is much slower if you swipe at an angle and not straight up or down: about 15%-30% less initial speed, and 20%-50% less total distance on a single fling, for a range of likely angles.
Swiping at an angle is the most natural motion for many ways of using a phone, especially when using the thumb to scroll. And Android native scrolling (*) does not share this behavior: swiping at an angle can reach the same top speeds as swiping precisely vertically. As a result, many users are accustomed to scrolling this way, and the way an app responds when they do is their primary experience of trying to scroll in the app.
((*) I mention Android because that's where I've tested. It's possible that iOS or other platforms share Flutter's behavior here, or Android's, or do something else.)
This is one of several issues that combine to produce user feedback that scrolling feels sluggish and unresponsive in the default scrolling behavior used on Android. The symptoms naturally blend together until you investigate closely, so here's the others I know of for cross-reference:
Steps to Reproduce
Run the
platform_tests/scroll_overlay
app on an Android device. (This may be the same on iOS and other platforms too, but I haven't yet tested that.)In
lib/main.dart
, delete the lineitemCount: 1000,
, in order to avoid Scroll protocol assumes ballistic scroll physics, and ClampingScrollPhysics isn't ballistic #120338. Reload.Scroll with a fling gesture: swipe rapidly upward and release. Wait a couple of seconds for the scroll to come to a stop.
Observe how far the scroll went, as shown by the numbered items in the Android- and Flutter-driven scrolling lists.
Scroll back to the top.
Repeat step 2, but this time swipe up at an angle. For example, swipe rapidly from about the lower-left corner of the screen to the upper-right corner.
Repeat step 3.
Expected results:
Swiping at top speed should cause the list to scroll equally far whether you do it straight vertically up (which can be natural when using the index finger) or up at an angle (which is natural when using the thumb, and in many postures with the index finger.)
Actual results:
The Flutter list scrolls much less far when swiping at an angle than it does when swiping straight up.
The Android list, on the other hand, scrolls the same distance in both cases, or nearly so. (If it doesn't, try again but swiping faster. There's a certain maximum fling distance no matter how hard you swipe, and it's the same whether straight up or at an angle. The total numbers may get slightly smaller at an angle, because they include the initial portion when your finger was still on the screen performing a drag, and that part is shorter when at an angle.)
Specifically:
This represents about a 19% reduction in the 30-degree case and a 56% reduction in the 45-degree case, compared to straight up. (By my calculations, using the fact that item N is 40+N logical pixels high.)
Logs
The text was updated successfully, but these errors were encountered: