Skip to content

Commit 22f51c3

Browse files
authored
Scroll inertia cancel [framework] (#106891)
1 parent bf22b7a commit 22f51c3

File tree

5 files changed

+129
-2
lines changed

5 files changed

+129
-2
lines changed

packages/flutter/lib/src/gestures/converter.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,15 @@ class PointerEventConverter {
259259
scrollDelta: scrollDelta,
260260
embedderId: datum.embedderId,
261261
);
262+
case ui.PointerSignalKind.scrollInertiaCancel:
263+
return PointerScrollInertiaCancelEvent(
264+
timeStamp: timeStamp,
265+
kind: kind,
266+
device: datum.device,
267+
position: position,
268+
embedderId: datum.embedderId,
269+
);
262270
case ui.PointerSignalKind.unknown:
263-
default: // ignore: no_default_cases, to allow adding a new [PointerSignalKind]
264-
// TODO(moffatman): Remove after landing https://github.com/flutter/engine/pull/34402
265271
// This branch should already have 'unknown' filtered out, but
266272
// we don't want to return anything or miss if someone adds a new
267273
// enumeration to PointerSignalKind.

packages/flutter/lib/src/gestures/events.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1830,6 +1830,91 @@ class _TransformedPointerScrollEvent extends _TransformedPointerEvent with _Copy
18301830
}
18311831
}
18321832

1833+
mixin _CopyPointerScrollInertiaCancelEvent on PointerEvent {
1834+
@override
1835+
PointerScrollInertiaCancelEvent copyWith({
1836+
Duration? timeStamp,
1837+
int? pointer,
1838+
PointerDeviceKind? kind,
1839+
int? device,
1840+
Offset? position,
1841+
Offset? delta,
1842+
int? buttons,
1843+
bool? obscured,
1844+
double? pressure,
1845+
double? pressureMin,
1846+
double? pressureMax,
1847+
double? distance,
1848+
double? distanceMax,
1849+
double? size,
1850+
double? radiusMajor,
1851+
double? radiusMinor,
1852+
double? radiusMin,
1853+
double? radiusMax,
1854+
double? orientation,
1855+
double? tilt,
1856+
bool? synthesized,
1857+
int? embedderId,
1858+
}) {
1859+
return PointerScrollInertiaCancelEvent(
1860+
timeStamp: timeStamp ?? this.timeStamp,
1861+
kind: kind ?? this.kind,
1862+
device: device ?? this.device,
1863+
position: position ?? this.position,
1864+
embedderId: embedderId ?? this.embedderId,
1865+
).transformed(transform);
1866+
}
1867+
}
1868+
1869+
/// The pointer issued a scroll-inertia cancel event.
1870+
///
1871+
/// Touching the trackpad immediately after a scroll is an example of an event
1872+
/// that would create a [PointerScrollInertiaCancelEvent].
1873+
///
1874+
/// See also:
1875+
///
1876+
/// * [Listener.onPointerSignal], which allows callers to be notified of these
1877+
/// events in a widget tree.
1878+
/// * [PointerSignalResolver], which provides an opt-in mechanism whereby
1879+
/// participating agents may disambiguate an event's target.
1880+
class PointerScrollInertiaCancelEvent extends PointerSignalEvent with _PointerEventDescription, _CopyPointerScrollInertiaCancelEvent {
1881+
/// Creates a pointer scroll-inertia cancel event.
1882+
///
1883+
/// All of the arguments must be non-null.
1884+
const PointerScrollInertiaCancelEvent({
1885+
super.timeStamp,
1886+
super.kind,
1887+
super.device,
1888+
super.position,
1889+
super.embedderId,
1890+
}) : assert(timeStamp != null),
1891+
assert(kind != null),
1892+
assert(device != null),
1893+
assert(position != null);
1894+
1895+
@override
1896+
PointerScrollInertiaCancelEvent transformed(Matrix4? transform) {
1897+
if (transform == null || transform == this.transform) {
1898+
return this;
1899+
}
1900+
return _TransformedPointerScrollInertiaCancelEvent(original as PointerScrollInertiaCancelEvent? ?? this, transform);
1901+
}
1902+
}
1903+
1904+
class _TransformedPointerScrollInertiaCancelEvent extends _TransformedPointerEvent with _CopyPointerScrollInertiaCancelEvent implements PointerScrollInertiaCancelEvent {
1905+
_TransformedPointerScrollInertiaCancelEvent(this.original, this.transform)
1906+
: assert(original != null), assert(transform != null);
1907+
1908+
@override
1909+
final PointerScrollInertiaCancelEvent original;
1910+
1911+
@override
1912+
final Matrix4 transform;
1913+
1914+
@override
1915+
PointerScrollInertiaCancelEvent transformed(Matrix4? transform) => original.transformed(transform);
1916+
}
1917+
18331918
mixin _CopyPointerPanZoomStartEvent on PointerEvent {
18341919
@override
18351920
PointerPanZoomStartEvent copyWith({

packages/flutter/lib/src/widgets/scrollable.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,9 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
731731
if (delta != 0.0 && targetScrollOffset != position.pixels) {
732732
GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll);
733733
}
734+
} else if (event is PointerScrollInertiaCancelEvent) {
735+
position.jumpTo(position.pixels);
736+
// Don't use the pointer signal resolver, all hit-tested scrollables should stop.
734737
}
735738
}
736739

packages/flutter/test/widgets/scrollable_test.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,22 @@ void main() {
14271427
expect(syntheticScrollableNode!.hasFlag(ui.SemanticsFlag.hasImplicitScrolling), isTrue);
14281428
handle.dispose();
14291429
});
1430+
1431+
testWidgets('Scroll inertia cancel event', (WidgetTester tester) async {
1432+
await pumpTest(tester, null);
1433+
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
1434+
expect(getScrollOffset(tester), dragOffset);
1435+
await tester.pump(); // trigger fling
1436+
expect(getScrollOffset(tester), dragOffset);
1437+
await tester.pump(const Duration(milliseconds: 200));
1438+
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
1439+
await tester.sendEventToBinding(testPointer.hover(tester.getCenter(find.byType(Scrollable))));
1440+
await tester.sendEventToBinding(testPointer.scrollInertiaCancel()); // Cancel partway through.
1441+
await tester.pump();
1442+
expect(getScrollOffset(tester), closeTo(333.2944, 0.0001));
1443+
await tester.pump(const Duration(milliseconds: 4800));
1444+
expect(getScrollOffset(tester), closeTo(333.2944, 0.0001));
1445+
});
14301446
}
14311447

14321448
// ignore: must_be_immutable

packages/flutter_test/lib/src/test_pointer.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,23 @@ class TestPointer {
304304
);
305305
}
306306

307+
/// Create a [PointerScrollInertiaCancelEvent] (e.g., user resting their finger on the trackpad).
308+
///
309+
/// By default, the time stamp on the event is [Duration.zero]. You can give a
310+
/// specific time stamp by passing the `timeStamp` argument.
311+
PointerScrollInertiaCancelEvent scrollInertiaCancel({
312+
Duration timeStamp = Duration.zero,
313+
}) {
314+
assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate pointer signal events");
315+
assert(location != null);
316+
return PointerScrollInertiaCancelEvent(
317+
timeStamp: timeStamp,
318+
kind: kind,
319+
device: _device,
320+
position: location!
321+
);
322+
}
323+
307324
/// Create a [PointerPanZoomStartEvent] (e.g., trackpad scroll; not scroll wheel
308325
/// or finger-drag scroll) with the given delta.
309326
///

0 commit comments

Comments
 (0)