Catch and log any exceptions thrown by an app's gesture recognizer callbacks (#6542)
If a recognizer is interrupted by an exception from a callback, it could be left in an inconsistent state and be unable to process future events
This commit is contained in:
parent
ef386c1547
commit
b41645cc17
@ -182,7 +182,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
_initialPosition = event.position;
|
_initialPosition = event.position;
|
||||||
_pendingDragOffset = Offset.zero;
|
_pendingDragOffset = Offset.zero;
|
||||||
if (onDown != null)
|
if (onDown != null)
|
||||||
onDown(new DragDownDetails(globalPosition: _initialPosition));
|
invokeCallback/*<Null>*/('onDown', () => onDown(new DragDownDetails(globalPosition: _initialPosition)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,11 +196,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
Offset delta = event.delta;
|
Offset delta = event.delta;
|
||||||
if (_state == _DragState.accepted) {
|
if (_state == _DragState.accepted) {
|
||||||
if (onUpdate != null) {
|
if (onUpdate != null) {
|
||||||
onUpdate(new DragUpdateDetails(
|
invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new DragUpdateDetails(
|
||||||
delta: _getDeltaForDetails(delta),
|
delta: _getDeltaForDetails(delta),
|
||||||
primaryDelta: _getPrimaryDeltaForDetails(delta),
|
primaryDelta: _getPrimaryDeltaForDetails(delta),
|
||||||
globalPosition: event.position
|
globalPosition: event.position
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_pendingDragOffset += delta;
|
_pendingDragOffset += delta;
|
||||||
@ -218,12 +218,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
Offset delta = _pendingDragOffset;
|
Offset delta = _pendingDragOffset;
|
||||||
_pendingDragOffset = Offset.zero;
|
_pendingDragOffset = Offset.zero;
|
||||||
if (onStart != null)
|
if (onStart != null)
|
||||||
onStart(new DragStartDetails(globalPosition: _initialPosition));
|
invokeCallback/*<Null>*/('onStart', () => onStart(new DragStartDetails(globalPosition: _initialPosition)));
|
||||||
if (delta != Offset.zero && onUpdate != null) {
|
if (delta != Offset.zero && onUpdate != null) {
|
||||||
onUpdate(new DragUpdateDetails(
|
invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new DragUpdateDetails(
|
||||||
delta: _getDeltaForDetails(delta),
|
delta: _getDeltaForDetails(delta),
|
||||||
primaryDelta: _getPrimaryDeltaForDetails(delta)
|
primaryDelta: _getPrimaryDeltaForDetails(delta)
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,7 +239,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
resolve(GestureDisposition.rejected);
|
resolve(GestureDisposition.rejected);
|
||||||
_state = _DragState.ready;
|
_state = _DragState.ready;
|
||||||
if (onCancel != null)
|
if (onCancel != null)
|
||||||
onCancel();
|
invokeCallback/*<Null>*/('onCancel', onCancel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bool wasAccepted = (_state == _DragState.accepted);
|
bool wasAccepted = (_state == _DragState.accepted);
|
||||||
@ -253,9 +253,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
|
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
|
||||||
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
|
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
|
||||||
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
||||||
onEnd(new DragEndDetails(velocity: velocity));
|
invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails(velocity: velocity)));
|
||||||
} else {
|
} else {
|
||||||
onEnd(new DragEndDetails(velocity: Velocity.zero));
|
invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails(velocity: Velocity.zero)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_velocityTrackers.clear();
|
_velocityTrackers.clear();
|
||||||
|
@ -26,7 +26,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
|||||||
void didExceedDeadline() {
|
void didExceedDeadline() {
|
||||||
resolve(GestureDisposition.accepted);
|
resolve(GestureDisposition.accepted);
|
||||||
if (onLongPress != null)
|
if (onLongPress != null)
|
||||||
onLongPress();
|
invokeCallback/*<Null>*/('onLongPress', onLongPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -255,7 +255,7 @@ abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> exten
|
|||||||
assert(state._pendingDelta != null);
|
assert(state._pendingDelta != null);
|
||||||
Drag drag;
|
Drag drag;
|
||||||
if (onStart != null)
|
if (onStart != null)
|
||||||
drag = onStart(initialPosition);
|
drag = invokeCallback/*<Drag>*/('onStart', () => onStart(initialPosition));
|
||||||
if (drag != null) {
|
if (drag != null) {
|
||||||
state._startDrag(drag);
|
state._startDrag(drag);
|
||||||
} else {
|
} else {
|
||||||
|
@ -191,7 +191,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
|
|||||||
_freezeTracker(tracker);
|
_freezeTracker(tracker);
|
||||||
_trackers.remove(tracker.pointer);
|
_trackers.remove(tracker.pointer);
|
||||||
if (onDoubleTap != null)
|
if (onDoubleTap != null)
|
||||||
onDoubleTap();
|
invokeCallback/*<Null>*/('onDoubleTap', onDoubleTap);
|
||||||
_reset();
|
_reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,7 +359,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
|
|||||||
longTapDelay: longTapDelay
|
longTapDelay: longTapDelay
|
||||||
);
|
);
|
||||||
if (onTapDown != null)
|
if (onTapDown != null)
|
||||||
onTapDown(event.pointer, new TapDownDetails(globalPosition: event.position));
|
invokeCallback/*<Null>*/('onTapDown', () => onTapDown(event.pointer, new TapDownDetails(globalPosition: event.position)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -380,19 +380,19 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
|
|||||||
_gestureMap.remove(pointer);
|
_gestureMap.remove(pointer);
|
||||||
if (resolution == _TapResolution.tap) {
|
if (resolution == _TapResolution.tap) {
|
||||||
if (onTapUp != null)
|
if (onTapUp != null)
|
||||||
onTapUp(pointer, new TapUpDetails(globalPosition: globalPosition));
|
invokeCallback/*<Null>*/('onTapUp', () => onTapUp(pointer, new TapUpDetails(globalPosition: globalPosition)));
|
||||||
if (onTap != null)
|
if (onTap != null)
|
||||||
onTap(pointer);
|
invokeCallback/*<Null>*/('onTap', () => onTap(pointer));
|
||||||
} else {
|
} else {
|
||||||
if (onTapCancel != null)
|
if (onTapCancel != null)
|
||||||
onTapCancel(pointer);
|
invokeCallback/*<Null>*/('onTapCancel', () => onTapCancel(pointer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleLongTap(int pointer, Point lastPosition) {
|
void _handleLongTap(int pointer, Point lastPosition) {
|
||||||
assert(_gestureMap.containsKey(pointer));
|
assert(_gestureMap.containsKey(pointer));
|
||||||
if (onLongTapDown != null)
|
if (onLongTapDown != null)
|
||||||
onLongTapDown(pointer, new TapDownDetails(globalPosition: lastPosition));
|
invokeCallback/*<Null>*/('onLongTapDown', () => onLongTapDown(pointer, new TapDownDetails(globalPosition: lastPosition)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -6,6 +6,7 @@ import 'dart:async';
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:ui' show Point, Offset;
|
import 'dart:ui' show Point, Offset;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import 'arena.dart';
|
import 'arena.dart';
|
||||||
@ -16,6 +17,8 @@ import 'pointer_router.dart';
|
|||||||
|
|
||||||
export 'pointer_router.dart' show PointerRouter;
|
export 'pointer_router.dart' show PointerRouter;
|
||||||
|
|
||||||
|
typedef T RecognizerCallback<T>();
|
||||||
|
|
||||||
/// The base class that all GestureRecognizers should inherit from.
|
/// The base class that all GestureRecognizers should inherit from.
|
||||||
///
|
///
|
||||||
/// Provides a basic API that can be used by classes that work with
|
/// Provides a basic API that can be used by classes that work with
|
||||||
@ -48,6 +51,28 @@ abstract class GestureRecognizer extends GestureArenaMember {
|
|||||||
/// Returns a very short pretty description of the gesture that the
|
/// Returns a very short pretty description of the gesture that the
|
||||||
/// recognizer looks for, like 'tap' or 'horizontal drag'.
|
/// recognizer looks for, like 'tap' or 'horizontal drag'.
|
||||||
String toStringShort() => toString();
|
String toStringShort() => toString();
|
||||||
|
|
||||||
|
/// Invoke a callback provided by the application and log any exceptions.
|
||||||
|
@protected
|
||||||
|
dynamic/*=T*/ invokeCallback/*<T>*/(String name, RecognizerCallback<dynamic/*=T*/> callback) {
|
||||||
|
dynamic/*=T*/ result;
|
||||||
|
try {
|
||||||
|
result = callback();
|
||||||
|
} catch (exception, stack) {
|
||||||
|
FlutterError.reportError(new FlutterErrorDetails(
|
||||||
|
exception: exception,
|
||||||
|
stack: stack,
|
||||||
|
library: 'gesture',
|
||||||
|
context: 'while handling a gesture',
|
||||||
|
informationCollector: (StringBuffer information) {
|
||||||
|
information.writeln('Handler: $name');
|
||||||
|
information.writeln('Recognizer:');
|
||||||
|
information.writeln(' $this');
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Base class for gesture recognizers that can only recognize one
|
/// Base class for gesture recognizers that can only recognize one
|
||||||
|
@ -180,9 +180,9 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
|
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
|
||||||
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
|
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
|
||||||
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
||||||
onEnd(new ScaleEndDetails(velocity: velocity));
|
invokeCallback/*<Null>*/('onEnd', () => onEnd(new ScaleEndDetails(velocity: velocity)));
|
||||||
} else {
|
} else {
|
||||||
onEnd(new ScaleEndDetails(velocity: Velocity.zero));
|
invokeCallback/*<Null>*/('onEnd', () => onEnd(new ScaleEndDetails(velocity: Velocity.zero)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_state = ScaleState.accepted;
|
_state = ScaleState.accepted;
|
||||||
@ -200,11 +200,11 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
if (_state == ScaleState.accepted && !configChanged) {
|
if (_state == ScaleState.accepted && !configChanged) {
|
||||||
_state = ScaleState.started;
|
_state = ScaleState.started;
|
||||||
if (onStart != null)
|
if (onStart != null)
|
||||||
onStart(new ScaleStartDetails(focalPoint: focalPoint));
|
invokeCallback/*<Null>*/('onStart', () => onStart(new ScaleStartDetails(focalPoint: focalPoint)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_state == ScaleState.started && onUpdate != null)
|
if (_state == ScaleState.started && onUpdate != null)
|
||||||
onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: focalPoint));
|
invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: focalPoint)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -99,7 +99,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
|||||||
void resolve(GestureDisposition disposition) {
|
void resolve(GestureDisposition disposition) {
|
||||||
if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
|
if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
|
||||||
if (onTapCancel != null)
|
if (onTapCancel != null)
|
||||||
onTapCancel();
|
invokeCallback/*<Null>*/('onTapCancel', onTapCancel);
|
||||||
_reset();
|
_reset();
|
||||||
}
|
}
|
||||||
super.resolve(disposition);
|
super.resolve(disposition);
|
||||||
@ -126,7 +126,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
|||||||
if (pointer == primaryPointer) {
|
if (pointer == primaryPointer) {
|
||||||
assert(state == GestureRecognizerState.defunct);
|
assert(state == GestureRecognizerState.defunct);
|
||||||
if (onTapCancel != null)
|
if (onTapCancel != null)
|
||||||
onTapCancel();
|
invokeCallback/*<Null>*/('onTapCancel', onTapCancel);
|
||||||
_reset();
|
_reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,7 +134,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
|||||||
void _checkDown() {
|
void _checkDown() {
|
||||||
if (!_sentTapDown) {
|
if (!_sentTapDown) {
|
||||||
if (onTapDown != null)
|
if (onTapDown != null)
|
||||||
onTapDown(new TapDownDetails(globalPosition: initialPosition));
|
invokeCallback/*<Null>*/('onTapDown', () => onTapDown(new TapDownDetails(globalPosition: initialPosition)));
|
||||||
_sentTapDown = true;
|
_sentTapDown = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,9 +143,9 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
|||||||
if (_wonArenaForPrimaryPointer && _finalPosition != null) {
|
if (_wonArenaForPrimaryPointer && _finalPosition != null) {
|
||||||
resolve(GestureDisposition.accepted);
|
resolve(GestureDisposition.accepted);
|
||||||
if (onTapUp != null)
|
if (onTapUp != null)
|
||||||
onTapUp(new TapUpDetails(globalPosition: _finalPosition));
|
invokeCallback/*<Null>*/('onTapUp', () => onTapUp(new TapUpDetails(globalPosition: _finalPosition)));
|
||||||
if (onTap != null)
|
if (onTap != null)
|
||||||
onTap();
|
invokeCallback/*<Null>*/('onTap', onTap);
|
||||||
_reset();
|
_reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
@ -259,4 +260,28 @@ void main() {
|
|||||||
tap.dispose();
|
tap.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testGesture('Should log exceptions from callbacks', (GestureTester tester) {
|
||||||
|
TapGestureRecognizer tap = new TapGestureRecognizer();
|
||||||
|
|
||||||
|
tap.onTap = () {
|
||||||
|
throw new Exception(test);
|
||||||
|
};
|
||||||
|
|
||||||
|
FlutterExceptionHandler previousErrorHandler = FlutterError.onError;
|
||||||
|
bool gotError = false;
|
||||||
|
FlutterError.onError = (FlutterErrorDetails details) {
|
||||||
|
gotError = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
tap.addPointer(down1);
|
||||||
|
tester.closeArena(1);
|
||||||
|
tester.route(down1);
|
||||||
|
expect(gotError, isFalse);
|
||||||
|
|
||||||
|
tester.route(up1);
|
||||||
|
expect(gotError, isTrue);
|
||||||
|
|
||||||
|
FlutterError.onError = previousErrorHandler;
|
||||||
|
tap.dispose();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user