Unverified Commit 60a8b333 authored by YeungKC's avatar YeungKC Committed by GitHub

fix mouse wheel scroll miscontrol of ScrollPosition. (#66039)

parent 8291f481
......@@ -1072,6 +1072,63 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
goBallistic(0.0);
}
void pointerScroll(double delta) {
assert(delta != 0.0);
goIdle();
updateUserScrollDirection(
delta < 0.0 ? ScrollDirection.forward : ScrollDirection.reverse
);
if (_innerPositions.isEmpty) {
// Does not enter overscroll.
_outerPosition!.applyClampedPointerSignalUpdate(delta);
} else if (delta > 0.0) {
// Dragging "up" - delta is positive
// Prioritize getting rid of any inner overscroll, and then the outer
// view, so that the app bar will scroll out of the way asap.
double outerDelta = delta;
for (final _NestedScrollPosition position in _innerPositions) {
if (position.pixels < 0.0) { // This inner position is in overscroll.
final double potentialOuterDelta = position.applyClampedPointerSignalUpdate(delta);
// In case there are multiple positions in varying states of
// overscroll, the first to 'reach' the outer view above takes
// precedence.
outerDelta = math.max(outerDelta, potentialOuterDelta);
}
}
if (outerDelta != 0.0) {
final double innerDelta = _outerPosition!.applyClampedPointerSignalUpdate(
outerDelta
);
if (innerDelta != 0.0) {
for (final _NestedScrollPosition position in _innerPositions)
position.applyClampedPointerSignalUpdate(innerDelta);
}
}
} else {
// Dragging "down" - delta is negative
double innerDelta = delta;
// Apply delta to the outer header first if it is configured to float.
if (_floatHeaderSlivers)
innerDelta = _outerPosition!.applyClampedPointerSignalUpdate(delta);
if (innerDelta != 0.0) {
// Apply the innerDelta, if we have not floated in the outer scrollable,
// any leftover delta after this will be passed on to the outer
// scrollable by the outerDelta.
double outerDelta = 0.0; // it will go negative if it changes
for (final _NestedScrollPosition position in _innerPositions) {
final double overscroll = position.applyClampedPointerSignalUpdate(innerDelta);
outerDelta = math.min(outerDelta, overscroll);
}
if (outerDelta != 0.0)
_outerPosition!.applyClampedPointerSignalUpdate(outerDelta);
}
}
goBallistic(0.0);
}
@override
double setPixels(double newPixels) {
assert(false);
......@@ -1386,6 +1443,32 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
return 0.0;
}
// Returns the amount of delta that was not used.
//
// Negative delta represents a forward ScrollDirection, while the positive
// would be a reverse ScrollDirection.
//
// The method doesn't take into account the effects of [ScrollPhysics].
double applyClampedPointerSignalUpdate(double delta) {
assert(delta != 0.0);
final double min = delta > 0.0
? -double.infinity
: math.min(minScrollExtent, pixels);
// The logic for max is equivalent but on the other side.
final double max = delta < 0.0
? double.infinity
: math.max(maxScrollExtent, pixels);
final double newPixels = (pixels + delta).clamp(min, max);
final double clampedDelta = newPixels - pixels;
if (clampedDelta == 0.0)
return delta;
forcePixels(newPixels);
didUpdateScrollPositionBy(clampedDelta);
return delta - clampedDelta;
}
@override
ScrollDirection get userScrollDirection => coordinator.userScrollDirection;
......@@ -1475,6 +1558,12 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
return coordinator.jumpTo(coordinator.unnestOffset(value, this));
}
@override
void pointerScroll(double delta) {
return coordinator.pointerScroll(delta);
}
@override
void jumpToWithoutSettling(double value) {
assert(false);
......
......@@ -761,6 +761,22 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
@override
void jumpTo(double value);
/// Changes the scrolling position based on a pointer signal from current
/// value to delta without animation and without checking if new value is in
/// range, taking min/max scroll extent into account.
///
/// Any active animation is canceled. If the user is currently scrolling, that
/// action is canceled.
///
/// This method dispatches the start/update/end sequence of scrolling
/// notifications.
///
/// This method is very similar to [jumpTo], but [pointerScroll] will
/// update the [ScrollDirection].
///
// TODO(YeungKC): Support trackpad scroll, https://github.com/flutter/flutter/issues/23604.
void pointerScroll(double delta);
/// Calls [jumpTo] if duration is null or [Duration.zero], otherwise
/// [animateTo] is called.
///
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
......@@ -202,6 +204,27 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc
goBallistic(0.0);
}
@override
void pointerScroll(double delta) {
assert(delta != 0.0);
final double targetPixels =
math.min(math.max(pixels + delta, minScrollExtent), maxScrollExtent);
if (targetPixels != pixels) {
goIdle();
updateUserScrollDirection(
-delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse
);
final double oldPixels = pixels;
forcePixels(targetPixels);
didStartScroll();
didUpdateScrollPositionBy(pixels - oldPixels);
didEndScroll();
goBallistic(0.0);
}
}
@Deprecated('This will lead to bugs.') // ignore: flutter_deprecation_syntax, https://github.com/flutter/flutter/issues/44609
@override
void jumpToWithoutSettling(double value) {
......
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/gestures.dart';
......@@ -624,9 +623,9 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
// SCROLL WHEEL
// Returns the offset that should result from applying [event] to the current
// position, taking min/max scroll extent into account.
double _targetScrollOffsetForPointerScroll(PointerScrollEvent event) {
// Returns the delta that should result from applying [event] with axis and
// direction taken into account.
double _targetScrollDeltaForPointerScroll(PointerScrollEvent event) {
double delta = widget.axis == Axis.horizontal
? event.scrollDelta.dx
: event.scrollDelta.dy;
......@@ -635,15 +634,14 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
delta *= -1;
}
return math.min(math.max(position.pixels + delta, position.minScrollExtent),
position.maxScrollExtent);
return delta;
}
void _receivedPointerSignal(PointerSignalEvent event) {
if (event is PointerScrollEvent && _position != null) {
final double targetScrollOffset = _targetScrollOffsetForPointerScroll(event);
final double targetScrollOffset = _targetScrollDeltaForPointerScroll(event);
// Only express interest in the event if it would actually result in a scroll.
if (targetScrollOffset != position.pixels) {
if (targetScrollOffset != 0) {
GestureBinding.instance!.pointerSignalResolver.register(event, _handlePointerScroll);
}
}
......@@ -654,9 +652,9 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
if (_physics != null && !_physics!.shouldAcceptUserOffset(position)) {
return;
}
final double targetScrollOffset = _targetScrollOffsetForPointerScroll(event as PointerScrollEvent);
if (targetScrollOffset != position.pixels) {
position.jumpTo(targetScrollOffset);
final double targetScrollOffset = _targetScrollDeltaForPointerScroll(event as PointerScrollEvent);
if (targetScrollOffset != 0) {
position.pointerScroll(targetScrollOffset);
}
}
......
......@@ -323,6 +323,39 @@ void main() {
expect(getScrollOffset(tester), 0.0);
});
testWidgets('Holding scroll and Scroll pointer signal will update ScrollDirection.forward / ScrollDirection.reverse', (WidgetTester tester) async {
ScrollDirection? lastUserScrollingDirection;
final ScrollController controller = ScrollController();
await pumpTest(tester, TargetPlatform.fuchsia, controller: controller);
controller.addListener(() {
if(controller.position.userScrollDirection != ScrollDirection.idle)
lastUserScrollingDirection = controller.position.userScrollDirection;
});
await tester.drag(find.byType(Viewport), const Offset(0.0, -20.0), touchSlopY: 0.0);
expect(lastUserScrollingDirection, ScrollDirection.reverse);
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
// Create a hover event so that |testPointer| has a location when generating the scroll.
testPointer.hover(scrollEventLocation);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
expect(lastUserScrollingDirection, ScrollDirection.reverse);
await tester.drag(find.byType(Viewport), const Offset(0.0, 20.0), touchSlopY: 0.0);
expect(lastUserScrollingDirection, ScrollDirection.forward);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -20.0)));
expect(lastUserScrollingDirection, ScrollDirection.forward);
});
testWidgets('Scrolls in correct direction when scroll axis is reversed', (WidgetTester tester) async {
await pumpTest(tester, TargetPlatform.fuchsia, reverse: true);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment