diff --git a/packages/flutter/lib/src/gestures/converter.dart b/packages/flutter/lib/src/gestures/converter.dart index c871b40e516..d6d4c10a70a 100644 --- a/packages/flutter/lib/src/gestures/converter.dart +++ b/packages/flutter/lib/src/gestures/converter.dart @@ -259,9 +259,15 @@ class PointerEventConverter { scrollDelta: scrollDelta, embedderId: datum.embedderId, ); + case ui.PointerSignalKind.scrollInertiaCancel: + return PointerScrollInertiaCancelEvent( + timeStamp: timeStamp, + kind: kind, + device: datum.device, + position: position, + embedderId: datum.embedderId, + ); case ui.PointerSignalKind.unknown: - default: // ignore: no_default_cases, to allow adding a new [PointerSignalKind] - // TODO(moffatman): Remove after landing https://github.com/flutter/engine/pull/34402 // This branch should already have 'unknown' filtered out, but // we don't want to return anything or miss if someone adds a new // enumeration to PointerSignalKind. diff --git a/packages/flutter/lib/src/gestures/events.dart b/packages/flutter/lib/src/gestures/events.dart index 9dc764ae7b7..0c9f9bbaa10 100644 --- a/packages/flutter/lib/src/gestures/events.dart +++ b/packages/flutter/lib/src/gestures/events.dart @@ -1830,6 +1830,91 @@ class _TransformedPointerScrollEvent extends _TransformedPointerEvent with _Copy } } +mixin _CopyPointerScrollInertiaCancelEvent on PointerEvent { + @override + PointerScrollInertiaCancelEvent copyWith({ + Duration? timeStamp, + int? pointer, + PointerDeviceKind? kind, + int? device, + Offset? position, + Offset? delta, + int? buttons, + bool? obscured, + double? pressure, + double? pressureMin, + double? pressureMax, + double? distance, + double? distanceMax, + double? size, + double? radiusMajor, + double? radiusMinor, + double? radiusMin, + double? radiusMax, + double? orientation, + double? tilt, + bool? synthesized, + int? embedderId, + }) { + return PointerScrollInertiaCancelEvent( + timeStamp: timeStamp ?? this.timeStamp, + kind: kind ?? this.kind, + device: device ?? this.device, + position: position ?? this.position, + embedderId: embedderId ?? this.embedderId, + ).transformed(transform); + } +} + +/// The pointer issued a scroll-inertia cancel event. +/// +/// Touching the trackpad immediately after a scroll is an example of an event +/// that would create a [PointerScrollInertiaCancelEvent]. +/// +/// See also: +/// +/// * [Listener.onPointerSignal], which allows callers to be notified of these +/// events in a widget tree. +/// * [PointerSignalResolver], which provides an opt-in mechanism whereby +/// participating agents may disambiguate an event's target. +class PointerScrollInertiaCancelEvent extends PointerSignalEvent with _PointerEventDescription, _CopyPointerScrollInertiaCancelEvent { + /// Creates a pointer scroll-inertia cancel event. + /// + /// All of the arguments must be non-null. + const PointerScrollInertiaCancelEvent({ + super.timeStamp, + super.kind, + super.device, + super.position, + super.embedderId, + }) : assert(timeStamp != null), + assert(kind != null), + assert(device != null), + assert(position != null); + + @override + PointerScrollInertiaCancelEvent transformed(Matrix4? transform) { + if (transform == null || transform == this.transform) { + return this; + } + return _TransformedPointerScrollInertiaCancelEvent(original as PointerScrollInertiaCancelEvent? ?? this, transform); + } +} + +class _TransformedPointerScrollInertiaCancelEvent extends _TransformedPointerEvent with _CopyPointerScrollInertiaCancelEvent implements PointerScrollInertiaCancelEvent { + _TransformedPointerScrollInertiaCancelEvent(this.original, this.transform) + : assert(original != null), assert(transform != null); + + @override + final PointerScrollInertiaCancelEvent original; + + @override + final Matrix4 transform; + + @override + PointerScrollInertiaCancelEvent transformed(Matrix4? transform) => original.transformed(transform); +} + mixin _CopyPointerPanZoomStartEvent on PointerEvent { @override PointerPanZoomStartEvent copyWith({ diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 1cc4cf3f0df..2a65b2e498e 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -731,6 +731,9 @@ class ScrollableState extends State with TickerProviderStateMixin, R if (delta != 0.0 && targetScrollOffset != position.pixels) { GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll); } + } else if (event is PointerScrollInertiaCancelEvent) { + position.jumpTo(position.pixels); + // Don't use the pointer signal resolver, all hit-tested scrollables should stop. } } diff --git a/packages/flutter/test/widgets/scrollable_test.dart b/packages/flutter/test/widgets/scrollable_test.dart index 1ab8c2cc98c..6cdea4f8817 100644 --- a/packages/flutter/test/widgets/scrollable_test.dart +++ b/packages/flutter/test/widgets/scrollable_test.dart @@ -1427,6 +1427,22 @@ void main() { expect(syntheticScrollableNode!.hasFlag(ui.SemanticsFlag.hasImplicitScrolling), isTrue); handle.dispose(); }); + + testWidgets('Scroll inertia cancel event', (WidgetTester tester) async { + await pumpTest(tester, null); + await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0); + expect(getScrollOffset(tester), dragOffset); + await tester.pump(); // trigger fling + expect(getScrollOffset(tester), dragOffset); + await tester.pump(const Duration(milliseconds: 200)); + final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse); + await tester.sendEventToBinding(testPointer.hover(tester.getCenter(find.byType(Scrollable)))); + await tester.sendEventToBinding(testPointer.scrollInertiaCancel()); // Cancel partway through. + await tester.pump(); + expect(getScrollOffset(tester), closeTo(333.2944, 0.0001)); + await tester.pump(const Duration(milliseconds: 4800)); + expect(getScrollOffset(tester), closeTo(333.2944, 0.0001)); + }); } // ignore: must_be_immutable diff --git a/packages/flutter_test/lib/src/test_pointer.dart b/packages/flutter_test/lib/src/test_pointer.dart index efe36e20a8d..cbfbe860162 100644 --- a/packages/flutter_test/lib/src/test_pointer.dart +++ b/packages/flutter_test/lib/src/test_pointer.dart @@ -304,6 +304,23 @@ class TestPointer { ); } + /// Create a [PointerScrollInertiaCancelEvent] (e.g., user resting their finger on the trackpad). + /// + /// By default, the time stamp on the event is [Duration.zero]. You can give a + /// specific time stamp by passing the `timeStamp` argument. + PointerScrollInertiaCancelEvent scrollInertiaCancel({ + Duration timeStamp = Duration.zero, + }) { + assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate pointer signal events"); + assert(location != null); + return PointerScrollInertiaCancelEvent( + timeStamp: timeStamp, + kind: kind, + device: _device, + position: location! + ); + } + /// Create a [PointerPanZoomStartEvent] (e.g., trackpad scroll; not scroll wheel /// or finger-drag scroll) with the given delta. ///