// Copyright 2014 The Flutter Authors. All rights reserved. // 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_test/flutter_test.dart'; import 'gesture_tester.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); testGesture('Should recognize scale gestures', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer(); final TapGestureRecognizer tap = TapGestureRecognizer(); bool didStartScale = false; Offset? updatedFocalPoint; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; }; double? updatedScale; double? updatedHorizontalScale; double? updatedVerticalScale; Offset? updatedDelta; scale.onUpdate = (ScaleUpdateDetails details) { updatedScale = details.scale; updatedHorizontalScale = details.horizontalScale; updatedVerticalScale = details.verticalScale; updatedFocalPoint = details.focalPoint; updatedDelta = details.focalPointDelta; }; bool didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; bool didTap = false; tap.onTap = () { didTap = true; }; final TestPointer pointer1 = TestPointer(); final PointerDownEvent down = pointer1.down(Offset.zero); scale.addPointer(down); tap.addPointer(down); tester.closeArena(1); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); expect(didTap, isFalse); // One-finger panning tester.route(down); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); expect(didTap, isFalse); tester.route(pointer1.move(const Offset(20.0, 30.0))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(20.0, 30.0)); updatedFocalPoint = null; expect(updatedScale, 1.0); updatedScale = null; expect(updatedDelta, const Offset(20.0, 30.0)); updatedDelta = null; expect(didEndScale, isFalse); expect(didTap, isFalse); // Two-finger scaling final TestPointer pointer2 = TestPointer(2); final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0)); scale.addPointer(down2); tap.addPointer(down2); tester.closeArena(2); tester.route(down2); expect(didEndScale, isTrue); didEndScale = false; expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didStartScale, isFalse); // Zoom in tester.route(pointer2.move(const Offset(0.0, 10.0))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(10.0, 20.0)); updatedFocalPoint = null; expect(updatedScale, 2.0); expect(updatedHorizontalScale, 2.0); expect(updatedVerticalScale, 2.0); expect(updatedDelta, const Offset(-5.0, -5.0)); updatedScale = null; updatedHorizontalScale = null; updatedVerticalScale = null; updatedDelta = null; expect(didEndScale, isFalse); expect(didTap, isFalse); // Zoom out tester.route(pointer2.move(const Offset(15.0, 25.0))); expect(updatedFocalPoint, const Offset(17.5, 27.5)); expect(updatedScale, 0.5); expect(updatedHorizontalScale, 0.5); expect(updatedVerticalScale, 0.5); expect(updatedDelta, const Offset(7.5, 7.5)); expect(didTap, isFalse); // Horizontal scaling tester.route(pointer2.move(const Offset(0.0, 20.0))); expect(updatedHorizontalScale, 2.0); expect(updatedVerticalScale, 1.0); // Vertical scaling tester.route(pointer2.move(const Offset(10.0, 10.0))); expect(updatedHorizontalScale, 1.0); expect(updatedVerticalScale, 2.0); expect(updatedDelta, const Offset(5.0, -5.0)); tester.route(pointer2.move(const Offset(15.0, 25.0))); updatedFocalPoint = null; updatedScale = null; updatedDelta = null; // Three-finger scaling final TestPointer pointer3 = TestPointer(3); final PointerDownEvent down3 = pointer3.down(const Offset(25.0, 35.0)); scale.addPointer(down3); tap.addPointer(down3); tester.closeArena(3); tester.route(down3); expect(didEndScale, isTrue); didEndScale = false; expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didStartScale, isFalse); // Zoom in tester.route(pointer3.move(const Offset(55.0, 65.0))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(30.0, 40.0)); updatedFocalPoint = null; expect(updatedScale, 5.0); updatedScale = null; expect(updatedDelta, const Offset(10.0, 10.0)); updatedDelta = null; expect(didEndScale, isFalse); expect(didTap, isFalse); // Return to original positions but with different fingers tester.route(pointer1.move(const Offset(25.0, 35.0))); tester.route(pointer2.move(const Offset(20.0, 30.0))); tester.route(pointer3.move(const Offset(15.0, 25.0))); expect(didStartScale, isFalse); expect(updatedFocalPoint, const Offset(20.0, 30.0)); updatedFocalPoint = null; expect(updatedScale, 1.0); updatedScale = null; expect(updatedDelta!.dx, closeTo(-13.3, 0.1)); expect(updatedDelta!.dy, closeTo(-13.3, 0.1)); updatedDelta = null; expect(didEndScale, isFalse); expect(didTap, isFalse); tester.route(pointer1.up()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didEndScale, isTrue); didEndScale = false; expect(didTap, isFalse); // Continue scaling with two fingers tester.route(pointer3.move(const Offset(10.0, 20.0))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(15.0, 25.0)); updatedFocalPoint = null; expect(updatedScale, 2.0); updatedScale = null; expect(updatedDelta, const Offset(-2.5, -2.5)); updatedDelta = null; // Continue rotating with two fingers tester.route(pointer3.move(const Offset(30.0, 40.0))); expect(updatedFocalPoint, const Offset(25.0, 35.0)); updatedFocalPoint = null; expect(updatedScale, 2.0); updatedScale = null; expect(updatedDelta, const Offset(10.0, 10.0)); updatedDelta = null; tester.route(pointer3.move(const Offset(10.0, 20.0))); expect(updatedFocalPoint, const Offset(15.0, 25.0)); updatedFocalPoint = null; expect(updatedScale, 2.0); updatedScale = null; expect(updatedDelta, const Offset(-10.0, -10.0)); updatedDelta = null; tester.route(pointer2.up()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didEndScale, isTrue); didEndScale = false; expect(didTap, isFalse); // Continue panning with one finger tester.route(pointer3.move(Offset.zero)); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, Offset.zero); updatedFocalPoint = null; expect(updatedScale, 1.0); updatedScale = null; expect(updatedDelta, const Offset(-10.0, -20.0)); updatedDelta = null; // We are done tester.route(pointer3.up()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didEndScale, isTrue); didEndScale = false; expect(didTap, isFalse); scale.dispose(); tap.dispose(); }); testGesture('Rejects scale gestures from unallowed device kinds', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer( supportedDevices: <PointerDeviceKind>{ PointerDeviceKind.touch }, ); bool didStartScale = false; scale.onStart = (ScaleStartDetails details) { didStartScale = true; }; double? updatedScale; scale.onUpdate = (ScaleUpdateDetails details) { updatedScale = details.scale; }; final TestPointer mousePointer = TestPointer(1, PointerDeviceKind.mouse); final PointerDownEvent down = mousePointer.down(Offset.zero); scale.addPointer(down); tester.closeArena(1); // One-finger panning tester.route(down); expect(didStartScale, isFalse); expect(updatedScale, isNull); // Using a mouse, the scale gesture shouldn't even start. tester.route(mousePointer.move(const Offset(20.0, 30.0))); expect(didStartScale, isFalse); expect(updatedScale, isNull); scale.dispose(); }); testGesture('Scale gestures starting from allowed device kinds cannot be ended from unallowed devices', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer( supportedDevices: <PointerDeviceKind>{ PointerDeviceKind.touch }, ); bool didStartScale = false; Offset? updatedFocalPoint; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; }; double? updatedScale; scale.onUpdate = (ScaleUpdateDetails details) { updatedScale = details.scale; updatedFocalPoint = details.focalPoint; }; bool didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; final TestPointer touchPointer = TestPointer(); final PointerDownEvent down = touchPointer.down(Offset.zero); scale.addPointer(down); tester.closeArena(1); // One-finger panning tester.route(down); expect(didStartScale, isTrue); didStartScale = false; expect(updatedScale, isNull); expect(updatedFocalPoint, Offset.zero); expect(didEndScale, isFalse); // The gesture can start using one touch finger. tester.route(touchPointer.move(const Offset(20.0, 30.0))); expect(updatedFocalPoint, const Offset(20.0, 30.0)); updatedFocalPoint = null; expect(updatedScale, 1.0); updatedScale = null; expect(didEndScale, isFalse); // Two-finger scaling final TestPointer mousePointer = TestPointer(2, PointerDeviceKind.mouse); final PointerDownEvent down2 = mousePointer.down(const Offset(10.0, 20.0)); scale.addPointer(down2); tester.closeArena(2); tester.route(down2); // Mouse-generated events are ignored. expect(didEndScale, isFalse); expect(updatedScale, isNull); expect(didStartScale, isFalse); // Zoom in using a mouse doesn't work either. tester.route(mousePointer.move(const Offset(0.0, 10.0))); expect(updatedScale, isNull); expect(didEndScale, isFalse); scale.dispose(); }); testGesture('Scale gesture competes with drag', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer(); final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); final List<String> log = <String>[]; scale.onStart = (ScaleStartDetails details) { log.add('scale-start'); }; scale.onUpdate = (ScaleUpdateDetails details) { log.add('scale-update'); }; scale.onEnd = (ScaleEndDetails details) { log.add('scale-end'); }; drag.onStart = (DragStartDetails details) { log.add('drag-start'); }; drag.onEnd = (DragEndDetails details) { log.add('drag-end'); }; final TestPointer pointer1 = TestPointer(); final PointerDownEvent down = pointer1.down(const Offset(10.0, 10.0)); scale.addPointer(down); drag.addPointer(down); tester.closeArena(1); expect(log, isEmpty); // Vertical moves are scales. tester.route(down); expect(log, isEmpty); // Scale will win if focal point delta exceeds 18.0*2. tester.route(pointer1.move(const Offset(10.0, 50.0))); // Delta of 40.0 exceeds 18.0*2. expect(log, equals(<String>['scale-start', 'scale-update'])); log.clear(); final TestPointer pointer2 = TestPointer(2); final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0)); scale.addPointer(down2); drag.addPointer(down2); tester.closeArena(2); expect(log, isEmpty); // Second pointer joins scale even though it moves horizontally. tester.route(down2); expect(log, <String>['scale-end']); log.clear(); tester.route(pointer2.move(const Offset(30.0, 20.0))); expect(log, equals(<String>['scale-start', 'scale-update'])); log.clear(); tester.route(pointer1.up()); expect(log, equals(<String>['scale-end'])); log.clear(); tester.route(pointer2.up()); expect(log, isEmpty); log.clear(); // Horizontal moves are either drags or scales, depending on which wins first. // TODO(ianh): https://github.com/flutter/flutter/issues/11384 // In this case, we move fast, so that the scale wins. If we moved slowly, // the horizontal drag would win, since it was added first. final TestPointer pointer3 = TestPointer(3); final PointerDownEvent down3 = pointer3.down(const Offset(30.0, 30.0)); scale.addPointer(down3); drag.addPointer(down3); tester.closeArena(3); tester.route(down3); expect(log, isEmpty); tester.route(pointer3.move(const Offset(100.0, 30.0))); expect(log, equals(<String>['scale-start', 'scale-update'])); log.clear(); tester.route(pointer3.up()); expect(log, equals(<String>['scale-end'])); log.clear(); scale.dispose(); drag.dispose(); }); testGesture('Should recognize rotation gestures', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer(); final TapGestureRecognizer tap = TapGestureRecognizer(); bool didStartScale = false; Offset? updatedFocalPoint; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; }; double? updatedRotation; Offset? updatedDelta; scale.onUpdate = (ScaleUpdateDetails details) { updatedRotation = details.rotation; updatedFocalPoint = details.focalPoint; updatedDelta = details.focalPointDelta; }; bool didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; bool didTap = false; tap.onTap = () { didTap = true; }; final TestPointer pointer1 = TestPointer(); final PointerDownEvent down = pointer1.down(Offset.zero); scale.addPointer(down); tap.addPointer(down); tester.closeArena(1); expect(didStartScale, isFalse); expect(updatedRotation, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); expect(didTap, isFalse); tester.route(down); tester.route(pointer1.move(const Offset(20.0, 30.0))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(20.0, 30.0)); updatedFocalPoint = null; expect(updatedDelta, const Offset(20.0, 30.0)); updatedDelta = null; expect(updatedRotation, 0.0); updatedRotation = null; expect(didEndScale, isFalse); expect(didTap, isFalse); // Two-finger scaling final TestPointer pointer2 = TestPointer(2); final PointerDownEvent down2 = pointer2.down(const Offset(30.0, 40.0)); scale.addPointer(down2); tap.addPointer(down2); tester.closeArena(2); tester.route(down2); expect(didEndScale, isTrue); didEndScale = false; expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(updatedRotation, isNull); expect(didStartScale, isFalse); // Zoom in tester.route(pointer2.move(const Offset(40.0, 50.0))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(30.0, 40.0)); updatedFocalPoint = null; expect(updatedDelta, const Offset(5.0, 5.0)); updatedDelta = null; expect(updatedRotation, 0.0); updatedRotation = null; expect(didEndScale, isFalse); expect(didTap, isFalse); // Rotation tester.route(pointer2.move(const Offset(0.0, 10.0))); expect(updatedFocalPoint, const Offset(10.0, 20.0)); updatedFocalPoint = null; expect(updatedDelta, const Offset(-20.0, -20.0)); updatedDelta = null; expect(updatedRotation, math.pi); updatedRotation = null; expect(didEndScale, isFalse); expect(didTap, isFalse); // Three-finger scaling final TestPointer pointer3 = TestPointer(3); final PointerDownEvent down3 = pointer3.down(const Offset(25.0, 35.0)); scale.addPointer(down3); tap.addPointer(down3); tester.closeArena(3); tester.route(down3); expect(didEndScale, isTrue); didEndScale = false; expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(updatedRotation, isNull); expect(didStartScale, isFalse); // Zoom in tester.route(pointer3.move(const Offset(55.0, 65.0))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(25.0, 35.0)); updatedFocalPoint = null; expect(updatedDelta, const Offset(10.0, 10.0)); updatedDelta = null; expect(updatedRotation, 0.0); updatedRotation = null; expect(didEndScale, isFalse); expect(didTap, isFalse); // Return to original positions but with different fingers tester.route(pointer1.move(const Offset(25.0, 35.0))); tester.route(pointer2.move(const Offset(20.0, 30.0))); tester.route(pointer3.move(const Offset(15.0, 25.0))); expect(didStartScale, isFalse); expect(updatedFocalPoint, const Offset(20.0, 30.0)); updatedFocalPoint = null; expect(updatedDelta!.dx, closeTo(-13.3, 0.1)); expect(updatedDelta!.dy, closeTo(-13.3, 0.1)); updatedDelta = null; expect(updatedRotation, 0.0); updatedRotation = null; expect(didEndScale, isFalse); expect(didTap, isFalse); tester.route(pointer1.up()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(updatedRotation, isNull); expect(didEndScale, isTrue); didEndScale = false; expect(didTap, isFalse); // Continue scaling with two fingers tester.route(pointer3.move(const Offset(10.0, 20.0))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(15.0, 25.0)); updatedFocalPoint = null; expect(updatedDelta, const Offset(-2.5, -2.5)); updatedDelta = null; expect(updatedRotation, 0.0); updatedRotation = null; // Continue rotating with two fingers tester.route(pointer3.move(const Offset(30.0, 40.0))); expect(updatedFocalPoint, const Offset(25.0, 35.0)); updatedFocalPoint = null; expect(updatedDelta, const Offset(10.0, 10.0)); updatedDelta = null; expect(updatedRotation, - math.pi); updatedRotation = null; tester.route(pointer3.move(const Offset(10.0, 20.0))); expect(updatedFocalPoint, const Offset(15.0, 25.0)); updatedFocalPoint = null; expect(updatedDelta, const Offset(-10.0, -10.0)); updatedDelta = null; expect(updatedRotation, 0.0); updatedRotation = null; tester.route(pointer2.up()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(updatedRotation, isNull); expect(didEndScale, isTrue); didEndScale = false; expect(didTap, isFalse); // We are done tester.route(pointer3.up()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(updatedRotation, isNull); expect(didEndScale, isFalse); didEndScale = false; expect(didTap, isFalse); scale.dispose(); tap.dispose(); }); // Regressing test for https://github.com/flutter/flutter/issues/78941 testGesture('First rotation test', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer(); double? updatedRotation; scale.onUpdate = (ScaleUpdateDetails details) { updatedRotation = details.rotation; }; final TestPointer pointer1 = TestPointer(); final PointerDownEvent down = pointer1.down(Offset.zero); scale.addPointer(down); tester.closeArena(1); tester.route(down); final TestPointer pointer2 = TestPointer(2); final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 10.0)); scale.addPointer(down2); tester.closeArena(2); tester.route(down2); expect(updatedRotation, isNull); // Rotation 45°. tester.route(pointer2.move(const Offset(0.0, 10.0))); expect(updatedRotation, math.pi / 4.0); }); testGesture('Scale gestures pointer count test', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer(); int pointerCountOfStart = 0; scale.onStart = (ScaleStartDetails details) => pointerCountOfStart = details.pointerCount; int pointerCountOfUpdate = 0; scale.onUpdate = (ScaleUpdateDetails details) => pointerCountOfUpdate = details.pointerCount; int pointerCountOfEnd = 0; scale.onEnd = (ScaleEndDetails details) => pointerCountOfEnd = details.pointerCount; final TestPointer pointer1 = TestPointer(); final PointerDownEvent down = pointer1.down(Offset.zero); scale.addPointer(down); tester.closeArena(1); // One-finger panning tester.route(down); // One pointer in contact with the screen now. expect(pointerCountOfStart, 1); tester.route(pointer1.move(const Offset(20.0, 30.0))); expect(pointerCountOfUpdate, 1); // Two-finger scaling final TestPointer pointer2 = TestPointer(2); final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0)); scale.addPointer(down2); tester.closeArena(2); tester.route(down2); // Two pointers in contact with the screen now. expect(pointerCountOfEnd, 2); // Additional pointer down will trigger an end event. tester.route(pointer2.move(const Offset(0.0, 10.0))); expect(pointerCountOfStart, 2); // The new pointer move will trigger a start event. expect(pointerCountOfUpdate, 2); tester.route(pointer1.up()); // One pointer in contact with the screen now. expect(pointerCountOfEnd, 1); tester.route(pointer2.move(const Offset(0.0, 10.0))); expect(pointerCountOfStart, 1); expect(pointerCountOfUpdate, 1); tester.route(pointer2.up()); // No pointer in contact with the screen now. expect(pointerCountOfEnd, 0); scale.dispose(); }); testGesture('Should recognize scale gestures from pointer pan/zoom events', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer(); final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); bool didStartScale = false; Offset? updatedFocalPoint; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; }; double? updatedScale; double? updatedHorizontalScale; double? updatedVerticalScale; Offset? updatedDelta; scale.onUpdate = (ScaleUpdateDetails details) { updatedScale = details.scale; updatedHorizontalScale = details.horizontalScale; updatedVerticalScale = details.verticalScale; updatedFocalPoint = details.focalPoint; updatedDelta = details.focalPointDelta; }; bool didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; final TestPointer pointer1 = TestPointer(2, PointerDeviceKind.trackpad); final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero); scale.addPointerPanZoom(start); drag.addPointerPanZoom(start); tester.closeArena(2); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); // Panning. tester.route(start); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(20.0, 30.0))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(20.0, 30.0)); updatedFocalPoint = null; expect(updatedScale, 1.0); updatedScale = null; expect(updatedDelta, const Offset(20.0, 30.0)); updatedDelta = null; expect(didEndScale, isFalse); // Zoom in. tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(20.0, 30.0), scale: 2.0)); expect(updatedFocalPoint, const Offset(20.0, 30.0)); updatedFocalPoint = null; expect(updatedScale, 2.0); expect(updatedHorizontalScale, 2.0); expect(updatedVerticalScale, 2.0); expect(updatedDelta, Offset.zero); updatedScale = null; updatedHorizontalScale = null; updatedVerticalScale = null; updatedDelta = null; expect(didEndScale, isFalse); // Zoom out. tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(20.0, 30.0))); expect(updatedFocalPoint, const Offset(20.0, 30.0)); updatedFocalPoint = null; expect(updatedScale, 1.0); expect(updatedHorizontalScale, 1.0); expect(updatedVerticalScale, 1.0); expect(updatedDelta, Offset.zero); updatedScale = null; updatedHorizontalScale = null; updatedVerticalScale = null; updatedDelta = null; expect(didEndScale, isFalse); // We are done. tester.route(pointer1.panZoomEnd()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didEndScale, isTrue); didEndScale = false; scale.dispose(); }); testGesture('Pointer pan/zooms should work alongside touches', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer(); final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); bool didStartScale = false; Offset? updatedFocalPoint; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; }; double? updatedScale; double? updatedHorizontalScale; double? updatedVerticalScale; Offset? updatedDelta; double? updatedRotation; scale.onUpdate = (ScaleUpdateDetails details) { updatedScale = details.scale; updatedHorizontalScale = details.horizontalScale; updatedVerticalScale = details.verticalScale; updatedFocalPoint = details.focalPoint; updatedDelta = details.focalPointDelta; updatedRotation = details.rotation; }; bool didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; final TestPointer touchPointer1 = TestPointer(2); final TestPointer touchPointer2 = TestPointer(3); final TestPointer panZoomPointer = TestPointer(4, PointerDeviceKind.trackpad); final PointerPanZoomStartEvent panZoomStart = panZoomPointer.panZoomStart(Offset.zero); scale.addPointerPanZoom(panZoomStart); drag.addPointerPanZoom(panZoomStart); tester.closeArena(4); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); // Panning starting with trackpad. tester.route(panZoomStart); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); tester.route(panZoomPointer.panZoomUpdate(Offset.zero, pan: const Offset(40, 40))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(40.0, 40.0)); updatedFocalPoint = null; expect(updatedScale, 1.0); updatedScale = null; expect(updatedDelta, const Offset(40.0, 40.0)); updatedDelta = null; expect(didEndScale, isFalse); // Add a touch pointer. final PointerDownEvent touchStart1 = touchPointer1.down(const Offset(40, 40)); scale.addPointer(touchStart1); drag.addPointer(touchStart1); tester.closeArena(2); tester.route(touchStart1); expect(didEndScale, isTrue); didEndScale = false; tester.route(touchPointer1.move(const Offset(10, 10))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(25, 25)); updatedFocalPoint = null; // 1 down pointer + pointer pan/zoom should not scale, only pan. expect(updatedScale, 1.0); updatedScale = null; expect(updatedDelta, const Offset(-15, -15)); updatedDelta = null; expect(didEndScale, isFalse); // Add a second touch pointer. final PointerDownEvent touchStart2 = touchPointer2.down(const Offset(10, 40)); scale.addPointer(touchStart2); drag.addPointer(touchStart2); tester.closeArena(3); tester.route(touchStart2); expect(didEndScale, isTrue); didEndScale = false; // Move the second pointer to cause pan, zoom, and rotation. tester.route(touchPointer2.move(const Offset(40, 40))); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, const Offset(30, 30)); updatedFocalPoint = null; expect(updatedScale, math.sqrt(2)); updatedScale = null; expect(updatedHorizontalScale, 1.0); updatedHorizontalScale = null; expect(updatedVerticalScale, 1.0); updatedVerticalScale = null; expect(updatedDelta, const Offset(10, 0)); updatedDelta = null; expect(updatedRotation, -math.pi / 4); updatedRotation = null; expect(didEndScale, isFalse); // Change the scale and angle of the pan/zoom to test combining. // Scale should be multiplied together. // Rotation angle should be added together. tester.route(panZoomPointer.panZoomUpdate(Offset.zero, pan: const Offset(40, 40), scale: math.sqrt(2), rotation: math.pi / 3)); expect(didStartScale, isFalse); expect(updatedFocalPoint, const Offset(30, 30)); updatedFocalPoint = null; expect(updatedScale, closeTo(2, 0.0001)); updatedScale = null; expect(updatedHorizontalScale, math.sqrt(2)); updatedHorizontalScale = null; expect(updatedVerticalScale, math.sqrt(2)); updatedVerticalScale = null; expect(updatedDelta, Offset.zero); updatedDelta = null; expect(updatedRotation, closeTo(math.pi / 12, 0.0001)); updatedRotation = null; expect(didEndScale, isFalse); // Move the pan/zoom origin to test combining. tester.route(panZoomPointer.panZoomUpdate(const Offset(15, 15), pan: const Offset(55, 55), scale: math.sqrt(2), rotation: math.pi / 3)); expect(didStartScale, isFalse); expect(updatedFocalPoint, const Offset(40, 40)); updatedFocalPoint = null; expect(updatedScale, closeTo(2, 0.0001)); updatedScale = null; expect(updatedDelta, const Offset(10, 10)); updatedDelta = null; expect(updatedRotation, closeTo(math.pi / 12, 0.0001)); updatedRotation = null; expect(didEndScale, isFalse); // We are done. tester.route(panZoomPointer.panZoomEnd()); expect(updatedFocalPoint, isNull); expect(didEndScale, isTrue); didEndScale = false; expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didStartScale, isFalse); tester.route(touchPointer1.up()); expect(updatedFocalPoint, isNull); expect(didEndScale, isFalse); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didStartScale, isFalse); tester.route(touchPointer2.up()); expect(didEndScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didStartScale, isFalse); scale.dispose(); }); testGesture('Scale gesture competes with drag for trackpad gesture', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer(); final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); final List<String> log = <String>[]; scale.onStart = (ScaleStartDetails details) { log.add('scale-start'); }; scale.onUpdate = (ScaleUpdateDetails details) { log.add('scale-update'); }; scale.onEnd = (ScaleEndDetails details) { log.add('scale-end'); }; drag.onStart = (DragStartDetails details) { log.add('drag-start'); }; drag.onEnd = (DragEndDetails details) { log.add('drag-end'); }; final TestPointer pointer1 = TestPointer(2, PointerDeviceKind.trackpad); final PointerPanZoomStartEvent down = pointer1.panZoomStart(const Offset(10.0, 10.0)); scale.addPointerPanZoom(down); drag.addPointerPanZoom(down); tester.closeArena(2); expect(log, isEmpty); // Vertical moves are scales. tester.route(down); expect(log, isEmpty); // Scale will win if focal point delta exceeds 18.0*2. tester.route(pointer1.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(10.0, 40.0))); // delta of 40.0 exceeds 18.0*2. expect(log, equals(<String>['scale-start', 'scale-update'])); log.clear(); final TestPointer pointer2 = TestPointer(3, PointerDeviceKind.trackpad); final PointerPanZoomStartEvent down2 = pointer2.panZoomStart(const Offset(10.0, 20.0)); scale.addPointerPanZoom(down2); drag.addPointerPanZoom(down2); tester.closeArena(3); expect(log, isEmpty); // Second pointer joins scale even though it moves horizontally. tester.route(down2); expect(log, <String>['scale-end']); log.clear(); tester.route(pointer2.panZoomUpdate(const Offset(10.0, 20.0), pan: const Offset(20.0, 0.0))); expect(log, equals(<String>['scale-start', 'scale-update'])); log.clear(); tester.route(pointer1.panZoomEnd()); expect(log, equals(<String>['scale-end'])); log.clear(); tester.route(pointer2.panZoomEnd()); expect(log, isEmpty); log.clear(); // Horizontal moves are either drags or scales, depending on which wins first. // TODO(ianh): https://github.com/flutter/flutter/issues/11384 // In this case, we move fast, so that the scale wins. If we moved slowly, // the horizontal drag would win, since it was added first. final TestPointer pointer3 = TestPointer(4, PointerDeviceKind.trackpad); final PointerPanZoomStartEvent down3 = pointer3.panZoomStart(const Offset(30.0, 30.0)); scale.addPointerPanZoom(down3); drag.addPointerPanZoom(down3); tester.closeArena(4); tester.route(down3); expect(log, isEmpty); tester.route(pointer3.panZoomUpdate(const Offset(30.0, 30.0), pan: const Offset(70.0, 0.0))); expect(log, equals(<String>['scale-start', 'scale-update'])); log.clear(); tester.route(pointer3.panZoomEnd()); expect(log, equals(<String>['scale-end'])); log.clear(); scale.dispose(); drag.dispose(); }); testGesture('Scale gesture from pan/zoom events properly handles DragStartBehavior.start', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer(dragStartBehavior: DragStartBehavior.start); final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); bool didStartScale = false; Offset? updatedFocalPoint; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; }; double? updatedScale; double? updatedHorizontalScale; double? updatedVerticalScale; double? updatedRotation; Offset? updatedDelta; scale.onUpdate = (ScaleUpdateDetails details) { updatedScale = details.scale; updatedHorizontalScale = details.horizontalScale; updatedVerticalScale = details.verticalScale; updatedFocalPoint = details.focalPoint; updatedRotation = details.rotation; updatedDelta = details.focalPointDelta; }; bool didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; final TestPointer pointer1 = TestPointer(2, PointerDeviceKind.trackpad); final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero); scale.addPointerPanZoom(start); drag.addPointerPanZoom(start); tester.closeArena(2); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); tester.route(start); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); // Zoom enough to win the gesture. tester.route(pointer1.panZoomUpdate(Offset.zero, scale: 1.1, rotation: 1)); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, Offset.zero); updatedFocalPoint = null; expect(updatedScale, 1.0); updatedScale = null; expect(updatedDelta, Offset.zero); updatedDelta = null; expect(didEndScale, isFalse); // Zoom in - should be relative to 1.1. tester.route(pointer1.panZoomUpdate(Offset.zero, scale: 1.21, rotation: 1.5)); expect(updatedFocalPoint, Offset.zero); updatedFocalPoint = null; expect(updatedScale, closeTo(1.1, 0.0001)); expect(updatedHorizontalScale, closeTo(1.1, 0.0001)); expect(updatedVerticalScale, closeTo(1.1, 0.0001)); expect(updatedRotation, 0.5); expect(updatedDelta, Offset.zero); updatedScale = null; updatedHorizontalScale = null; updatedVerticalScale = null; updatedRotation = null; updatedDelta = null; expect(didEndScale, isFalse); // Zoom out - should be relative to 1.1. tester.route(pointer1.panZoomUpdate(Offset.zero, scale: 0.99, rotation: 1.0)); expect(updatedFocalPoint, Offset.zero); updatedFocalPoint = null; expect(updatedScale, closeTo(0.9, 0.0001)); expect(updatedHorizontalScale, closeTo(0.9, 0.0001)); expect(updatedVerticalScale, closeTo(0.9, 0.0001)); expect(updatedRotation, 0.0); expect(updatedDelta, Offset.zero); updatedScale = null; updatedHorizontalScale = null; updatedVerticalScale = null; updatedDelta = null; expect(didEndScale, isFalse); // We are done. tester.route(pointer1.panZoomEnd()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didEndScale, isTrue); didEndScale = false; scale.dispose(); }); testGesture('scale trackpadScrollCausesScale', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer( dragStartBehavior: DragStartBehavior.start, trackpadScrollCausesScale: true ); bool didStartScale = false; Offset? updatedFocalPoint; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; }; double? updatedScale; Offset? updatedDelta; scale.onUpdate = (ScaleUpdateDetails details) { updatedScale = details.scale; updatedFocalPoint = details.focalPoint; updatedDelta = details.focalPointDelta; }; bool didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; final TestPointer pointer1 = TestPointer(2, PointerDeviceKind.trackpad); final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero); scale.addPointerPanZoom(start); tester.closeArena(2); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); tester.route(start); expect(didStartScale, isTrue); didStartScale = false; expect(updatedScale, isNull); expect(updatedFocalPoint, Offset.zero); updatedFocalPoint = null; expect(updatedDelta, isNull); expect(didEndScale, isFalse); // Zoom in by scrolling up. tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(0, -200))); expect(didStartScale, isFalse); expect(updatedFocalPoint, Offset.zero); updatedFocalPoint = null; expect(updatedScale, math.e); updatedScale = null; expect(updatedDelta, Offset.zero); updatedDelta = null; expect(didEndScale, isFalse); // A horizontal scroll should do nothing. tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(200, -200))); expect(didStartScale, isFalse); expect(updatedFocalPoint, Offset.zero); updatedFocalPoint = null; expect(updatedScale, math.e); updatedScale = null; expect(updatedDelta, Offset.zero); updatedDelta = null; expect(didEndScale, isFalse); // End. tester.route(pointer1.panZoomEnd()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didEndScale, isTrue); didEndScale = false; // Try with a different trackpadScrollToScaleFactor scale.trackpadScrollToScaleFactor = const Offset(1/125, 0); final PointerPanZoomStartEvent start2 = pointer1.panZoomStart(Offset.zero); scale.addPointerPanZoom(start2); tester.closeArena(2); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); tester.route(start2); expect(didStartScale, isTrue); didStartScale = false; expect(updatedScale, isNull); expect(updatedFocalPoint, Offset.zero); updatedFocalPoint = null; expect(updatedDelta, isNull); expect(didEndScale, isFalse); // Zoom in by scrolling left. tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(125, 0))); expect(didStartScale, isFalse); didStartScale = false; expect(updatedFocalPoint, Offset.zero); updatedFocalPoint = null; expect(updatedScale, math.e); updatedScale = null; expect(updatedDelta, Offset.zero); updatedDelta = null; expect(didEndScale, isFalse); // A vertical scroll should do nothing. tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(125, 125))); expect(didStartScale, isFalse); expect(updatedFocalPoint, Offset.zero); updatedFocalPoint = null; expect(updatedScale, math.e); updatedScale = null; expect(updatedDelta, Offset.zero); updatedDelta = null; expect(didEndScale, isFalse); // End. tester.route(pointer1.panZoomEnd()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didEndScale, isTrue); didEndScale = false; scale.dispose(); }); testGesture('scale ending velocity', (GestureTester tester) { final ScaleGestureRecognizer scale = ScaleGestureRecognizer( dragStartBehavior: DragStartBehavior.start, trackpadScrollCausesScale: true ); bool didStartScale = false; Offset? updatedFocalPoint; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; }; bool didEndScale = false; double? scaleEndVelocity; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; scaleEndVelocity = details.scaleVelocity; }; final TestPointer pointer1 = TestPointer(2, PointerDeviceKind.trackpad); final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero); scale.addPointerPanZoom(start); tester.closeArena(2); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(didEndScale, isFalse); tester.route(start); expect(didStartScale, isTrue); didStartScale = false; expect(updatedFocalPoint, Offset.zero); updatedFocalPoint = null; expect(didEndScale, isFalse); // Zoom in by scrolling up. for (int i = 0; i < 100; i++) { tester.route(pointer1.panZoomUpdate( Offset.zero, pan: Offset(0, i * -10), timeStamp: Duration(milliseconds: i * 25) )); } // End. tester.route(pointer1.panZoomEnd(timeStamp: const Duration(milliseconds: 2500))); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(didEndScale, isTrue); didEndScale = false; expect(scaleEndVelocity, moreOrLessEquals(281.41454098027765)); scale.dispose(); }); }