Commit f9ae2267 authored by Adam Barth's avatar Adam Barth Committed by Ian Hickson

Improve cooperation between scale and drag gestures (#9298)

Now the scale gesture will accept if its focal point moves more than the pan
slop. This change lets it compete with a drag gesture (e.g., a containing scrol
view) in the same way that the pan gesture does.

Fixes #8735
parent 8ee6525c
...@@ -121,22 +121,18 @@ class _GridPhotoViewerState extends State<GridPhotoViewer> with SingleTickerProv ...@@ -121,22 +121,18 @@ class _GridPhotoViewerState extends State<GridPhotoViewer> with SingleTickerProv
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new LayoutBuilder( return new GestureDetector(
builder: (BuildContext context, BoxConstraints constraints) { onScaleStart: _handleOnScaleStart,
return new GestureDetector( onScaleUpdate: _handleOnScaleUpdate,
onScaleStart: _handleOnScaleStart, onScaleEnd: _handleOnScaleEnd,
onScaleUpdate: _handleOnScaleUpdate, child: new ClipRect(
onScaleEnd: _handleOnScaleEnd, child: new Transform(
child: new Transform( transform: new Matrix4.identity()
transform: new Matrix4.identity() ..translate(_offset.dx, _offset.dy)
..translate(_offset.dx, _offset.dy) ..scale(_scale),
..scale(_scale), child: new Image.asset(config.photo.assetName, fit: BoxFit.cover),
child: new ClipRect( ),
child: new Image.asset(config.photo.assetName, fit: BoxFit.cover), ),
),
),
);
}
); );
} }
} }
......
...@@ -9,7 +9,7 @@ import 'recognizer.dart'; ...@@ -9,7 +9,7 @@ import 'recognizer.dart';
import 'velocity_tracker.dart'; import 'velocity_tracker.dart';
/// The possible states of a [ScaleGestureRecognizer]. /// The possible states of a [ScaleGestureRecognizer].
enum ScaleState { enum _ScaleState {
/// The recognizer is ready to start recognizing a gesture. /// The recognizer is ready to start recognizing a gesture.
ready, ready,
...@@ -39,6 +39,9 @@ class ScaleStartDetails { ...@@ -39,6 +39,9 @@ class ScaleStartDetails {
/// The initial focal point of the pointers in contact with the screen. /// The initial focal point of the pointers in contact with the screen.
/// Reported in global coordinates. /// Reported in global coordinates.
final Point focalPoint; final Point focalPoint;
@override
String toString() => 'ScaleStartDetails(focalPoint: $focalPoint)';
} }
/// Details for [GestureScaleUpdateCallback]. /// Details for [GestureScaleUpdateCallback].
...@@ -59,6 +62,9 @@ class ScaleUpdateDetails { ...@@ -59,6 +62,9 @@ class ScaleUpdateDetails {
/// The scale implied by the pointers in contact with the screen. A value /// The scale implied by the pointers in contact with the screen. A value
/// greater than or equal to zero. /// greater than or equal to zero.
final double scale; final double scale;
@override
String toString() => 'ScaleUpdateDetails(focalPoint: $focalPoint, scale: $scale)';
} }
/// Details for [GestureScaleEndCallback]. /// Details for [GestureScaleEndCallback].
...@@ -72,6 +78,9 @@ class ScaleEndDetails { ...@@ -72,6 +78,9 @@ class ScaleEndDetails {
/// The velocity of the last pointer to be lifted off of the screen. /// The velocity of the last pointer to be lifted off of the screen.
final Velocity velocity; final Velocity velocity;
@override
String toString() => 'ScaleEndDetails(velocity: $velocity)';
} }
/// Signature for when the pointers in contact with the screen have established /// Signature for when the pointers in contact with the screen have established
...@@ -110,8 +119,10 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -110,8 +119,10 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
/// The pointers are no longer in contact with the screen. /// The pointers are no longer in contact with the screen.
GestureScaleEndCallback onEnd; GestureScaleEndCallback onEnd;
ScaleState _state = ScaleState.ready; _ScaleState _state = _ScaleState.ready;
Point _initialFocalPoint;
Point _currentFocalPoint;
double _initialSpan; double _initialSpan;
double _currentSpan; double _currentSpan;
Map<int, Point> _pointerLocations; Map<int, Point> _pointerLocations;
...@@ -123,8 +134,8 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -123,8 +134,8 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
void addPointer(PointerEvent event) { void addPointer(PointerEvent event) {
startTrackingPointer(event.pointer); startTrackingPointer(event.pointer);
_velocityTrackers[event.pointer] = new VelocityTracker(); _velocityTrackers[event.pointer] = new VelocityTracker();
if (_state == ScaleState.ready) { if (_state == _ScaleState.ready) {
_state = ScaleState.possible; _state = _ScaleState.possible;
_initialSpan = 0.0; _initialSpan = 0.0;
_currentSpan = 0.0; _currentSpan = 0.0;
_pointerLocations = <int, Point>{}; _pointerLocations = <int, Point>{};
...@@ -133,104 +144,127 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -133,104 +144,127 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
@override @override
void handleEvent(PointerEvent event) { void handleEvent(PointerEvent event) {
assert(_state != ScaleState.ready); assert(_state != _ScaleState.ready);
bool configChanged = false; bool didChangeConfiguration = false;
bool shouldStartIfAccepted = false;
if (event is PointerMoveEvent) { if (event is PointerMoveEvent) {
final VelocityTracker tracker = _velocityTrackers[event.pointer]; final VelocityTracker tracker = _velocityTrackers[event.pointer];
assert(tracker != null); assert(tracker != null);
tracker.addPosition(event.timeStamp, event.position); tracker.addPosition(event.timeStamp, event.position);
_pointerLocations[event.pointer] = event.position; _pointerLocations[event.pointer] = event.position;
shouldStartIfAccepted = true;
} else if (event is PointerDownEvent) { } else if (event is PointerDownEvent) {
configChanged = true;
_pointerLocations[event.pointer] = event.position; _pointerLocations[event.pointer] = event.position;
} else if (event is PointerUpEvent) { didChangeConfiguration = true;
configChanged = true; shouldStartIfAccepted = true;
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
_pointerLocations.remove(event.pointer); _pointerLocations.remove(event.pointer);
didChangeConfiguration = true;
} }
_update(configChanged, event.pointer); _update();
if (!didChangeConfiguration || _reconfigure(event.pointer))
_advanceStateMachine(shouldStartIfAccepted);
stopTrackingIfPointerNoLongerDown(event); stopTrackingIfPointerNoLongerDown(event);
} }
void _update(bool configChanged, int pointer) { void _update() {
final int count = _pointerLocations.keys.length; final int count = _pointerLocations.keys.length;
// Compute the focal point // Compute the focal point
Point focalPoint = Point.origin; Point focalPoint = Point.origin;
for (int pointer in _pointerLocations.keys) for (int pointer in _pointerLocations.keys)
focalPoint += _pointerLocations[pointer].toOffset(); focalPoint += _pointerLocations[pointer].toOffset();
focalPoint = new Point(focalPoint.x / count, focalPoint.y / count); _currentFocalPoint = count > 0 ? new Point(focalPoint.x / count, focalPoint.y / count) : Point.origin;
// Span is the average deviation from focal point // Span is the average deviation from focal point
double totalDeviation = 0.0; double totalDeviation = 0.0;
for (int pointer in _pointerLocations.keys) for (int pointer in _pointerLocations.keys)
totalDeviation += (focalPoint - _pointerLocations[pointer]).distance; totalDeviation += (_currentFocalPoint - _pointerLocations[pointer]).distance;
_currentSpan = count > 0 ? totalDeviation / count : 0.0; _currentSpan = count > 0 ? totalDeviation / count : 0.0;
}
if (configChanged) { bool _reconfigure(int pointer) {
_initialSpan = _currentSpan; _initialFocalPoint = _currentFocalPoint;
if (_state == ScaleState.started) { _initialSpan = _currentSpan;
if (onEnd != null) { if (_state == _ScaleState.started) {
final VelocityTracker tracker = _velocityTrackers[pointer]; if (onEnd != null) {
assert(tracker != null); final VelocityTracker tracker = _velocityTrackers[pointer];
assert(tracker != null);
Velocity velocity = tracker.getVelocity();
if (velocity != null && _isFlingGesture(velocity)) { Velocity velocity = tracker.getVelocity();
final Offset pixelsPerSecond = velocity.pixelsPerSecond; if (velocity != null && _isFlingGesture(velocity)) {
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) final Offset pixelsPerSecond = velocity.pixelsPerSecond;
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity); if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
invokeCallback<Null>('onEnd', () => onEnd(new ScaleEndDetails(velocity: velocity))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504 velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
} else { invokeCallback<Null>('onEnd', () => onEnd(new ScaleEndDetails(velocity: velocity))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
invokeCallback<Null>('onEnd', () => onEnd(new ScaleEndDetails(velocity: Velocity.zero))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504 } else {
} invokeCallback<Null>('onEnd', () => onEnd(new ScaleEndDetails(velocity: Velocity.zero))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
} }
_state = ScaleState.accepted;
} }
_state = _ScaleState.accepted;
return false;
} }
return true;
}
if (_state == ScaleState.ready) void _advanceStateMachine(bool shouldStartIfAccepted) {
_state = ScaleState.possible; if (_state == _ScaleState.ready)
_state = _ScaleState.possible;
if (_state == ScaleState.possible && if (_state == _ScaleState.possible) {
(_currentSpan - _initialSpan).abs() > kScaleSlop) { final double spanDelta = (_currentSpan - _initialSpan).abs();
final double focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance;
if (spanDelta > kScaleSlop || focalPointDelta > kPanSlop)
resolve(GestureDisposition.accepted);
} else if (_state.index >= _ScaleState.accepted.index) {
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
} }
if (_state == ScaleState.accepted && !configChanged) { if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
_state = ScaleState.started; _state = _ScaleState.started;
if (onStart != null) _dispatchOnStartCallbackIfNeeded();
invokeCallback<Null>('onStart', () => onStart(new ScaleStartDetails(focalPoint: focalPoint))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
} }
if (_state == ScaleState.started && onUpdate != null) if (_state == _ScaleState.started && onUpdate != null)
invokeCallback<Null>('onUpdate', () => onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: focalPoint))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504 invokeCallback<Null>('onUpdate', () => onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: _currentFocalPoint))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
}
void _dispatchOnStartCallbackIfNeeded() {
assert(_state == _ScaleState.started);
if (onStart != null)
invokeCallback<Null>('onStart', () => onStart(new ScaleStartDetails(focalPoint: _currentFocalPoint))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
} }
@override @override
void acceptGesture(int pointer) { void acceptGesture(int pointer) {
if (_state != ScaleState.accepted) { if (_state == _ScaleState.possible) {
_state = ScaleState.accepted; _state = _ScaleState.started;
_update(false, pointer); _dispatchOnStartCallbackIfNeeded();
} }
} }
@override
void rejectGesture(int pointer) {
stopTrackingPointer(pointer);
}
@override @override
void didStopTrackingLastPointer(int pointer) { void didStopTrackingLastPointer(int pointer) {
switch(_state) { switch(_state) {
case ScaleState.possible: case _ScaleState.possible:
resolve(GestureDisposition.rejected); resolve(GestureDisposition.rejected);
break; break;
case ScaleState.ready: case _ScaleState.ready:
assert(false); // We should have not seen a pointer yet assert(false); // We should have not seen a pointer yet
break; break;
case ScaleState.accepted: case _ScaleState.accepted:
break; break;
case ScaleState.started: case _ScaleState.started:
assert(false); // We should be in the accepted state when user is done assert(false); // We should be in the accepted state when user is done
break; break;
} }
_state = ScaleState.ready; _state = _ScaleState.ready;
} }
@override @override
......
...@@ -184,4 +184,81 @@ void main() { ...@@ -184,4 +184,81 @@ void main() {
scale.dispose(); scale.dispose();
tap.dispose(); tap.dispose();
}); });
testGesture('Scale gesture competes with drag', (GestureTester tester) {
final ScaleGestureRecognizer scale = new ScaleGestureRecognizer();
final HorizontalDragGestureRecognizer drag = new 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 = new TestPointer(1);
final PointerDownEvent down = pointer1.down(const Point(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);
tester.route(pointer1.move(const Point(10.0, 30.0)));
expect(log, equals(<String>['scale-start', 'scale-update']));
log.clear();
final TestPointer pointer2 = new TestPointer(2);
final PointerDownEvent down2 = pointer2.down(const Point(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 Point(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 drags.
final TestPointer pointer3 = new TestPointer(3);
final PointerDownEvent down3 = pointer3.down(const Point(30.0, 30.0));
scale.addPointer(down3);
drag.addPointer(down3);
tester.closeArena(3);
tester.route(down3);
expect(log, isEmpty);
tester.route(pointer3.move(const Point(50.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();
});
} }
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