Commit 74e93e4c authored by Renan's avatar Renan Committed by Ian Hickson

Make ScaleGestureRecognizer detect pointer rotation (#17345)

parent ff1f8dd1
......@@ -2,6 +2,8 @@
// 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 'arena.dart';
import 'constants.dart';
import 'events.dart';
......@@ -47,13 +49,15 @@ class ScaleStartDetails {
class ScaleUpdateDetails {
/// Creates details for [GestureScaleUpdateCallback].
///
/// The [focalPoint] and [scale] arguments must not be null. The [scale]
/// The [focalPoint], [scale], [rotation] arguments must not be null. The [scale]
/// argument must be greater than or equal to zero.
ScaleUpdateDetails({
this.focalPoint = Offset.zero,
this.scale = 1.0,
this.rotation = 0.0,
}) : assert(focalPoint != null),
assert(scale != null && scale >= 0.0);
assert(scale != null && scale >= 0.0),
assert(rotation != null);
/// The focal point of the pointers in contact with the screen. Reported in
/// global coordinates.
......@@ -63,8 +67,12 @@ class ScaleUpdateDetails {
/// greater than or equal to zero.
final double scale;
/// The angle implied by the first two pointers to enter in contact with
/// the screen. Expressed in radians.
final double rotation;
@override
String toString() => 'ScaleUpdateDetails(focalPoint: $focalPoint, scale: $scale)';
String toString() => 'ScaleUpdateDetails(focalPoint: $focalPoint, scale: $scale, rotation: $rotation)';
}
/// Details for [GestureScaleEndCallback].
......@@ -99,11 +107,41 @@ bool _isFlingGesture(Velocity velocity) {
return speedSquared > kMinFlingVelocity * kMinFlingVelocity;
}
/// Defines a line between two pointers on screen.
///
/// [_LineBetweenPointers] is an abstraction of a line between two pointers in
/// contact with the screen. Used to track the rotation of a scale gesture.
class _LineBetweenPointers{
/// Creates a [_LineBetweenPointers]. None of the [pointerStartLocation], [pointerStartId]
/// [pointerEndLocation] and [pointerEndId] must be null. [pointerStartId] and [pointerEndId]
/// should be different.
_LineBetweenPointers({
this.pointerStartLocation = Offset.zero,
this.pointerStartId = 0,
this.pointerEndLocation = Offset.zero,
this.pointerEndId = 1
}) : assert(pointerStartLocation != null && pointerEndLocation != null),
assert(pointerStartId != null && pointerEndId != null),
assert(pointerStartId != pointerEndId);
// The location and the id of the pointer that marks the start of the line.
final Offset pointerStartLocation;
final int pointerStartId;
// The location and the id of the pointer that marks the end of the line.
final Offset pointerEndLocation;
final int pointerEndId;
}
/// Recognizes a scale gesture.
///
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
/// calculates their focal point and indicated scale. When a focal pointer is
/// established, the recognizer calls [onStart]. As the focal point and scale
/// calculates their focal point, indicated scale and rotation. When a focal pointer is
/// established, the recognizer calls [onStart]. As the focal point, scale, rotation
/// change, the recognizer calls [onUpdate]. When the pointers are no longer in
/// contact with the screen, the recognizer calls [onEnd].
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
......@@ -127,11 +165,34 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
Offset _currentFocalPoint;
double _initialSpan;
double _currentSpan;
_LineBetweenPointers _initialLine;
_LineBetweenPointers _currentLine;
Map<int, Offset> _pointerLocations;
List<int> _pointerQueue; /// A queue to sort pointers in order of entrance
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
double _computeRotationFactor() {
if (_initialLine == null || _currentLine == null) {
return 0.0;
}
final double fx = _initialLine.pointerStartLocation.dx;
final double fy = _initialLine.pointerStartLocation.dy;
final double sx = _initialLine.pointerEndLocation.dx;
final double sy = _initialLine.pointerEndLocation.dy;
final double nfx = _currentLine.pointerStartLocation.dx;
final double nfy = _currentLine.pointerStartLocation.dy;
final double nsx = _currentLine.pointerEndLocation.dx;
final double nsy = _currentLine.pointerEndLocation.dy;
final double angle1 = math.atan2(fy - sy, fx - sx);
final double angle2 = math.atan2(nfy - nsy, nfx - nsx);
return angle2 - angle1;
}
@override
void addPointer(PointerEvent event) {
startTrackingPointer(event.pointer);
......@@ -141,6 +202,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_initialSpan = 0.0;
_currentSpan = 0.0;
_pointerLocations = <int, Offset>{};
_pointerQueue = <int>[];
}
}
......@@ -158,14 +220,18 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
shouldStartIfAccepted = true;
} else if (event is PointerDownEvent) {
_pointerLocations[event.pointer] = event.position;
_pointerQueue.add(event.pointer);
didChangeConfiguration = true;
shouldStartIfAccepted = true;
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
_pointerLocations.remove(event.pointer);
_pointerQueue.remove(event.pointer);
didChangeConfiguration = true;
}
_updateLines();
_update();
if (!didChangeConfiguration || _reconfigure(event.pointer))
_advanceStateMachine(shouldStartIfAccepted);
stopTrackingIfPointerNoLongerDown(event);
......@@ -187,9 +253,40 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_currentSpan = count > 0 ? totalDeviation / count : 0.0;
}
/// Updates [_initialLine] and [_currentLine] accordingly to the situation of
/// the registered pointers
void _updateLines() {
final int count = _pointerLocations.keys.length;
assert(_pointerQueue.length >= count);
/// In case of just one pointer registered, reconfigure [_initialLine]
if (count < 2) {
_initialLine = _currentLine;
} else if (_initialLine != null &&
_initialLine.pointerStartId == _pointerQueue[0] &&
_initialLine.pointerEndId == _pointerQueue[1]) {
/// Rotation updated, set the [_currentLine]
_currentLine = _LineBetweenPointers(
pointerStartId: _pointerQueue[0],
pointerStartLocation: _pointerLocations[_pointerQueue[0]],
pointerEndId: _pointerQueue[1],
pointerEndLocation: _pointerLocations[ _pointerQueue[1]]
);
} else {
/// A new rotation process is on the way, set the [_initialLine]
_initialLine = _LineBetweenPointers(
pointerStartId: _pointerQueue[0],
pointerStartLocation: _pointerLocations[_pointerQueue[0]],
pointerEndId: _pointerQueue[1],
pointerEndLocation: _pointerLocations[ _pointerQueue[1]]
);
_currentLine = null;
}
}
bool _reconfigure(int pointer) {
_initialFocalPoint = _currentFocalPoint;
_initialSpan = _currentSpan;
_initialLine = _currentLine;
if (_state == _ScaleState.started) {
if (onEnd != null) {
final VelocityTracker tracker = _velocityTrackers[pointer];
......@@ -230,7 +327,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
}
if (_state == _ScaleState.started && onUpdate != null)
invokeCallback<void>('onUpdate', () => onUpdate(ScaleUpdateDetails(scale: _scaleFactor, focalPoint: _currentFocalPoint)));
invokeCallback<void>('onUpdate', () => onUpdate(ScaleUpdateDetails(scale: _scaleFactor, focalPoint: _currentFocalPoint, rotation: _computeRotationFactor())));
}
void _dispatchOnStartCallbackIfNeeded() {
......
......@@ -2,6 +2,8 @@
// 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_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
......@@ -155,6 +157,18 @@ void main() {
expect(updatedScale, 2.0);
updatedScale = 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;
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;
tester.route(pointer2.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
......@@ -266,4 +280,179 @@ void main() {
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;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedRotation = details.rotation;
updatedFocalPoint = details.focalPoint;
};
bool didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
bool didTap = false;
tap.onTap = () {
didTap = true;
};
final TestPointer pointer1 = TestPointer(1);
final PointerDownEvent down = pointer1.down(const Offset(0.0, 0.0));
scale.addPointer(down);
tap.addPointer(down);
tester.closeArena(1);
expect(didStartScale, isFalse);
expect(updatedRotation, isNull);
expect(updatedFocalPoint, 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(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(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(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(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(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(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(updatedRotation, 0.0);
updatedRotation = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
tester.route(pointer1.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, 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(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(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(updatedRotation, 0.0);
updatedRotation = null;
tester.route(pointer2.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, 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(updatedRotation, isNull);
expect(didEndScale, isFalse);
didEndScale = false;
expect(didTap, isFalse);
scale.dispose();
tap.dispose();
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment