Unverified Commit 22f51c34 authored by Callum Moffat's avatar Callum Moffat Committed by GitHub

Scroll inertia cancel [framework] (#106891)

parent bf22b7ae
...@@ -259,9 +259,15 @@ class PointerEventConverter { ...@@ -259,9 +259,15 @@ class PointerEventConverter {
scrollDelta: scrollDelta, scrollDelta: scrollDelta,
embedderId: datum.embedderId, 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: 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 // This branch should already have 'unknown' filtered out, but
// we don't want to return anything or miss if someone adds a new // we don't want to return anything or miss if someone adds a new
// enumeration to PointerSignalKind. // enumeration to PointerSignalKind.
......
...@@ -1830,6 +1830,91 @@ class _TransformedPointerScrollEvent extends _TransformedPointerEvent with _Copy ...@@ -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 { mixin _CopyPointerPanZoomStartEvent on PointerEvent {
@override @override
PointerPanZoomStartEvent copyWith({ PointerPanZoomStartEvent copyWith({
......
...@@ -731,6 +731,9 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -731,6 +731,9 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
if (delta != 0.0 && targetScrollOffset != position.pixels) { if (delta != 0.0 && targetScrollOffset != position.pixels) {
GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll); 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.
} }
} }
......
...@@ -1427,6 +1427,22 @@ void main() { ...@@ -1427,6 +1427,22 @@ void main() {
expect(syntheticScrollableNode!.hasFlag(ui.SemanticsFlag.hasImplicitScrolling), isTrue); expect(syntheticScrollableNode!.hasFlag(ui.SemanticsFlag.hasImplicitScrolling), isTrue);
handle.dispose(); 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 // ignore: must_be_immutable
......
...@@ -304,6 +304,23 @@ class TestPointer { ...@@ -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 /// Create a [PointerPanZoomStartEvent] (e.g., trackpad scroll; not scroll wheel
/// or finger-drag scroll) with the given delta. /// or finger-drag scroll) with the given delta.
/// ///
......
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