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;
|
||||
_pendingDragOffset = Offset.zero;
|
||||
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;
|
||||
if (_state == _DragState.accepted) {
|
||||
if (onUpdate != null) {
|
||||
onUpdate(new DragUpdateDetails(
|
||||
invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new DragUpdateDetails(
|
||||
delta: _getDeltaForDetails(delta),
|
||||
primaryDelta: _getPrimaryDeltaForDetails(delta),
|
||||
globalPosition: event.position
|
||||
));
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
_pendingDragOffset += delta;
|
||||
@ -218,12 +218,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
Offset delta = _pendingDragOffset;
|
||||
_pendingDragOffset = Offset.zero;
|
||||
if (onStart != null)
|
||||
onStart(new DragStartDetails(globalPosition: _initialPosition));
|
||||
invokeCallback/*<Null>*/('onStart', () => onStart(new DragStartDetails(globalPosition: _initialPosition)));
|
||||
if (delta != Offset.zero && onUpdate != null) {
|
||||
onUpdate(new DragUpdateDetails(
|
||||
invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new DragUpdateDetails(
|
||||
delta: _getDeltaForDetails(delta),
|
||||
primaryDelta: _getPrimaryDeltaForDetails(delta)
|
||||
));
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -239,7 +239,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
resolve(GestureDisposition.rejected);
|
||||
_state = _DragState.ready;
|
||||
if (onCancel != null)
|
||||
onCancel();
|
||||
invokeCallback/*<Null>*/('onCancel', onCancel);
|
||||
return;
|
||||
}
|
||||
bool wasAccepted = (_state == _DragState.accepted);
|
||||
@ -253,9 +253,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
|
||||
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
|
||||
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
||||
onEnd(new DragEndDetails(velocity: velocity));
|
||||
invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails(velocity: velocity)));
|
||||
} else {
|
||||
onEnd(new DragEndDetails(velocity: Velocity.zero));
|
||||
invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails(velocity: Velocity.zero)));
|
||||
}
|
||||
}
|
||||
_velocityTrackers.clear();
|
||||
|
@ -26,7 +26,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
void didExceedDeadline() {
|
||||
resolve(GestureDisposition.accepted);
|
||||
if (onLongPress != null)
|
||||
onLongPress();
|
||||
invokeCallback/*<Null>*/('onLongPress', onLongPress);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -255,7 +255,7 @@ abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> exten
|
||||
assert(state._pendingDelta != null);
|
||||
Drag drag;
|
||||
if (onStart != null)
|
||||
drag = onStart(initialPosition);
|
||||
drag = invokeCallback/*<Drag>*/('onStart', () => onStart(initialPosition));
|
||||
if (drag != null) {
|
||||
state._startDrag(drag);
|
||||
} else {
|
||||
|
@ -191,7 +191,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||
_freezeTracker(tracker);
|
||||
_trackers.remove(tracker.pointer);
|
||||
if (onDoubleTap != null)
|
||||
onDoubleTap();
|
||||
invokeCallback/*<Null>*/('onDoubleTap', onDoubleTap);
|
||||
_reset();
|
||||
}
|
||||
|
||||
@ -359,7 +359,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
|
||||
longTapDelay: longTapDelay
|
||||
);
|
||||
if (onTapDown != null)
|
||||
onTapDown(event.pointer, new TapDownDetails(globalPosition: event.position));
|
||||
invokeCallback/*<Null>*/('onTapDown', () => onTapDown(event.pointer, new TapDownDetails(globalPosition: event.position)));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -380,19 +380,19 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
|
||||
_gestureMap.remove(pointer);
|
||||
if (resolution == _TapResolution.tap) {
|
||||
if (onTapUp != null)
|
||||
onTapUp(pointer, new TapUpDetails(globalPosition: globalPosition));
|
||||
invokeCallback/*<Null>*/('onTapUp', () => onTapUp(pointer, new TapUpDetails(globalPosition: globalPosition)));
|
||||
if (onTap != null)
|
||||
onTap(pointer);
|
||||
invokeCallback/*<Null>*/('onTap', () => onTap(pointer));
|
||||
} else {
|
||||
if (onTapCancel != null)
|
||||
onTapCancel(pointer);
|
||||
invokeCallback/*<Null>*/('onTapCancel', () => onTapCancel(pointer));
|
||||
}
|
||||
}
|
||||
|
||||
void _handleLongTap(int pointer, Point lastPosition) {
|
||||
assert(_gestureMap.containsKey(pointer));
|
||||
if (onLongTapDown != null)
|
||||
onLongTapDown(pointer, new TapDownDetails(globalPosition: lastPosition));
|
||||
invokeCallback/*<Null>*/('onLongTapDown', () => onLongTapDown(pointer, new TapDownDetails(globalPosition: lastPosition)));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -6,6 +6,7 @@ import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:ui' show Point, Offset;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'arena.dart';
|
||||
@ -16,6 +17,8 @@ import 'pointer_router.dart';
|
||||
|
||||
export 'pointer_router.dart' show PointerRouter;
|
||||
|
||||
typedef T RecognizerCallback<T>();
|
||||
|
||||
/// The base class that all GestureRecognizers should inherit from.
|
||||
///
|
||||
/// 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
|
||||
/// recognizer looks for, like 'tap' or 'horizontal drag'.
|
||||
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
|
||||
|
@ -180,9 +180,9 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
|
||||
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
|
||||
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
||||
onEnd(new ScaleEndDetails(velocity: velocity));
|
||||
invokeCallback/*<Null>*/('onEnd', () => onEnd(new ScaleEndDetails(velocity: velocity)));
|
||||
} else {
|
||||
onEnd(new ScaleEndDetails(velocity: Velocity.zero));
|
||||
invokeCallback/*<Null>*/('onEnd', () => onEnd(new ScaleEndDetails(velocity: Velocity.zero)));
|
||||
}
|
||||
}
|
||||
_state = ScaleState.accepted;
|
||||
@ -200,11 +200,11 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
if (_state == ScaleState.accepted && !configChanged) {
|
||||
_state = ScaleState.started;
|
||||
if (onStart != null)
|
||||
onStart(new ScaleStartDetails(focalPoint: focalPoint));
|
||||
invokeCallback/*<Null>*/('onStart', () => onStart(new ScaleStartDetails(focalPoint: focalPoint)));
|
||||
}
|
||||
|
||||
if (_state == ScaleState.started && onUpdate != null)
|
||||
onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: focalPoint));
|
||||
invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: focalPoint)));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -99,7 +99,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
void resolve(GestureDisposition disposition) {
|
||||
if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
|
||||
if (onTapCancel != null)
|
||||
onTapCancel();
|
||||
invokeCallback/*<Null>*/('onTapCancel', onTapCancel);
|
||||
_reset();
|
||||
}
|
||||
super.resolve(disposition);
|
||||
@ -126,7 +126,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
if (pointer == primaryPointer) {
|
||||
assert(state == GestureRecognizerState.defunct);
|
||||
if (onTapCancel != null)
|
||||
onTapCancel();
|
||||
invokeCallback/*<Null>*/('onTapCancel', onTapCancel);
|
||||
_reset();
|
||||
}
|
||||
}
|
||||
@ -134,7 +134,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
void _checkDown() {
|
||||
if (!_sentTapDown) {
|
||||
if (onTapDown != null)
|
||||
onTapDown(new TapDownDetails(globalPosition: initialPosition));
|
||||
invokeCallback/*<Null>*/('onTapDown', () => onTapDown(new TapDownDetails(globalPosition: initialPosition)));
|
||||
_sentTapDown = true;
|
||||
}
|
||||
}
|
||||
@ -143,9 +143,9 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
if (_wonArenaForPrimaryPointer && _finalPosition != null) {
|
||||
resolve(GestureDisposition.accepted);
|
||||
if (onTapUp != null)
|
||||
onTapUp(new TapUpDetails(globalPosition: _finalPosition));
|
||||
invokeCallback/*<Null>*/('onTapUp', () => onTapUp(new TapUpDetails(globalPosition: _finalPosition)));
|
||||
if (onTap != null)
|
||||
onTap();
|
||||
invokeCallback/*<Null>*/('onTap', onTap);
|
||||
_reset();
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
@ -259,4 +260,28 @@ void main() {
|
||||
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