// 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 scale = ScaleGestureRecognizer(); final tap = TapGestureRecognizer(); var didStartScale = false; Offset? updatedFocalPoint; int? updatedPointerCount; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; updatedPointerCount = details.pointerCount; }; 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; updatedPointerCount = details.pointerCount; }; var didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; var didTap = false; tap.onTap = () { didTap = true; }; final 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(updatedPointerCount, 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(updatedPointerCount, 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(updatedPointerCount, 1); updatedPointerCount = null; expect(didEndScale, isFalse); expect(didTap, isFalse); // Two-finger scaling final 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(updatedPointerCount, 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)); expect(updatedPointerCount, 2); updatedScale = null; updatedHorizontalScale = null; updatedVerticalScale = null; updatedDelta = null; updatedPointerCount = 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(updatedPointerCount, 2); expect(didTap, isFalse); // Horizontal scaling tester.route(pointer2.move(const Offset(0.0, 20.0))); expect(updatedHorizontalScale, 2.0); expect(updatedVerticalScale, 1.0); expect(updatedPointerCount, 2); // 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)); expect(updatedPointerCount, 2); tester.route(pointer2.move(const Offset(15.0, 25.0))); updatedFocalPoint = null; updatedScale = null; updatedDelta = null; updatedPointerCount = null; // Three-finger scaling final 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(updatedPointerCount, 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(updatedPointerCount, 3); updatedPointerCount = 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(updatedPointerCount, 3); updatedPointerCount = 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; expect(updatedPointerCount, 2); updatedPointerCount = 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; expect(updatedPointerCount, 2); updatedPointerCount = 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; expect(updatedPointerCount, 1); updatedPointerCount = null; // We are done tester.route(pointer3.up()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(updatedPointerCount, isNull); expect(didEndScale, isTrue); didEndScale = false; expect(didTap, isFalse); scale.dispose(); tap.dispose(); }); testGesture('Rejects scale gestures from unallowed device kinds', (GestureTester tester) { final scale = ScaleGestureRecognizer( supportedDevices: {PointerDeviceKind.touch}, ); var didStartScale = false; scale.onStart = (ScaleStartDetails details) { didStartScale = true; }; double? updatedScale; scale.onUpdate = (ScaleUpdateDetails details) { updatedScale = details.scale; }; final 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 scale = ScaleGestureRecognizer( supportedDevices: {PointerDeviceKind.touch}, ); var 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; }; var didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; final 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 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 scale = ScaleGestureRecognizer(); final drag = HorizontalDragGestureRecognizer(); final log = []; 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 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(['scale-start', 'scale-update'])); log.clear(); final 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, ['scale-end']); log.clear(); tester.route(pointer2.move(const Offset(30.0, 20.0))); expect(log, equals(['scale-start', 'scale-update'])); log.clear(); tester.route(pointer1.up()); expect(log, equals(['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 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(['scale-start', 'scale-update'])); log.clear(); tester.route(pointer3.up()); expect(log, equals(['scale-end'])); log.clear(); scale.dispose(); drag.dispose(); }); testGesture('Should recognize rotation gestures', (GestureTester tester) { final scale = ScaleGestureRecognizer(); final tap = TapGestureRecognizer(); var didStartScale = false; Offset? updatedFocalPoint; int? updatedPointerCount; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; updatedPointerCount = details.pointerCount; }; double? updatedRotation; Offset? updatedDelta; scale.onUpdate = (ScaleUpdateDetails details) { updatedRotation = details.rotation; updatedFocalPoint = details.focalPoint; updatedDelta = details.focalPointDelta; updatedPointerCount = details.pointerCount; }; var didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; var didTap = false; tap.onTap = () { didTap = true; }; final 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(updatedPointerCount, 1); updatedPointerCount = null; expect(didEndScale, isFalse); expect(didTap, isFalse); // Two-finger scaling final 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(updatedPointerCount, 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(updatedPointerCount, 2); updatedPointerCount = 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(updatedPointerCount, 2); updatedPointerCount = null; expect(didEndScale, isFalse); expect(didTap, isFalse); // Three-finger scaling final 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(updatedPointerCount, 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(updatedPointerCount, 3); updatedPointerCount = 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(updatedPointerCount, 3); updatedPointerCount = 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; expect(updatedPointerCount, 2); updatedPointerCount = 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; expect(updatedPointerCount, 2); updatedPointerCount = 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; expect(updatedPointerCount, 2); updatedPointerCount = null; tester.route(pointer2.up()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(updatedRotation, isNull); expect(updatedPointerCount, 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(updatedPointerCount, 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 scale = ScaleGestureRecognizer(); addTearDown(scale.dispose); double? updatedRotation; scale.onUpdate = (ScaleUpdateDetails details) { updatedRotation = details.rotation; }; final pointer1 = TestPointer(); final PointerDownEvent down = pointer1.down(Offset.zero); scale.addPointer(down); tester.closeArena(1); tester.route(down); final 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 scale = ScaleGestureRecognizer(); var pointerCountOfStart = 0; scale.onStart = (ScaleStartDetails details) => pointerCountOfStart = details.pointerCount; var pointerCountOfUpdate = 0; scale.onUpdate = (ScaleUpdateDetails details) => pointerCountOfUpdate = details.pointerCount; var pointerCountOfEnd = 0; scale.onEnd = (ScaleEndDetails details) => pointerCountOfEnd = details.pointerCount; final 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 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 scale = ScaleGestureRecognizer(); addTearDown(scale.dispose); final drag = HorizontalDragGestureRecognizer(); addTearDown(drag.dispose); var didStartScale = false; Offset? updatedFocalPoint; int? updatedPointerCount; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; updatedPointerCount = details.pointerCount; }; 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; updatedPointerCount = details.pointerCount; }; var didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; final 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(updatedPointerCount, isNull); expect(didEndScale, isFalse); // Panning. tester.route(start); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(updatedPointerCount, 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(updatedPointerCount, 2); updatedPointerCount = 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); expect(updatedPointerCount, 2); updatedScale = null; updatedHorizontalScale = null; updatedVerticalScale = null; updatedDelta = null; updatedPointerCount = 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); expect(updatedPointerCount, 2); updatedScale = null; updatedHorizontalScale = null; updatedVerticalScale = null; updatedDelta = null; updatedPointerCount = null; expect(didEndScale, isFalse); // We are done. tester.route(pointer1.panZoomEnd()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(updatedPointerCount, isNull); expect(didEndScale, isTrue); didEndScale = false; scale.dispose(); }); testGesture('Pointer pan/zooms should work alongside touches', (GestureTester tester) { final scale = ScaleGestureRecognizer(); addTearDown(scale.dispose); final drag = HorizontalDragGestureRecognizer(); addTearDown(drag.dispose); var didStartScale = false; Offset? updatedFocalPoint; int? updatedPointerCount; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; updatedPointerCount = details.pointerCount; }; 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; updatedPointerCount = details.pointerCount; }; var didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; final touchPointer1 = TestPointer(2); final touchPointer2 = TestPointer(3); final 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(updatedPointerCount, isNull); expect(didEndScale, isFalse); // Panning starting with trackpad. tester.route(panZoomStart); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(updatedPointerCount, 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(updatedPointerCount, 2); updatedPointerCount = 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(updatedPointerCount, 3); updatedPointerCount = 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(updatedPointerCount, 4); updatedPointerCount = 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(updatedPointerCount, 4); updatedPointerCount = 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(updatedPointerCount, 4); updatedPointerCount = 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(updatedPointerCount, isNull); expect(didStartScale, isFalse); tester.route(touchPointer1.up()); expect(updatedFocalPoint, isNull); expect(didEndScale, isFalse); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(updatedPointerCount, isNull); expect(didStartScale, isFalse); tester.route(touchPointer2.up()); expect(didEndScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(updatedPointerCount, isNull); expect(didStartScale, isFalse); scale.dispose(); }); testGesture('Scale gesture competes with drag for trackpad gesture', (GestureTester tester) { final scale = ScaleGestureRecognizer(); final drag = HorizontalDragGestureRecognizer(); final log = []; 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 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(['scale-start', 'scale-update'])); log.clear(); final 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, ['scale-end']); log.clear(); tester.route(pointer2.panZoomUpdate(const Offset(10.0, 20.0), pan: const Offset(20.0, 0.0))); expect(log, equals(['scale-start', 'scale-update'])); log.clear(); tester.route(pointer1.panZoomEnd()); expect(log, equals(['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 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(['scale-start', 'scale-update'])); log.clear(); tester.route(pointer3.panZoomEnd()); expect(log, equals(['scale-end'])); log.clear(); scale.dispose(); drag.dispose(); }); testGesture('Scale gesture from pan/zoom events properly handles DragStartBehavior.start', ( GestureTester tester, ) { final scale = ScaleGestureRecognizer(dragStartBehavior: DragStartBehavior.start); addTearDown(scale.dispose); final drag = HorizontalDragGestureRecognizer(); addTearDown(drag.dispose); var 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; }; var didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; final 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; }); testGesture('scale trackpadScrollCausesScale', (GestureTester tester) { final scale = ScaleGestureRecognizer( dragStartBehavior: DragStartBehavior.start, trackpadScrollCausesScale: true, ); var didStartScale = false; Offset? updatedFocalPoint; int? updatedPointerCount; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; updatedPointerCount = details.pointerCount; }; double? updatedScale; Offset? updatedDelta; scale.onUpdate = (ScaleUpdateDetails details) { updatedScale = details.scale; updatedFocalPoint = details.focalPoint; updatedDelta = details.focalPointDelta; updatedPointerCount = details.pointerCount; }; var didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; final 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(updatedPointerCount, 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(updatedPointerCount, 2); updatedPointerCount = null; 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(updatedPointerCount, 2); updatedPointerCount = 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(updatedPointerCount, 2); updatedPointerCount = null; expect(didEndScale, isFalse); // End. tester.route(pointer1.panZoomEnd()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(updatedPointerCount, 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(updatedPointerCount, 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(updatedPointerCount, 2); updatedPointerCount = null; 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(updatedPointerCount, 2); updatedPointerCount = 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(updatedPointerCount, 2); updatedPointerCount = null; expect(didEndScale, isFalse); // End. tester.route(pointer1.panZoomEnd()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(updatedPointerCount, isNull); expect(didEndScale, isTrue); didEndScale = false; scale.dispose(); }); testGesture('scale ending velocity', (GestureTester tester) { final scale = ScaleGestureRecognizer( dragStartBehavior: DragStartBehavior.start, trackpadScrollCausesScale: true, ); var didStartScale = false; Offset? updatedFocalPoint; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; }; var didEndScale = false; double? scaleEndVelocity; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; scaleEndVelocity = details.scaleVelocity; }; final 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 (var 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(); }); testGesture( 'ScaleStartDetails and ScaleUpdateDetails callbacks should contain their event.timestamp', (GestureTester tester) { final scale = ScaleGestureRecognizer(); final tap = TapGestureRecognizer(); var didStartScale = false; Offset? updatedFocalPoint; Duration? initialSourceTimestamp; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedFocalPoint = details.focalPoint; initialSourceTimestamp = details.sourceTimeStamp; }; double? updatedScale; double? updatedHorizontalScale; double? updatedVerticalScale; Offset? updatedDelta; Duration? updatedSourceTimestamp; scale.onUpdate = (ScaleUpdateDetails details) { updatedScale = details.scale; updatedHorizontalScale = details.horizontalScale; updatedVerticalScale = details.verticalScale; updatedFocalPoint = details.focalPoint; updatedDelta = details.focalPointDelta; updatedSourceTimestamp = details.sourceTimeStamp; }; var didEndScale = false; scale.onEnd = (ScaleEndDetails details) { didEndScale = true; }; var didTap = false; tap.onTap = () { didTap = true; }; final pointer1 = TestPointer(); final PointerDownEvent down = pointer1.down( Offset.zero, timeStamp: const Duration(milliseconds: 10), ); scale.addPointer(down); tap.addPointer(down); tester.closeArena(1); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(updatedSourceTimestamp, isNull); expect(didEndScale, isFalse); expect(didTap, isFalse); expect(initialSourceTimestamp, isNull); // One-finger panning. tester.route(down); expect(didStartScale, isFalse); expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(didEndScale, isFalse); expect(didTap, isFalse); expect(initialSourceTimestamp, isNull); tester.route( pointer1.move(const Offset(20.0, 30.0), timeStamp: const Duration(milliseconds: 20)), ); 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(updatedSourceTimestamp, const Duration(milliseconds: 20)); updatedSourceTimestamp = null; expect(initialSourceTimestamp, const Duration(milliseconds: 10)); initialSourceTimestamp = null; expect(didEndScale, isFalse); expect(didTap, isFalse); expect(scale.pointerCount, 1); // Two-finger scaling. final pointer2 = TestPointer(2); final PointerDownEvent down2 = pointer2.down( const Offset(10.0, 20.0), timeStamp: const Duration(milliseconds: 30), ); scale.addPointer(down2); tap.addPointer(down2); tester.closeArena(2); tester.route(down2); expect(scale.pointerCount, 2); expect(didEndScale, isTrue); didEndScale = false; expect(updatedScale, isNull); expect(updatedFocalPoint, isNull); expect(updatedDelta, isNull); expect(updatedSourceTimestamp, isNull); expect(didStartScale, isFalse); expect(initialSourceTimestamp, isNull); // Zoom in. tester.route( pointer2.move(const Offset(0.0, 10.0), timeStamp: const Duration(milliseconds: 40)), ); 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)); expect(updatedSourceTimestamp, const Duration(milliseconds: 40)); expect(initialSourceTimestamp, const Duration(milliseconds: 40)); updatedScale = null; updatedHorizontalScale = null; updatedVerticalScale = null; updatedDelta = null; updatedSourceTimestamp = null; initialSourceTimestamp = null; expect(didEndScale, isFalse); expect(didTap, isFalse); // Zoom out. tester.route( pointer2.move(const Offset(15.0, 25.0), timeStamp: const Duration(milliseconds: 50)), ); 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(updatedSourceTimestamp, const Duration(milliseconds: 50)); expect(didTap, isFalse); expect(initialSourceTimestamp, isNull); // Horizontal scaling. tester.route( pointer2.move(const Offset(0.0, 20.0), timeStamp: const Duration(milliseconds: 60)), ); expect(updatedHorizontalScale, 2.0); expect(updatedVerticalScale, 1.0); expect(updatedSourceTimestamp, const Duration(milliseconds: 60)); expect(initialSourceTimestamp, isNull); // Vertical scaling. tester.route( pointer2.move(const Offset(10.0, 10.0), timeStamp: const Duration(milliseconds: 70)), ); expect(updatedHorizontalScale, 1.0); expect(updatedVerticalScale, 2.0); expect(updatedDelta, const Offset(5.0, -5.0)); expect(updatedSourceTimestamp, const Duration(milliseconds: 70)); expect(initialSourceTimestamp, isNull); tester.route(pointer2.move(const Offset(15.0, 25.0))); updatedFocalPoint = null; updatedScale = null; updatedDelta = null; updatedSourceTimestamp = null; // Three-finger scaling. final pointer3 = TestPointer(3); final PointerDownEvent down3 = pointer3.down( const Offset(25.0, 35.0), timeStamp: const Duration(milliseconds: 80), ); 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); expect(initialSourceTimestamp, isNull); // Zoom in. tester.route( pointer3.move(const Offset(55.0, 65.0), timeStamp: const Duration(milliseconds: 90)), ); 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(updatedSourceTimestamp, const Duration(milliseconds: 90)); updatedSourceTimestamp = null; expect(initialSourceTimestamp, const Duration(milliseconds: 90)); initialSourceTimestamp = 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), timeStamp: const Duration(milliseconds: 100)), ); tester.route( pointer2.move(const Offset(20.0, 30.0), timeStamp: const Duration(milliseconds: 110)), ); tester.route( pointer3.move(const Offset(15.0, 25.0), timeStamp: const Duration(milliseconds: 120)), ); 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); expect(updatedSourceTimestamp, const Duration(milliseconds: 120)); updatedSourceTimestamp = null; expect(initialSourceTimestamp, isNull); tester.route(pointer1.up()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didEndScale, isTrue); expect(updatedSourceTimestamp, isNull); expect(initialSourceTimestamp, isNull); didEndScale = false; expect(didTap, isFalse); // Continue scaling with two fingers. tester.route( pointer3.move(const Offset(10.0, 20.0), timeStamp: const Duration(milliseconds: 130)), ); 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; expect(updatedSourceTimestamp, const Duration(milliseconds: 130)); updatedSourceTimestamp = null; expect(initialSourceTimestamp, const Duration(milliseconds: 130)); initialSourceTimestamp = null; // Continue rotating with two fingers. tester.route( pointer3.move(const Offset(30.0, 40.0), timeStamp: const Duration(milliseconds: 140)), ); 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; expect(updatedSourceTimestamp, const Duration(milliseconds: 140)); updatedSourceTimestamp = null; expect(initialSourceTimestamp, isNull); tester.route( pointer3.move(const Offset(10.0, 20.0), timeStamp: const Duration(milliseconds: 140)), ); 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; expect(updatedSourceTimestamp, const Duration(milliseconds: 140)); updatedSourceTimestamp = null; expect(initialSourceTimestamp, isNull); tester.route(pointer2.up()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(updatedSourceTimestamp, isNull); expect(initialSourceTimestamp, isNull); expect(didEndScale, isTrue); didEndScale = false; expect(didTap, isFalse); // Continue panning with one finger. tester.route(pointer3.move(Offset.zero, timeStamp: const Duration(milliseconds: 150))); 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; expect(updatedSourceTimestamp, const Duration(milliseconds: 150)); updatedSourceTimestamp = null; expect(initialSourceTimestamp, const Duration(milliseconds: 150)); initialSourceTimestamp = null; // We are done. tester.route(pointer3.up()); expect(didStartScale, isFalse); expect(updatedFocalPoint, isNull); expect(updatedScale, isNull); expect(updatedDelta, isNull); expect(didEndScale, isTrue); expect(updatedSourceTimestamp, isNull); expect(initialSourceTimestamp, isNull); didEndScale = false; expect(didTap, isFalse); scale.dispose(); tap.dispose(); }, ); testGesture('ScaleStartDetails should contain the correct PointerDeviceKind', ( GestureTester tester, ) { final scale = ScaleGestureRecognizer(); var didStartScale = false; PointerDeviceKind? updatedKind; scale.onStart = (ScaleStartDetails details) { didStartScale = true; updatedKind = details.kind; }; scale.onEnd = (ScaleEndDetails details) { didStartScale = false; }; // The default kind is touch. // ignore: avoid_redundant_argument_values final pointer1 = TestPointer(1, PointerDeviceKind.touch); final PointerDownEvent down = pointer1.down(Offset.zero); scale.addPointer(down); tester.closeArena(1); // One-finger panning tester.route(down); tester.route(pointer1.move(const Offset(20.0, 30.0))); expect(didStartScale, isTrue); expect(updatedKind, PointerDeviceKind.touch); tester.route(pointer1.up()); expect(didStartScale, isFalse); final pointer2 = TestPointer(2, PointerDeviceKind.mouse); final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0)); scale.addPointer(down2); tester.closeArena(2); tester.route(down2); tester.route(pointer2.move(const Offset(20.0, 30.0))); expect(didStartScale, isTrue); expect(updatedKind, PointerDeviceKind.mouse); tester.route(pointer2.up()); expect(didStartScale, isFalse); final pointer3 = TestPointer(3, PointerDeviceKind.stylus); final PointerDownEvent down3 = pointer3.down(const Offset(10.0, 20.0)); scale.addPointer(down3); tester.closeArena(3); tester.route(down3); tester.route(pointer3.move(const Offset(20.0, 30.0))); expect(didStartScale, isTrue); expect(updatedKind, PointerDeviceKind.stylus); tester.route(pointer3.up()); expect(didStartScale, isFalse); final pointer4 = TestPointer(4, PointerDeviceKind.invertedStylus); final PointerDownEvent down4 = pointer4.down(const Offset(10.0, 20.0)); scale.addPointer(down4); tester.closeArena(4); tester.route(down4); tester.route(pointer4.move(const Offset(20.0, 30.0))); expect(didStartScale, isTrue); expect(updatedKind, PointerDeviceKind.invertedStylus); tester.route(pointer4.up()); expect(didStartScale, isFalse); final pointer5 = TestPointer(5, PointerDeviceKind.unknown); final PointerDownEvent down5 = pointer5.down(const Offset(10.0, 20.0)); scale.addPointer(down5); tester.closeArena(5); tester.route(down5); tester.route(pointer5.move(const Offset(20.0, 30.0))); expect(didStartScale, isTrue); expect(updatedKind, PointerDeviceKind.unknown); tester.route(pointer5.up()); expect(didStartScale, isFalse); final pointer6 = TestPointer(6, PointerDeviceKind.trackpad); final PointerPanZoomStartEvent down6 = pointer6.panZoomStart(const Offset(10.0, 20.0)); scale.addPointerPanZoom(down6); tester.closeArena(6); tester.route(down6); tester.route(pointer6.panZoomUpdate(const Offset(20.0, 30.0))); expect(didStartScale, isTrue); expect(updatedKind, PointerDeviceKind.trackpad); tester.route(pointer6.panZoomEnd()); expect(didStartScale, isFalse); scale.dispose(); }); }