Skip to content

Commit 601f48c

Browse files
authored
InteractiveViewer discrete trackpad panning (#112171)
* InteractiveViewer web trackpad panning * Address feedback
1 parent 8cfc606 commit 601f48c

File tree

4 files changed

+97
-5
lines changed

4 files changed

+97
-5
lines changed

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,8 +1788,7 @@ class PointerScrollEvent extends PointerSignalEvent with _PointerEventDescriptio
17881788
assert(kind != null),
17891789
assert(device != null),
17901790
assert(position != null),
1791-
assert(scrollDelta != null),
1792-
assert(!identical(kind, PointerDeviceKind.trackpad));
1791+
assert(scrollDelta != null);
17931792

17941793
@override
17951794
final Offset scrollDelta;

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

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -954,11 +954,57 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
954954
_controller.forward();
955955
}
956956

957-
// Handle mousewheel scroll events.
957+
// Handle mousewheel and web trackpad scroll events.
958958
void _receivedPointerSignal(PointerSignalEvent event) {
959959
final double scaleChange;
960960
if (event is PointerScrollEvent) {
961-
// Ignore left and right scroll.
961+
if (event.kind == PointerDeviceKind.trackpad) {
962+
// Trackpad scroll, so treat it as a pan.
963+
widget.onInteractionStart?.call(
964+
ScaleStartDetails(
965+
focalPoint: event.position,
966+
localFocalPoint: event.localPosition,
967+
),
968+
);
969+
970+
final Offset localDelta = PointerEvent.transformDeltaViaPositions(
971+
untransformedEndPosition: event.position + event.scrollDelta,
972+
untransformedDelta: event.scrollDelta,
973+
transform: event.transform,
974+
);
975+
976+
if (!_gestureIsSupported(_GestureType.pan)) {
977+
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
978+
focalPoint: event.position - event.scrollDelta,
979+
localFocalPoint: event.localPosition - event.scrollDelta,
980+
focalPointDelta: -localDelta,
981+
));
982+
widget.onInteractionEnd?.call(ScaleEndDetails());
983+
return;
984+
}
985+
986+
final Offset focalPointScene = _transformationController!.toScene(
987+
event.localPosition,
988+
);
989+
990+
final Offset newFocalPointScene = _transformationController!.toScene(
991+
event.localPosition - localDelta,
992+
);
993+
994+
_transformationController!.value = _matrixTranslate(
995+
_transformationController!.value,
996+
newFocalPointScene - focalPointScene
997+
);
998+
999+
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
1000+
focalPoint: event.position - event.scrollDelta,
1001+
localFocalPoint: event.localPosition - localDelta,
1002+
focalPointDelta: -localDelta
1003+
));
1004+
widget.onInteractionEnd?.call(ScaleEndDetails());
1005+
return;
1006+
}
1007+
// Ignore left and right mouse wheel scroll.
9621008
if (event.scrollDelta.dy == 0.0) {
9631009
return;
9641010
}

packages/flutter/test/gestures/events_test.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -850,12 +850,15 @@ void main() {
850850
expect(const PointerHoverEvent(kind: PointerDeviceKind.trackpad), isNotNull);
851851
// Regression test for https://github.com/flutter/flutter/issues/108176
852852
expect(const PointerScrollInertiaCancelEvent(kind: PointerDeviceKind.trackpad), isNotNull);
853+
854+
expect(const PointerScrollEvent(kind: PointerDeviceKind.trackpad), isNotNull);
853855
// The test passes if it compiles.
854856
});
855857

856858
test('Ensure certain event types are not allowed', () {
857859
expect(() => PointerDownEvent(kind: PointerDeviceKind.trackpad), throwsAssertionError);
858-
expect(() => PointerScrollEvent(kind: PointerDeviceKind.trackpad), throwsAssertionError);
860+
expect(() => PointerMoveEvent(kind: PointerDeviceKind.trackpad), throwsAssertionError);
861+
expect(() => PointerUpEvent(kind: PointerDeviceKind.trackpad), throwsAssertionError);
859862
});
860863
}
861864

packages/flutter/test/widgets/interactive_viewer_test.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1722,6 +1722,50 @@ void main() {
17221722
expect(translation2.y, lessThan(translation1.y));
17231723
});
17241724

1725+
testWidgets('discrete scroll pointer events', (WidgetTester tester) async {
1726+
final TransformationController transformationController = TransformationController();
1727+
const double boundaryMargin = 50.0;
1728+
await tester.pumpWidget(
1729+
MaterialApp(
1730+
home: Scaffold(
1731+
body: Center(
1732+
child: InteractiveViewer(
1733+
boundaryMargin: const EdgeInsets.all(boundaryMargin),
1734+
transformationController: transformationController,
1735+
child: const SizedBox(width: 200.0, height: 200.0),
1736+
),
1737+
),
1738+
),
1739+
),
1740+
);
1741+
1742+
expect(transformationController.value.getMaxScaleOnAxis(), 1.0);
1743+
Vector3 translation = transformationController.value.getTranslation();
1744+
expect(translation.x, 0);
1745+
expect(translation.y, 0);
1746+
1747+
// Send a mouse scroll event, it should cause a scale.
1748+
final TestPointer mouse = TestPointer(1, PointerDeviceKind.mouse);
1749+
await tester.sendEventToBinding(mouse.hover(tester.getCenter(find.byType(SizedBox))));
1750+
await tester.sendEventToBinding(mouse.scroll(const Offset(300, -200)));
1751+
await tester.pump();
1752+
expect(transformationController.value.getMaxScaleOnAxis(), 2.5);
1753+
translation = transformationController.value.getTranslation();
1754+
// Will be translated to maintain centering.
1755+
expect(translation.x, -150);
1756+
expect(translation.y, -150);
1757+
1758+
// Send a trackpad scroll event, it should cause a pan and no scale.
1759+
final TestPointer trackpad = TestPointer(1, PointerDeviceKind.trackpad);
1760+
await tester.sendEventToBinding(trackpad.hover(tester.getCenter(find.byType(SizedBox))));
1761+
await tester.sendEventToBinding(trackpad.scroll(const Offset(100, -25)));
1762+
await tester.pump();
1763+
expect(transformationController.value.getMaxScaleOnAxis(), 2.5);
1764+
translation = transformationController.value.getTranslation();
1765+
expect(translation.x, -250);
1766+
expect(translation.y, -125);
1767+
});
1768+
17251769
testWidgets('discrete scale pointer event', (WidgetTester tester) async {
17261770
final TransformationController transformationController = TransformationController();
17271771
const double boundaryMargin = 50.0;

0 commit comments

Comments
 (0)