Commit 87445e59 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Increase the touch slop. (#11419)

It was 8.0. It's now arbitrarily 18.0.

Changing this required adjusting some tests. Adjusting the tests
required debugging the tests. Debugging the tests required some tools
to help debugging gesture recognizers and gesture arenas, so I added
some. It also required updating some toString() methods which resulted
in some changes to the tree diagnostics logic.

Also I cleaned up some docs while I was at it.
parent 990dae85
...@@ -22,6 +22,7 @@ export 'package:meta/meta.dart' show ...@@ -22,6 +22,7 @@ export 'package:meta/meta.dart' show
// bool _first; // bool _first;
// bool _lights; // bool _lights;
// bool _visible; // bool _visible;
// bool inherit;
// class Cat { } // class Cat { }
// double _volume; // double _volume;
// dynamic _calculation; // dynamic _calculation;
......
...@@ -430,7 +430,7 @@ class _CupertinoEdgeShadowDecoration extends Decoration { ...@@ -430,7 +430,7 @@ class _CupertinoEdgeShadowDecoration extends Decoration {
}) { }) {
return new DiagnosticsNode.lazy( return new DiagnosticsNode.lazy(
name: name, name: name,
object: this, value: this,
style: style, style: style,
description: '$runtimeType', description: '$runtimeType',
fillProperties: (List<DiagnosticsNode> properties) { fillProperties: (List<DiagnosticsNode> properties) {
......
...@@ -4,6 +4,10 @@ ...@@ -4,6 +4,10 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'debug.dart';
/// Whether the gesture was accepted or rejected. /// Whether the gesture was accepted or rejected.
enum GestureDisposition { enum GestureDisposition {
/// This gesture was accepted as the interpretation of the user's input. /// This gesture was accepted as the interpretation of the user's input.
...@@ -42,7 +46,8 @@ class GestureArenaEntry { ...@@ -42,7 +46,8 @@ class GestureArenaEntry {
/// Call this member to claim victory (with accepted) or admit defeat (with rejected). /// Call this member to claim victory (with accepted) or admit defeat (with rejected).
/// ///
/// It's fine to attempt to resolve an arena that is already resolved. /// It's fine to attempt to resolve a gesture recognizer for an arena that is
/// already resolved.
void resolve(GestureDisposition disposition) { void resolve(GestureDisposition disposition) {
_arena._resolve(_pointer, _member, disposition); _arena._resolve(_pointer, _member, disposition);
} }
...@@ -64,19 +69,47 @@ class _GestureArena { ...@@ -64,19 +69,47 @@ class _GestureArena {
assert(isOpen); assert(isOpen);
members.add(member); members.add(member);
} }
@override
String toString() {
final StringBuffer buffer = new StringBuffer();
if (members.isEmpty) {
buffer.write('<empty>');
} else {
buffer.write(members.map<String>((GestureArenaMember member) {
if (member == eagerWinner)
return '$member (eager winner)';
return '$member';
}).join(', '));
}
if (isOpen)
buffer.write(' [open]');
if (isHeld)
buffer.write(' [held]');
if (hasPendingSweep)
buffer.write(' [hasPendingSweep]');
return buffer.toString();
}
} }
/// The first member to accept or the last member to not to reject wins. /// The first member to accept or the last member to not to reject wins.
/// ///
/// See [https://flutter.io/gestures/#gesture-disambiguation] for more /// See [https://flutter.io/gestures/#gesture-disambiguation] for more
/// information about the role this class plays in the gesture system. /// information about the role this class plays in the gesture system.
///
/// To debug problems with gestures, consider using
/// [debugPrintGestureArenaDiagnostics].
class GestureArenaManager { class GestureArenaManager {
final Map<int, _GestureArena> _arenas = <int, _GestureArena>{}; final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
/// Adds a new member (e.g., gesture recognizer) to the arena. /// Adds a new member (e.g., gesture recognizer) to the arena.
GestureArenaEntry add(int pointer, GestureArenaMember member) { GestureArenaEntry add(int pointer, GestureArenaMember member) {
final _GestureArena state = _arenas.putIfAbsent(pointer, () => new _GestureArena()); final _GestureArena state = _arenas.putIfAbsent(pointer, () {
assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
return new _GestureArena();
});
state.add(member); state.add(member);
assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
return new GestureArenaEntry._(this, pointer, member); return new GestureArenaEntry._(this, pointer, member);
} }
...@@ -88,6 +121,7 @@ class GestureArenaManager { ...@@ -88,6 +121,7 @@ class GestureArenaManager {
if (state == null) if (state == null)
return; // This arena either never existed or has been resolved. return; // This arena either never existed or has been resolved.
state.isOpen = false; state.isOpen = false;
assert(_debugLogDiagnostic(pointer, 'Closing', state));
_tryToResolveArena(pointer, state); _tryToResolveArena(pointer, state);
} }
...@@ -111,13 +145,16 @@ class GestureArenaManager { ...@@ -111,13 +145,16 @@ class GestureArenaManager {
assert(!state.isOpen); assert(!state.isOpen);
if (state.isHeld) { if (state.isHeld) {
state.hasPendingSweep = true; state.hasPendingSweep = true;
return; // This arena is being held for a long-lived member assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
return; // This arena is being held for a long-lived member.
} }
assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
_arenas.remove(pointer); _arenas.remove(pointer);
if (state.members.isNotEmpty) { if (state.members.isNotEmpty) {
// First member wins // First member wins.
assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
state.members.first.acceptGesture(pointer); state.members.first.acceptGesture(pointer);
// Give all the other members the bad news // Give all the other members the bad news.
for (int i = 1; i < state.members.length; i++) for (int i = 1; i < state.members.length; i++)
state.members[i].rejectGesture(pointer); state.members[i].rejectGesture(pointer);
} }
...@@ -140,6 +177,7 @@ class GestureArenaManager { ...@@ -140,6 +177,7 @@ class GestureArenaManager {
if (state == null) if (state == null)
return; // This arena either never existed or has been resolved. return; // This arena either never existed or has been resolved.
state.isHeld = true; state.isHeld = true;
assert(_debugLogDiagnostic(pointer, 'Holding', state));
} }
/// Releases a hold, allowing the arena to be swept. /// Releases a hold, allowing the arena to be swept.
...@@ -156,37 +194,19 @@ class GestureArenaManager { ...@@ -156,37 +194,19 @@ class GestureArenaManager {
if (state == null) if (state == null)
return; // This arena either never existed or has been resolved. return; // This arena either never existed or has been resolved.
state.isHeld = false; state.isHeld = false;
assert(_debugLogDiagnostic(pointer, 'Releasing', state));
if (state.hasPendingSweep) if (state.hasPendingSweep)
sweep(pointer); sweep(pointer);
} }
void _resolveByDefault(int pointer, _GestureArena state) { /// Reject or accept a gesture recognizer.
if (!_arenas.containsKey(pointer)) ///
return; // Already resolved earlier. /// This is called by calling [GestureArenaEntry.resolve] on the object returned from [add].
assert(_arenas[pointer] == state);
assert(!state.isOpen);
final List<GestureArenaMember> members = state.members;
assert(members.length == 1);
_arenas.remove(pointer);
state.members.first.acceptGesture(pointer);
}
void _tryToResolveArena(int pointer, _GestureArena state) {
assert(_arenas[pointer] == state);
assert(!state.isOpen);
if (state.members.length == 1) {
scheduleMicrotask(() => _resolveByDefault(pointer, state));
} else if (state.members.isEmpty) {
_arenas.remove(pointer);
} else if (state.eagerWinner != null) {
_resolveInFavorOf(pointer, state, state.eagerWinner);
}
}
void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) { void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
final _GestureArena state = _arenas[pointer]; final _GestureArena state = _arenas[pointer];
if (state == null) if (state == null)
return; // This arena has already resolved. return; // This arena has already resolved.
assert(_debugLogDiagnostic(pointer, '${ disposition == GestureDisposition.accepted ? "Accepting" : "Rejecting" }: $member'));
assert(state.members.contains(member)); assert(state.members.contains(member));
if (disposition == GestureDisposition.rejected) { if (disposition == GestureDisposition.rejected) {
state.members.remove(member); state.members.remove(member);
...@@ -198,11 +218,38 @@ class GestureArenaManager { ...@@ -198,11 +218,38 @@ class GestureArenaManager {
if (state.isOpen) { if (state.isOpen) {
state.eagerWinner ??= member; state.eagerWinner ??= member;
} else { } else {
assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
_resolveInFavorOf(pointer, state, member); _resolveInFavorOf(pointer, state, member);
} }
} }
} }
void _tryToResolveArena(int pointer, _GestureArena state) {
assert(_arenas[pointer] == state);
assert(!state.isOpen);
if (state.members.length == 1) {
scheduleMicrotask(() => _resolveByDefault(pointer, state));
} else if (state.members.isEmpty) {
_arenas.remove(pointer);
assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
} else if (state.eagerWinner != null) {
assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
_resolveInFavorOf(pointer, state, state.eagerWinner);
}
}
void _resolveByDefault(int pointer, _GestureArena state) {
if (!_arenas.containsKey(pointer))
return; // Already resolved earlier.
assert(_arenas[pointer] == state);
assert(!state.isOpen);
final List<GestureArenaMember> members = state.members;
assert(members.length == 1);
_arenas.remove(pointer);
assert(_debugLogDiagnostic(pointer, 'Default winner: ${state.members.first}'));
state.members.first.acceptGesture(pointer);
}
void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) { void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
assert(state == _arenas[pointer]); assert(state == _arenas[pointer]);
assert(state != null); assert(state != null);
...@@ -215,4 +262,16 @@ class GestureArenaManager { ...@@ -215,4 +262,16 @@ class GestureArenaManager {
} }
member.acceptGesture(pointer); member.acceptGesture(pointer);
} }
bool _debugLogDiagnostic(int pointer, String message, [ _GestureArena state ]) {
assert(() {
if (debugPrintGestureArenaDiagnostics) {
final int count = state != null ? state.members.length : null;
final String s = count != 1 ? 's' : '';
debugPrint('Gesture arena ${pointer.toString().padRight(4)}$message${ count != null ? " with $count member$s." : ""}');
}
return true;
});
return true;
}
} }
...@@ -50,23 +50,27 @@ const double kDoubleTapSlop = 100.0; // Logical pixels ...@@ -50,23 +50,27 @@ const double kDoubleTapSlop = 100.0; // Logical pixels
/// displayed on the screen, from the moment they were last requested. /// displayed on the screen, from the moment they were last requested.
const Duration kZoomControlsTimeout = const Duration(milliseconds: 3000); const Duration kZoomControlsTimeout = const Duration(milliseconds: 3000);
/// The distance a touch has to travel for us to be confident that the gesture /// The distance a touch has to travel for the framework to be confident that
/// is a scroll gesture. /// the gesture is a scroll gesture, or, inversely, the maximum distance that a
const double kTouchSlop = 8.0; // Logical pixels /// touch can travel before the framework becomes confident that it is not a
/// tap.
/// The distance a touch has to travel for us to be confident that the gesture // This value was empirically derived. We started at 8.0 and increased it to
/// is a paging gesture. (Currently not used, because paging uses a regular drag // 18.0 after getting complaints that it was too difficult to hit targets.
/// gesture, which uses kTouchSlop.) const double kTouchSlop = 18.0; // Logical pixels
/// The distance a touch has to travel for the framework to be confident that
/// the gesture is a paging gesture. (Currently not used, because paging uses a
/// regular drag gesture, which uses kTouchSlop.)
// TODO(ianh): Create variants of HorizontalDragGestureRecognizer et al for // TODO(ianh): Create variants of HorizontalDragGestureRecognizer et al for
// paging, which use this constant. // paging, which use this constant.
const double kPagingTouchSlop = kTouchSlop * 2.0; // Logical pixels const double kPagingTouchSlop = kTouchSlop * 2.0; // Logical pixels
/// The distance a touch has to travel for us to be confident that the gesture /// The distance a touch has to travel for the framework to be confident that
/// is a panning gesture. /// the gesture is a panning gesture.
const double kPanSlop = kTouchSlop * 2.0; // Logical pixels const double kPanSlop = kTouchSlop * 2.0; // Logical pixels
/// The distance a touch has to travel for us to be confident that the gesture /// The distance a touch has to travel for the framework to be confident that
/// is a scale gesture. /// the gesture is a scale gesture.
const double kScaleSlop = kTouchSlop; // Logical pixels const double kScaleSlop = kTouchSlop; // Logical pixels
/// The margin around a dialog, popup menu, or other window-like widget inside /// The margin around a dialog, popup menu, or other window-like widget inside
......
...@@ -15,6 +15,31 @@ import 'package:flutter/foundation.dart'; ...@@ -15,6 +15,31 @@ import 'package:flutter/foundation.dart';
/// This has no effect in release builds. /// This has no effect in release builds.
bool debugPrintHitTestResults = false; bool debugPrintHitTestResults = false;
/// Prints information about gesture recognizers and gesture arenas.
///
/// This flag only has an effect in debug mode.
///
/// See also:
///
/// * [GestureArenaManager], the class that manages gesture arenas.
/// * [debugPrintRecognizerCallbacksTrace], for debugging issues with
/// gesture recognizers.
bool debugPrintGestureArenaDiagnostics = false;
/// Logs a message every time a gesture recognizer callback is invoked.
///
/// This flag only has an effect in debug mode.
///
/// This is specifically used by [GestureRecognizer.invokeCallback]. Gesture
/// recognizers that do not use this method to invoke callbacks may not honor
/// the [debugPrintRecognizerCallbacksTrace] flag.
///
/// See also:
///
/// * [debugPrintGestureArenaDiagnostics], for debugging issues with gesture
/// arenas.
bool debugPrintRecognizerCallbacksTrace = false;
/// Returns true if none of the gestures library debug variables have been changed. /// Returns true if none of the gestures library debug variables have been changed.
/// ///
/// This function is used by the test framework to ensure that debug variables /// This function is used by the test framework to ensure that debug variables
...@@ -24,7 +49,9 @@ bool debugPrintHitTestResults = false; ...@@ -24,7 +49,9 @@ bool debugPrintHitTestResults = false;
/// a complete list. /// a complete list.
bool debugAssertAllGesturesVarsUnset(String reason) { bool debugAssertAllGesturesVarsUnset(String reason) {
assert(() { assert(() {
if (debugPrintHitTestResults) if (debugPrintHitTestResults ||
debugPrintGestureArenaDiagnostics ||
debugPrintRecognizerCallbacksTrace)
throw new FlutterError(reason); throw new FlutterError(reason);
return true; return true;
}); });
......
...@@ -17,7 +17,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -17,7 +17,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// Creates a long-press gesture recognizer. /// Creates a long-press gesture recognizer.
/// ///
/// Consider assigning the [onLongPress] callback after creating this object. /// Consider assigning the [onLongPress] callback after creating this object.
LongPressGestureRecognizer() : super(deadline: kLongPressTimeout); LongPressGestureRecognizer({ Object debugOwner }) : super(deadline: kLongPressTimeout, debugOwner: debugOwner);
/// Called when a long-press is recongized. /// Called when a long-press is recongized.
GestureLongPressCallback onLongPress; GestureLongPressCallback onLongPress;
......
...@@ -43,10 +43,13 @@ typedef void GestureDragCancelCallback(); ...@@ -43,10 +43,13 @@ typedef void GestureDragCancelCallback();
/// ///
/// See also: /// See also:
/// ///
/// * [HorizontalDragGestureRecognizer] /// * [HorizontalDragGestureRecognizer], for left and right drags.
/// * [VerticalDragGestureRecognizer] /// * [VerticalDragGestureRecognizer], for up and down drags.
/// * [PanGestureRecognizer] /// * [PanGestureRecognizer], for drags that are not locked to a single axis.
abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// Initialize the object.
DragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
/// A pointer has contacted the screen and might begin to move. /// A pointer has contacted the screen and might begin to move.
/// ///
/// The position of the pointer is provided in the callback's `details` /// The position of the pointer is provided in the callback's `details`
...@@ -194,12 +197,18 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -194,12 +197,18 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
invokeCallback<Null>('onEnd', () => onEnd(new DragEndDetails( // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504 invokeCallback<Null>('onEnd', () => onEnd(new DragEndDetails( // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
velocity: velocity, velocity: velocity,
primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond), primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond),
))); )), debugReport: () {
return '$estimate; fling at $velocity.';
});
} else { } else {
invokeCallback<Null>('onEnd', () => onEnd(new DragEndDetails( // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504 invokeCallback<Null>('onEnd', () => onEnd(new DragEndDetails( // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
velocity: Velocity.zero, velocity: Velocity.zero,
primaryVelocity: 0.0, primaryVelocity: 0.0,
))); )), debugReport: () {
if (estimate == null)
return 'Could not estimate velocity.';
return '$estimate; judged to not be a fling.';
});
} }
} }
_velocityTrackers.clear(); _velocityTrackers.clear();
...@@ -218,8 +227,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -218,8 +227,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// ///
/// See also: /// See also:
/// ///
/// * [VerticalMultiDragGestureRecognizer] /// * [HorizontalDragGestureRecognizer], for a similar recognizer but for
/// horizontal movement.
/// * [MultiDragGestureRecognizer], for a family of gesture recognizers that
/// track each touch point independently.
class VerticalDragGestureRecognizer extends DragGestureRecognizer { class VerticalDragGestureRecognizer extends DragGestureRecognizer {
/// Create a gesture recognizer for interactions in the vertical axis.
VerticalDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override @override
bool _isFlingGesture(VelocityEstimate estimate) { bool _isFlingGesture(VelocityEstimate estimate) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity; final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
...@@ -246,8 +261,14 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer { ...@@ -246,8 +261,14 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
/// ///
/// See also: /// See also:
/// ///
/// * [HorizontalMultiDragGestureRecognizer] /// * [VerticalDragGestureRecognizer], for a similar recognizer but for
/// vertical movement.
/// * [MultiDragGestureRecognizer], for a family of gesture recognizers that
/// track each touch point independently.
class HorizontalDragGestureRecognizer extends DragGestureRecognizer { class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
/// Create a gesture recognizer for interactions in the horizontal axis.
HorizontalDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override @override
bool _isFlingGesture(VelocityEstimate estimate) { bool _isFlingGesture(VelocityEstimate estimate) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity; final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
...@@ -272,15 +293,21 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer { ...@@ -272,15 +293,21 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
/// ///
/// See also: /// See also:
/// ///
/// * [ImmediateMultiDragGestureRecognizer] /// * [ImmediateMultiDragGestureRecognizer], for a similar recognizer that
/// * [DelayedMultiDragGestureRecognizer] /// tracks each touch point independently.
/// * [DelayedMultiDragGestureRecognizer], for a similar recognizer that
/// tracks each touch point independently, but that doesn't start until
/// some time has passed.
class PanGestureRecognizer extends DragGestureRecognizer { class PanGestureRecognizer extends DragGestureRecognizer {
/// Create a gesture recognizer for tracking movement on a plane.
PanGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override @override
bool _isFlingGesture(VelocityEstimate estimate) { bool _isFlingGesture(VelocityEstimate estimate) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity; final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
final double minDistance = minFlingDistance ?? kTouchSlop; final double minDistance = minFlingDistance ?? kTouchSlop;
return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity && return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity
estimate.offset.distanceSquared > minDistance * minDistance; && estimate.offset.distanceSquared > minDistance * minDistance;
} }
@override @override
......
...@@ -169,11 +169,18 @@ abstract class MultiDragPointerState { ...@@ -169,11 +169,18 @@ abstract class MultiDragPointerState {
/// ///
/// See also: /// See also:
/// ///
/// * [HorizontalMultiDragGestureRecognizer] /// * [ImmediateMultiDragGestureRecognizer], the most straight-forward variant
/// * [VerticalMultiDragGestureRecognizer] /// of multi-pointer drag gesture recognizer.
/// * [ImmediateMultiDragGestureRecognizer] /// * [HorizontalMultiDragGestureRecognizer], which only recognizes drags that
/// * [DelayedMultiDragGestureRecognizer] /// start horizontally.
/// * [VerticalMultiDragGestureRecognizer], which only recognizes drags that
/// start vertically.
/// * [DelayedMultiDragGestureRecognizer], which only recognizes drags that
/// start after a long-press gesture.
abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer { abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer {
/// Initialize the object.
MultiDragGestureRecognizer({ @required Object debugOwner }) : super(debugOwner: debugOwner);
/// Called when this class recognizes the start of a drag gesture. /// Called when this class recognizes the start of a drag gesture.
/// ///
/// The remaining notifications for this drag gesture are delivered to the /// The remaining notifications for this drag gesture are delivered to the
...@@ -308,9 +315,18 @@ class _ImmediatePointerState extends MultiDragPointerState { ...@@ -308,9 +315,18 @@ class _ImmediatePointerState extends MultiDragPointerState {
/// ///
/// See also: /// See also:
/// ///
/// * [PanGestureRecognizer] /// * [PanGestureRecognizer], which recognizes only one drag gesture at a time,
/// * [DelayedMultiDragGestureRecognizer] /// regardless of how many fingers are involved.
/// * [HorizontalMultiDragGestureRecognizer], which only recognizes drags that
/// start horizontally.
/// * [VerticalMultiDragGestureRecognizer], which only recognizes drags that
/// start vertically.
/// * [DelayedMultiDragGestureRecognizer], which only recognizes drags that
/// start after a long-press gesture.
class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> { class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> {
/// Create a gesture recognizer for tracking multiple pointers at once.
ImmediateMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override @override
_ImmediatePointerState createNewPointerState(PointerDownEvent event) { _ImmediatePointerState createNewPointerState(PointerDownEvent event) {
return new _ImmediatePointerState(event.position); return new _ImmediatePointerState(event.position);
...@@ -346,8 +362,17 @@ class _HorizontalPointerState extends MultiDragPointerState { ...@@ -346,8 +362,17 @@ class _HorizontalPointerState extends MultiDragPointerState {
/// ///
/// See also: /// See also:
/// ///
/// * [HorizontalDragGestureRecognizer] /// * [HorizontalDragGestureRecognizer], a gesture recognizer that just
/// looks at horizontal movement.
/// * [ImmediateMultiDragGestureRecognizer], a similar recognizer, but without
/// the limitation that the drag must start horizontally.
/// * [VerticalMultiDragGestureRecognizer], which only recognizes drags that
/// start vertically.
class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> { class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> {
/// Create a gesture recognizer for tracking multiple pointers at once
/// but only if they first move horizontally.
HorizontalMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override @override
_HorizontalPointerState createNewPointerState(PointerDownEvent event) { _HorizontalPointerState createNewPointerState(PointerDownEvent event) {
return new _HorizontalPointerState(event.position); return new _HorizontalPointerState(event.position);
...@@ -383,8 +408,17 @@ class _VerticalPointerState extends MultiDragPointerState { ...@@ -383,8 +408,17 @@ class _VerticalPointerState extends MultiDragPointerState {
/// ///
/// See also: /// See also:
/// ///
/// * [VerticalDragGestureRecognizer] /// * [VerticalDragGestureRecognizer], a gesture recognizer that just
/// looks at vertical movement.
/// * [ImmediateMultiDragGestureRecognizer], a similar recognizer, but without
/// the limitation that the drag must start vertically.
/// * [HorizontalMultiDragGestureRecognizer], which only recognizes drags that
/// start horizontally.
class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> { class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> {
/// Create a gesture recognizer for tracking multiple pointers at once
/// but only if they first move vertically.
VerticalMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override @override
_VerticalPointerState createNewPointerState(PointerDownEvent event) { _VerticalPointerState createNewPointerState(PointerDownEvent event) {
return new _VerticalPointerState(event.position); return new _VerticalPointerState(event.position);
...@@ -457,7 +491,8 @@ class _DelayedPointerState extends MultiDragPointerState { ...@@ -457,7 +491,8 @@ class _DelayedPointerState extends MultiDragPointerState {
} }
} }
/// Recognizes movement both horizontally and vertically on a per-pointer basis after a delay. /// Recognizes movement both horizontally and vertically on a per-pointer basis
/// after a delay.
/// ///
/// In constrast to [ImmediateMultiDragGestureRecognizer], /// In constrast to [ImmediateMultiDragGestureRecognizer],
/// [DelayedMultiDragGestureRecognizer] waits for a [delay] before recognizing /// [DelayedMultiDragGestureRecognizer] waits for a [delay] before recognizing
...@@ -470,8 +505,10 @@ class _DelayedPointerState extends MultiDragPointerState { ...@@ -470,8 +505,10 @@ class _DelayedPointerState extends MultiDragPointerState {
/// ///
/// See also: /// See also:
/// ///
/// * [PanGestureRecognizer] /// * [ImmediateMultiDragGestureRecognizer], a similar recognizer but without
/// * [ImmediateMultiDragGestureRecognizer] /// the delay.
/// * [PanGestureRecognizer], which recognizes only one drag gesture at a time,
/// regardless of how many fingers are involved.
class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_DelayedPointerState> { class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_DelayedPointerState> {
/// Creates a drag recognizer that works on a per-pointer basis after a delay. /// Creates a drag recognizer that works on a per-pointer basis after a delay.
/// ///
...@@ -480,8 +517,10 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Dela ...@@ -480,8 +517,10 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Dela
/// defaults to [kLongPressTimeout] to match [LongPressGestureRecognizer] but /// defaults to [kLongPressTimeout] to match [LongPressGestureRecognizer] but
/// can be changed for specific behaviors. /// can be changed for specific behaviors.
DelayedMultiDragGestureRecognizer({ DelayedMultiDragGestureRecognizer({
this.delay: kLongPressTimeout this.delay: kLongPressTimeout,
}) : assert(delay != null); Object debugOwner,
}) : assert(delay != null),
super(debugOwner: debugOwner);
/// The amount of time the pointer must remain in the same place for the drag /// The amount of time the pointer must remain in the same place for the drag
/// to be recognized. /// to be recognized.
......
...@@ -68,6 +68,9 @@ class _TapTracker { ...@@ -68,6 +68,9 @@ class _TapTracker {
/// Recognizes when the user has tapped the screen at the same location twice in /// Recognizes when the user has tapped the screen at the same location twice in
/// quick succession. /// quick succession.
class DoubleTapGestureRecognizer extends GestureRecognizer { class DoubleTapGestureRecognizer extends GestureRecognizer {
/// Create a gesture recognizer for double taps.
DoubleTapGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
// Implementation notes: // Implementation notes:
// The double tap recognizer can be in one of four states. There's no // The double tap recognizer can be in one of four states. There's no
// explicit enum for the states, because they are already captured by // explicit enum for the states, because they are already captured by
...@@ -315,8 +318,9 @@ class MultiTapGestureRecognizer extends GestureRecognizer { ...@@ -315,8 +318,9 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
/// The [longTapDelay] defaults to [Duration.ZERO], which means /// The [longTapDelay] defaults to [Duration.ZERO], which means
/// [onLongTapDown] is called immediately after [onTapDown]. /// [onLongTapDown] is called immediately after [onTapDown].
MultiTapGestureRecognizer({ MultiTapGestureRecognizer({
this.longTapDelay: Duration.ZERO this.longTapDelay: Duration.ZERO,
}); Object debugOwner,
}) : super(debugOwner: debugOwner);
/// A pointer that might cause a tap has contacted the screen at a particular /// A pointer that might cause a tap has contacted the screen at a particular
/// location. /// location.
......
...@@ -11,6 +11,7 @@ import 'package:flutter/foundation.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter/foundation.dart';
import 'arena.dart'; import 'arena.dart';
import 'binding.dart'; import 'binding.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart';
import 'events.dart'; import 'events.dart';
import 'pointer_router.dart'; import 'pointer_router.dart';
import 'team.dart'; import 'team.dart';
...@@ -32,7 +33,21 @@ typedef T RecognizerCallback<T>(); ...@@ -32,7 +33,21 @@ typedef T RecognizerCallback<T>();
/// See also: /// See also:
/// ///
/// * [GestureDetector], the widget that is used to detect gestures. /// * [GestureDetector], the widget that is used to detect gestures.
abstract class GestureRecognizer extends GestureArenaMember { /// * [debugPrintRecognizerCallbacksTrace], a flag that can be set to help
/// debug issues with gesture recognizers.
abstract class GestureRecognizer extends GestureArenaMember with TreeDiagnosticsMixin {
/// Initializes the gesture recognizer.
///
/// The argument is optional and is only used for debug purposes (e.g. in the
/// [toString] serialization).
GestureRecognizer({ this.debugOwner });
/// The recognizer's owner.
///
/// This is used in the [toString] serialization to report the object for which
/// this gesture recognizer was created, to aid in debugging.
final Object debugOwner;
/// Registers a new pointer that might be relevant to this gesture /// Registers a new pointer that might be relevant to this gesture
/// detector. /// detector.
/// ///
...@@ -58,16 +73,32 @@ abstract class GestureRecognizer extends GestureArenaMember { ...@@ -58,16 +73,32 @@ 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();
/// Invoke a callback provided by the application, catching and logging any /// Invoke a callback provided by the application, catching and logging any
/// exceptions. /// exceptions.
/// ///
/// The `name` argument is ignored except when reporting exceptions. /// The `name` argument is ignored except when reporting exceptions.
///
/// The `debugReport` argument is optional and is used when
/// [debugPrintRecognizerCallbacksTrace] is true. If specified, it must be a
/// callback that returns a string describing useful debugging information,
/// e.g. the arguments passed to the callback.
@protected @protected
T invokeCallback<T>(String name, RecognizerCallback<T> callback) { T invokeCallback<T>(String name, RecognizerCallback<T> callback, { String debugReport() }) {
assert(callback != null);
T result; T result;
try { try {
assert(() {
if (debugPrintRecognizerCallbacksTrace) {
final String report = debugReport != null ? debugReport() : null;
// The 19 in the line below is the width of the prefix used by
// _debugLogDiagnostic in arena.dart.
final String prefix = debugPrintGestureArenaDiagnostics ? ' ' * 19 + '❙ ' : '';
debugPrint('$prefix$this calling $name callback.${ report?.isNotEmpty == true ? " $report" : "" }');
}
return true;
});
result = callback(); result = callback();
} catch (exception, stack) { } catch (exception, stack) {
FlutterError.reportError(new FlutterErrorDetails( FlutterError.reportError(new FlutterErrorDetails(
...@@ -86,7 +117,21 @@ abstract class GestureRecognizer extends GestureArenaMember { ...@@ -86,7 +117,21 @@ abstract class GestureRecognizer extends GestureArenaMember {
} }
@override @override
String toString() => describeIdentity(this); void debugFillProperties(List<DiagnosticsNode> description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<Object>('debugOwner', debugOwner, defaultValue: null));
}
@override
String toString() {
final String name = describeIdentity(this);
List<DiagnosticsNode> data = <DiagnosticsNode>[];
debugFillProperties(data);
data = data.where((DiagnosticsNode n) => !n.hidden).toList();
if (data.isEmpty)
return '$name';
return '$name(${data.join("; ")})';
}
} }
/// Base class for gesture recognizers that can only recognize one /// Base class for gesture recognizers that can only recognize one
...@@ -98,6 +143,9 @@ abstract class GestureRecognizer extends GestureArenaMember { ...@@ -98,6 +143,9 @@ abstract class GestureRecognizer extends GestureArenaMember {
/// which manages each pointer independently and can consider multiple /// which manages each pointer independently and can consider multiple
/// simultaneous touches to each result in a separate tap. /// simultaneous touches to each result in a separate tap.
abstract class OneSequenceGestureRecognizer extends GestureRecognizer { abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
/// Initialize the object.
OneSequenceGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{}; final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
final Set<int> _trackedPointers = new HashSet<int>(); final Set<int> _trackedPointers = new HashSet<int>();
...@@ -227,9 +275,15 @@ enum GestureRecognizerState { ...@@ -227,9 +275,15 @@ enum GestureRecognizerState {
} }
/// A base class for gesture recognizers that track a single primary pointer. /// A base class for gesture recognizers that track a single primary pointer.
///
/// Gestures based on this class will reject the gesture if the primary pointer
/// travels beyond [kTouchSlop] pixels from the original contact point.
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer { abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
/// Initializes the [deadline] field during construction of subclasses. /// Initializes the [deadline] field during construction of subclasses.
PrimaryPointerGestureRecognizer({ this.deadline }); PrimaryPointerGestureRecognizer({
this.deadline,
Object debugOwner,
}) : super(debugOwner: debugOwner);
/// If non-null, the recognizer will call [didExceedDeadline] after this /// If non-null, the recognizer will call [didExceedDeadline] after this
/// amount of time has elapsed since starting to track the primary pointer. /// amount of time has elapsed since starting to track the primary pointer.
...@@ -321,5 +375,8 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni ...@@ -321,5 +375,8 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
} }
@override @override
String toString() => '${describeIdentity(this)}($state)'; void debugFillProperties(List<DiagnosticsNode> description) {
super.debugFillProperties(description);
description.add(new EnumProperty<GestureRecognizerState>('state', state));
}
} }
...@@ -107,6 +107,9 @@ bool _isFlingGesture(Velocity velocity) { ...@@ -107,6 +107,9 @@ bool _isFlingGesture(Velocity velocity) {
/// change, the recognizer calls [onUpdate]. When the pointers are no longer in /// change, the recognizer calls [onUpdate]. When the pointers are no longer in
/// contact with the screen, the recognizer calls [onEnd]. /// contact with the screen, the recognizer calls [onEnd].
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
/// Create a gesture recognizer for interactions intended for scaling content.
ScaleGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
/// The pointers in contact with the screen have established a focal point and /// The pointers in contact with the screen have established a focal point and
/// initial scale of 1.0. /// initial scale of 1.0.
GestureScaleStartCallback onStart; GestureScaleStartCallback onStart;
......
...@@ -64,7 +64,7 @@ typedef void GestureTapCancelCallback(); ...@@ -64,7 +64,7 @@ typedef void GestureTapCancelCallback();
/// * [MultiTapGestureRecognizer] /// * [MultiTapGestureRecognizer]
class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// Creates a tap gesture recognizer. /// Creates a tap gesture recognizer.
TapGestureRecognizer() : super(deadline: kPressTimeout); TapGestureRecognizer({ Object debugOwner }) : super(deadline: kPressTimeout, debugOwner: debugOwner);
/// A pointer that might cause a tap has contacted the screen at a particular /// A pointer that might cause a tap has contacted the screen at a particular
/// location. /// location.
......
...@@ -121,7 +121,7 @@ class VelocityEstimate { ...@@ -121,7 +121,7 @@ class VelocityEstimate {
final Offset offset; final Offset offset;
@override @override
String toString() => 'VelocityEstimate(${pixelsPerSecond.dx.toStringAsFixed(1)}, ${pixelsPerSecond.dy.toStringAsFixed(1)})'; String toString() => 'VelocityEstimate(${pixelsPerSecond.dx.toStringAsFixed(1)}, ${pixelsPerSecond.dy.toStringAsFixed(1)}; offset: $offset, duration: $duration, confidence: ${confidence.toStringAsFixed(1)})';
} }
class _PointAtTime { class _PointAtTime {
......
...@@ -1605,7 +1605,7 @@ class BoxDecoration extends Decoration { ...@@ -1605,7 +1605,7 @@ class BoxDecoration extends Decoration {
DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style: DiagnosticsTreeStyle.whitespace }) { DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style: DiagnosticsTreeStyle.whitespace }) {
return new DiagnosticsNode.lazy( return new DiagnosticsNode.lazy(
name: name, name: name,
object: this, value: this,
description: '', description: '',
style: style, style: style,
emptyBodyDescription: '<no decorations specified>', emptyBodyDescription: '<no decorations specified>',
......
...@@ -215,7 +215,7 @@ class FlutterLogoDecoration extends Decoration { ...@@ -215,7 +215,7 @@ class FlutterLogoDecoration extends Decoration {
return new DiagnosticsNode.lazy( return new DiagnosticsNode.lazy(
name: name, name: name,
description: '$runtimeType', description: '$runtimeType',
object: this, value: this,
style: style, style: style,
fillProperties: (List<DiagnosticsNode> properties) { fillProperties: (List<DiagnosticsNode> properties) {
properties.add(new DiagnosticsNode.message('$lightColor/$darkColor on $textColor')); properties.add(new DiagnosticsNode.message('$lightColor/$darkColor on $textColor'));
......
...@@ -344,7 +344,7 @@ class TextSpan implements TreeDiagnostics { ...@@ -344,7 +344,7 @@ class TextSpan implements TreeDiagnostics {
}) { }) {
return new DiagnosticsNode.lazy( return new DiagnosticsNode.lazy(
name: name, name: name,
object: this, value: this,
description: '$runtimeType', description: '$runtimeType',
style: style, style: style,
fillProperties: (List<DiagnosticsNode> properties) { fillProperties: (List<DiagnosticsNode> properties) {
......
...@@ -462,7 +462,7 @@ class TextStyle extends TreeDiagnostics { ...@@ -462,7 +462,7 @@ class TextStyle extends TreeDiagnostics {
}) { }) {
return new DiagnosticsNode.lazy( return new DiagnosticsNode.lazy(
name: name, name: name,
object: this, value: this,
style: style, style: style,
description: '$runtimeType', description: '$runtimeType',
fillProperties: debugFillProperties, fillProperties: debugFillProperties,
......
...@@ -169,6 +169,7 @@ List<String> debugDescribeTransform(Matrix4 transform) { ...@@ -169,6 +169,7 @@ List<String> debugDescribeTransform(Matrix4 transform) {
/// Property which handles [Matrix4] that represent transforms. /// Property which handles [Matrix4] that represent transforms.
class TransformProperty extends DiagnosticsProperty<Matrix4> { class TransformProperty extends DiagnosticsProperty<Matrix4> {
/// Create a diagnostics property for [Matrix4] objects.
TransformProperty(String name, Matrix4 value, { TransformProperty(String name, Matrix4 value, {
Object defaultValue: kNoDefaultValue, Object defaultValue: kNoDefaultValue,
}) : super( }) : super(
......
...@@ -118,11 +118,11 @@ class RenderEditable extends RenderBox { ...@@ -118,11 +118,11 @@ class RenderEditable extends RenderBox {
_offset = offset { _offset = offset {
assert(_showCursor != null); assert(_showCursor != null);
assert(!_showCursor.value || cursorColor != null); assert(!_showCursor.value || cursorColor != null);
_tap = new TapGestureRecognizer() _tap = new TapGestureRecognizer(debugOwner: this)
..onTapDown = _handleTapDown ..onTapDown = _handleTapDown
..onTap = _handleTap ..onTap = _handleTap
..onTapCancel = _handleTapCancel; ..onTapCancel = _handleTapCancel;
_longPress = new LongPressGestureRecognizer() _longPress = new LongPressGestureRecognizer(debugOwner: this)
..onLongPress = _handleLongPress; ..onLongPress = _handleLongPress;
} }
......
...@@ -636,7 +636,7 @@ class SliverGeometry implements TreeDiagnostics { ...@@ -636,7 +636,7 @@ class SliverGeometry implements TreeDiagnostics {
}) { }) {
return new DiagnosticsNode.lazy( return new DiagnosticsNode.lazy(
name: name, name: name,
object: this, value: this,
description: 'SliverGeometry', description: 'SliverGeometry',
style: style, style: style,
emptyBodyDescription: '<no decorations specified>', emptyBodyDescription: '<no decorations specified>',
......
...@@ -3341,7 +3341,7 @@ abstract class Element implements BuildContext, TreeDiagnostics { ...@@ -3341,7 +3341,7 @@ abstract class Element implements BuildContext, TreeDiagnostics {
DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style }) { DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style }) {
return new DiagnosticsNode.lazy( return new DiagnosticsNode.lazy(
name: name, name: name,
object: this, value: this,
getChildren: () { getChildren: () {
final List<DiagnosticsNode> children = <DiagnosticsNode>[]; final List<DiagnosticsNode> children = <DiagnosticsNode>[];
visitChildren((Element child) { visitChildren((Element child) {
......
...@@ -296,7 +296,7 @@ class GestureDetector extends StatelessWidget { ...@@ -296,7 +296,7 @@ class GestureDetector extends StatelessWidget {
if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) { if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) {
gestures[TapGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( gestures[TapGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => new TapGestureRecognizer(), () => new TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) { (TapGestureRecognizer instance) {
instance instance
..onTapDown = onTapDown ..onTapDown = onTapDown
...@@ -309,7 +309,7 @@ class GestureDetector extends StatelessWidget { ...@@ -309,7 +309,7 @@ class GestureDetector extends StatelessWidget {
if (onDoubleTap != null) { if (onDoubleTap != null) {
gestures[DoubleTapGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>( gestures[DoubleTapGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => new DoubleTapGestureRecognizer(), () => new DoubleTapGestureRecognizer(debugOwner: this),
(DoubleTapGestureRecognizer instance) { (DoubleTapGestureRecognizer instance) {
instance instance
..onDoubleTap = onDoubleTap; ..onDoubleTap = onDoubleTap;
...@@ -319,7 +319,7 @@ class GestureDetector extends StatelessWidget { ...@@ -319,7 +319,7 @@ class GestureDetector extends StatelessWidget {
if (onLongPress != null) { if (onLongPress != null) {
gestures[LongPressGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>( gestures[LongPressGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => new LongPressGestureRecognizer(), () => new LongPressGestureRecognizer(debugOwner: this),
(LongPressGestureRecognizer instance) { (LongPressGestureRecognizer instance) {
instance instance
..onLongPress = onLongPress; ..onLongPress = onLongPress;
...@@ -333,7 +333,7 @@ class GestureDetector extends StatelessWidget { ...@@ -333,7 +333,7 @@ class GestureDetector extends StatelessWidget {
onVerticalDragEnd != null || onVerticalDragEnd != null ||
onVerticalDragCancel != null) { onVerticalDragCancel != null) {
gestures[VerticalDragGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>( gestures[VerticalDragGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => new VerticalDragGestureRecognizer(), () => new VerticalDragGestureRecognizer(debugOwner: this),
(VerticalDragGestureRecognizer instance) { (VerticalDragGestureRecognizer instance) {
instance instance
..onDown = onVerticalDragDown ..onDown = onVerticalDragDown
...@@ -351,7 +351,7 @@ class GestureDetector extends StatelessWidget { ...@@ -351,7 +351,7 @@ class GestureDetector extends StatelessWidget {
onHorizontalDragEnd != null || onHorizontalDragEnd != null ||
onHorizontalDragCancel != null) { onHorizontalDragCancel != null) {
gestures[HorizontalDragGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>( gestures[HorizontalDragGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => new HorizontalDragGestureRecognizer(), () => new HorizontalDragGestureRecognizer(debugOwner: this),
(HorizontalDragGestureRecognizer instance) { (HorizontalDragGestureRecognizer instance) {
instance instance
..onDown = onHorizontalDragDown ..onDown = onHorizontalDragDown
...@@ -369,7 +369,7 @@ class GestureDetector extends StatelessWidget { ...@@ -369,7 +369,7 @@ class GestureDetector extends StatelessWidget {
onPanEnd != null || onPanEnd != null ||
onPanCancel != null) { onPanCancel != null) {
gestures[PanGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>( gestures[PanGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => new PanGestureRecognizer(), () => new PanGestureRecognizer(debugOwner: this),
(PanGestureRecognizer instance) { (PanGestureRecognizer instance) {
instance instance
..onDown = onPanDown ..onDown = onPanDown
...@@ -383,7 +383,7 @@ class GestureDetector extends StatelessWidget { ...@@ -383,7 +383,7 @@ class GestureDetector extends StatelessWidget {
if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) { if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) {
gestures[ScaleGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>( gestures[ScaleGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(
() => new ScaleGestureRecognizer(), () => new ScaleGestureRecognizer(debugOwner: this),
(ScaleGestureRecognizer instance) { (ScaleGestureRecognizer instance) {
instance instance
..onStart = onScaleStart ..onStart = onScaleStart
...@@ -397,7 +397,7 @@ class GestureDetector extends StatelessWidget { ...@@ -397,7 +397,7 @@ class GestureDetector extends StatelessWidget {
gestures: gestures, gestures: gestures,
behavior: behavior, behavior: behavior,
excludeFromSemantics: excludeFromSemantics, excludeFromSemantics: excludeFromSemantics,
child: child child: child,
); );
} }
} }
...@@ -622,6 +622,7 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -622,6 +622,7 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
if (gestures.isEmpty) if (gestures.isEmpty)
gestures.add('<none>'); gestures.add('<none>');
description.add(new IterableProperty<String>('gestures', gestures)); description.add(new IterableProperty<String>('gestures', gestures));
description.add(new IterableProperty<GestureRecognizer>('recognizers', _recognizers.values, hidden: true));
} }
description.add(new EnumProperty<HitTestBehavior>('behavior', widget.behavior, defaultValue: null)); description.add(new EnumProperty<HitTestBehavior>('behavior', widget.behavior, defaultValue: null));
} }
......
...@@ -85,6 +85,12 @@ abstract class TextSelectionControls { ...@@ -85,6 +85,12 @@ abstract class TextSelectionControls {
/// Returns the size of the selection handle. /// Returns the size of the selection handle.
Size get handleSize; Size get handleSize;
/// Copy the current selection of the text field managed by the given
/// `delegate` to the [Clipboard]. Then, remove the selected text from the
/// text field and hide the toolbar.
///
/// This is called by subclasses when their cut affordance is activated by
/// the user.
void handleCut(TextSelectionDelegate delegate) { void handleCut(TextSelectionDelegate delegate) {
final TextEditingValue value = delegate.textEditingValue; final TextEditingValue value = delegate.textEditingValue;
Clipboard.setData(new ClipboardData( Clipboard.setData(new ClipboardData(
...@@ -100,6 +106,12 @@ abstract class TextSelectionControls { ...@@ -100,6 +106,12 @@ abstract class TextSelectionControls {
delegate.hideToolbar(); delegate.hideToolbar();
} }
/// Copy the current selection of the text field managed by the given
/// `delegate` to the [Clipboard]. Then, move the cursor to the end of the
/// text (collapsing the selection in the process), and hide the toolbar.
///
/// This is called by subclasses when their copy affordance is activated by
/// the user.
void handleCopy(TextSelectionDelegate delegate) { void handleCopy(TextSelectionDelegate delegate) {
final TextEditingValue value = delegate.textEditingValue; final TextEditingValue value = delegate.textEditingValue;
Clipboard.setData(new ClipboardData( Clipboard.setData(new ClipboardData(
...@@ -112,6 +124,17 @@ abstract class TextSelectionControls { ...@@ -112,6 +124,17 @@ abstract class TextSelectionControls {
delegate.hideToolbar(); delegate.hideToolbar();
} }
/// Paste the current clipboard selection (obtained from [Clipboard]) into
/// the text field managed by the given `delegate`, replacing its current
/// selection, if any. Then, hide the toolbar.
///
/// This is called by subclasses when their paste affordance is activated by
/// the user.
///
/// This function is asynchronous since interacting with the clipboard is
/// asynchronous. Race conditions may exist with this API as currently
/// implemented.
// TODO(ianh): https://github.com/flutter/flutter/issues/11427
Future<Null> handlePaste(TextSelectionDelegate delegate) async { Future<Null> handlePaste(TextSelectionDelegate delegate) async {
final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`. final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`.
final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain); final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
...@@ -128,6 +151,13 @@ abstract class TextSelectionControls { ...@@ -128,6 +151,13 @@ abstract class TextSelectionControls {
delegate.hideToolbar(); delegate.hideToolbar();
} }
/// Adjust the selection of the text field managed by the given `delegate` so
/// that everything is selected.
///
/// Does not hide the toolbar.
///
/// This is called by subclasses when their select-all affordance is activated
/// by the user.
void handleSelectAll(TextSelectionDelegate delegate) { void handleSelectAll(TextSelectionDelegate delegate) {
delegate.textEditingValue = new TextEditingValue( delegate.textEditingValue = new TextEditingValue(
text: delegate.textEditingValue.text, text: delegate.textEditingValue.text,
......
// Copyright 2017 The Chromium 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 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('debugPrintGestureArenaDiagnostics', (WidgetTester tester) {
PointerEvent event;
debugPrintGestureArenaDiagnostics = true;
final DebugPrintCallback oldCallback = debugPrint;
final List<String> log = <String>[];
debugPrint = (String s, { int wrapWidth }) { log.add(s); };
final TapGestureRecognizer tap = new TapGestureRecognizer()
..onTapDown = (TapDownDetails details) { }
..onTapUp = (TapUpDetails details) { }
..onTap = () { }
..onTapCancel = () { };
expect(log, isEmpty);
event = const PointerDownEvent(pointer: 1, position: const Offset(10.0, 10.0));
tap.addPointer(event);
expect(log, hasLength(2));
expect(log[0], equalsIgnoringHashCodes('Gesture arena 1 ❙ ★ Opening new gesture arena.'));
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Adding: TapGestureRecognizer#00000(state: ready)'));
log.clear();
GestureBinding.instance.gestureArena.close(1);
expect(log, hasLength(1));
expect(log[0], equalsIgnoringHashCodes('Gesture arena 1 ❙ Closing with 1 member.'));
log.clear();
GestureBinding.instance.pointerRouter.route(event);
expect(log, isEmpty);
event = const PointerUpEvent(pointer: 1, position: const Offset(12.0, 8.0));
GestureBinding.instance.pointerRouter.route(event);
expect(log, isEmpty);
GestureBinding.instance.gestureArena.sweep(1);
expect(log, hasLength(2));
expect(log[0], equalsIgnoringHashCodes('Gesture arena 1 ❙ Sweeping with 1 member.'));
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Winner: TapGestureRecognizer#00000(state: ready)'));
log.clear();
tap.dispose();
expect(log, isEmpty);
debugPrintGestureArenaDiagnostics = false;
debugPrint = oldCallback;
});
testWidgets('debugPrintRecognizerCallbacksTrace', (WidgetTester tester) {
PointerEvent event;
debugPrintRecognizerCallbacksTrace = true;
final DebugPrintCallback oldCallback = debugPrint;
final List<String> log = <String>[];
debugPrint = (String s, { int wrapWidth }) { log.add(s); };
final TapGestureRecognizer tap = new TapGestureRecognizer()
..onTapDown = (TapDownDetails details) { }
..onTapUp = (TapUpDetails details) { }
..onTap = () { }
..onTapCancel = () { };
expect(log, isEmpty);
event = const PointerDownEvent(pointer: 1, position: const Offset(10.0, 10.0));
tap.addPointer(event);
expect(log, isEmpty);
GestureBinding.instance.gestureArena.close(1);
expect(log, isEmpty);
GestureBinding.instance.pointerRouter.route(event);
expect(log, isEmpty);
event = const PointerUpEvent(pointer: 1, position: const Offset(12.0, 8.0));
GestureBinding.instance.pointerRouter.route(event);
expect(log, isEmpty);
GestureBinding.instance.gestureArena.sweep(1);
expect(log, hasLength(3));
expect(log[0], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready) calling onTapDown callback.'));
expect(log[1], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready) calling onTapUp callback.'));
expect(log[2], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready) calling onTap callback.'));
log.clear();
tap.dispose();
expect(log, isEmpty);
debugPrintRecognizerCallbacksTrace = false;
debugPrint = oldCallback;
});
testWidgets('debugPrintGestureArenaDiagnostics and debugPrintRecognizerCallbacksTrace', (WidgetTester tester) {
PointerEvent event;
debugPrintGestureArenaDiagnostics = true;
debugPrintRecognizerCallbacksTrace = true;
final DebugPrintCallback oldCallback = debugPrint;
final List<String> log = <String>[];
debugPrint = (String s, { int wrapWidth }) { log.add(s); };
final TapGestureRecognizer tap = new TapGestureRecognizer()
..onTapDown = (TapDownDetails details) { }
..onTapUp = (TapUpDetails details) { }
..onTap = () { }
..onTapCancel = () { };
expect(log, isEmpty);
event = const PointerDownEvent(pointer: 1, position: const Offset(10.0, 10.0));
tap.addPointer(event);
expect(log, hasLength(2));
expect(log[0], equalsIgnoringHashCodes('Gesture arena 1 ❙ ★ Opening new gesture arena.'));
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Adding: TapGestureRecognizer#00000(state: ready)'));
log.clear();
GestureBinding.instance.gestureArena.close(1);
expect(log, hasLength(1));
expect(log[0], equalsIgnoringHashCodes('Gesture arena 1 ❙ Closing with 1 member.'));
log.clear();
GestureBinding.instance.pointerRouter.route(event);
expect(log, isEmpty);
event = const PointerUpEvent(pointer: 1, position: const Offset(12.0, 8.0));
GestureBinding.instance.pointerRouter.route(event);
expect(log, isEmpty);
GestureBinding.instance.gestureArena.sweep(1);
expect(log, hasLength(5));
expect(log[0], equalsIgnoringHashCodes('Gesture arena 1 ❙ Sweeping with 1 member.'));
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Winner: TapGestureRecognizer#00000(state: ready)'));
expect(log[2], equalsIgnoringHashCodes(' ❙ TapGestureRecognizer#00000(state: ready) calling onTapDown callback.'));
expect(log[3], equalsIgnoringHashCodes(' ❙ TapGestureRecognizer#00000(state: ready) calling onTapUp callback.'));
expect(log[4], equalsIgnoringHashCodes(' ❙ TapGestureRecognizer#00000(state: ready) calling onTap callback.'));
log.clear();
tap.dispose();
expect(log, isEmpty);
debugPrintGestureArenaDiagnostics = false;
debugPrintRecognizerCallbacksTrace = false;
debugPrint = oldCallback;
});
}
...@@ -50,17 +50,21 @@ void main() { ...@@ -50,17 +50,21 @@ void main() {
expect(didEndPan, isFalse); expect(didEndPan, isFalse);
expect(didTap, isFalse); expect(didTap, isFalse);
tester.route(pointer.move(const Offset(20.0, 20.0))); // touch should give up when it hits kTouchSlop, which was 18.0 when this test was last updated.
expect(didStartPan, isTrue);
tester.route(pointer.move(const Offset(20.0, 20.0))); // moved 10 horizontally and 10 vertically which is 14 total
expect(didStartPan, isFalse); // 14 < 18
tester.route(pointer.move(const Offset(20.0, 30.0))); // moved 10 horizontally and 20 vertically which is 22 total
expect(didStartPan, isTrue); // 22 > 18
didStartPan = false; didStartPan = false;
expect(updatedScrollDelta, const Offset(10.0, 10.0)); expect(updatedScrollDelta, const Offset(10.0, 20.0));
updatedScrollDelta = null; updatedScrollDelta = null;
expect(didEndPan, isFalse); expect(didEndPan, isFalse);
expect(didTap, isFalse); expect(didTap, isFalse);
tester.route(pointer.move(const Offset(20.0, 25.0))); tester.route(pointer.move(const Offset(20.0, 25.0)));
expect(didStartPan, isFalse); expect(didStartPan, isFalse);
expect(updatedScrollDelta, const Offset(0.0, 5.0)); expect(updatedScrollDelta, const Offset(0.0, -5.0));
updatedScrollDelta = null; updatedScrollDelta = null;
expect(didEndPan, isFalse); expect(didEndPan, isFalse);
expect(didTap, isFalse); expect(didTap, isFalse);
......
...@@ -7,13 +7,12 @@ import 'package:flutter/gestures.dart'; ...@@ -7,13 +7,12 @@ import 'package:flutter/gestures.dart';
import 'gesture_tester.dart'; import 'gesture_tester.dart';
class TestDrag extends Drag { class TestDrag extends Drag { }
}
void main() { void main() {
setUp(ensureGestureBinding); setUp(ensureGestureBinding);
testGesture('MultiDrag control test', (GestureTester tester) { testGesture('MultiDrag: moving before delay rejects', (GestureTester tester) {
final DelayedMultiDragGestureRecognizer drag = new DelayedMultiDragGestureRecognizer(); final DelayedMultiDragGestureRecognizer drag = new DelayedMultiDragGestureRecognizer();
bool didStartDrag = false; bool didStartDrag = false;
...@@ -29,12 +28,37 @@ void main() { ...@@ -29,12 +28,37 @@ void main() {
expect(didStartDrag, isFalse); expect(didStartDrag, isFalse);
tester.async.flushMicrotasks(); tester.async.flushMicrotasks();
expect(didStartDrag, isFalse); expect(didStartDrag, isFalse);
tester.route(pointer.move(const Offset(20.0, 20.0))); tester.route(pointer.move(const Offset(20.0, 60.0))); // move more than touch slop before delay expires
expect(didStartDrag, isFalse);
tester.async.elapse(kLongPressTimeout * 2); // expire delay
expect(didStartDrag, isFalse);
tester.route(pointer.move(const Offset(30.0, 120.0))); // move some more after delay expires
expect(didStartDrag, isFalse);
drag.dispose();
});
testGesture('MultiDrag: delay triggers', (GestureTester tester) {
final DelayedMultiDragGestureRecognizer drag = new DelayedMultiDragGestureRecognizer();
bool didStartDrag = false;
drag.onStart = (Offset position) {
didStartDrag = true;
return new TestDrag();
};
final TestPointer pointer = new TestPointer(5);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
drag.addPointer(down);
tester.closeArena(5);
expect(didStartDrag, isFalse); expect(didStartDrag, isFalse);
tester.async.elapse(kLongPressTimeout * 2); tester.async.flushMicrotasks();
expect(didStartDrag, isFalse); expect(didStartDrag, isFalse);
tester.route(pointer.move(const Offset(30.0, 30.0))); tester.route(pointer.move(const Offset(20.0, 20.0))); // move less than touch slop before delay expires
expect(didStartDrag, isFalse); expect(didStartDrag, isFalse);
tester.async.elapse(kLongPressTimeout * 2); // expire delay
expect(didStartDrag, isTrue);
tester.route(pointer.move(const Offset(30.0, 70.0))); // move more than touch slop after delay expires
expect(didStartDrag, isTrue);
drag.dispose(); drag.dispose();
}); });
} }
...@@ -60,7 +60,7 @@ void main() { ...@@ -60,7 +60,7 @@ void main() {
expect(log, <String>['long-tap-down 6']); expect(log, <String>['long-tap-down 6']);
log.clear(); log.clear();
tester.route(pointer6.move(const Offset(4.0, 3.0))); tester.route(pointer6.move(const Offset(40.0, 30.0))); // move more than kTouchSlop from 15.0,15.0
expect(log, <String>['tap-cancel 6']); expect(log, <String>['tap-cancel 6']);
log.clear(); log.clear();
......
...@@ -6,22 +6,24 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -6,22 +6,24 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
class TestGestureRecognizer extends GestureRecognizer { class TestGestureRecognizer extends GestureRecognizer {
TestGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override @override
String toString() => 'toString content'; String toStringShort() => 'toStringShort content';
@override @override
void addPointer(PointerDownEvent event) {} void addPointer(PointerDownEvent event) { }
@override @override
void acceptGesture(int pointer) {} void acceptGesture(int pointer) { }
@override @override
void rejectGesture(int pointer) {} void rejectGesture(int pointer) { }
} }
void main() { void main() {
test('GestureRecognizer.toStringShort defaults to toString', () { test('GestureRecognizer smoketest', () {
final TestGestureRecognizer recognizer = new TestGestureRecognizer(); final TestGestureRecognizer recognizer = new TestGestureRecognizer(debugOwner: 0);
expect(recognizer.toStringShort(), equals(recognizer.toString())); expect(recognizer, hasAGoodToStringDeep);
}); });
} }
...@@ -39,7 +39,7 @@ void main() { ...@@ -39,7 +39,7 @@ void main() {
final TestPointer pointer1 = new TestPointer(1); final TestPointer pointer1 = new TestPointer(1);
final PointerDownEvent down = pointer1.down(const Offset(10.0, 10.0)); final PointerDownEvent down = pointer1.down(const Offset(0.0, 0.0));
scale.addPointer(down); scale.addPointer(down);
tap.addPointer(down); tap.addPointer(down);
...@@ -211,7 +211,9 @@ void main() { ...@@ -211,7 +211,9 @@ void main() {
tester.route(down); tester.route(down);
expect(log, isEmpty); expect(log, isEmpty);
tester.route(pointer1.move(const Offset(10.0, 30.0))); // 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(<String>['scale-start', 'scale-update'])); expect(log, equals(<String>['scale-start', 'scale-update']));
log.clear(); log.clear();
...@@ -240,7 +242,10 @@ void main() { ...@@ -240,7 +242,10 @@ void main() {
expect(log, isEmpty); expect(log, isEmpty);
log.clear(); log.clear();
// Horizontal moves are drags. // 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 TestPointer pointer3 = new TestPointer(3); final TestPointer pointer3 = new TestPointer(3);
final PointerDownEvent down3 = pointer3.down(const Offset(30.0, 30.0)); final PointerDownEvent down3 = pointer3.down(const Offset(30.0, 30.0));
scale.addPointer(down3); scale.addPointer(down3);
...@@ -250,7 +255,7 @@ void main() { ...@@ -250,7 +255,7 @@ void main() {
expect(log, isEmpty); expect(log, isEmpty);
tester.route(pointer3.move(const Offset(50.0, 30.0))); tester.route(pointer3.move(const Offset(100.0, 30.0)));
expect(log, equals(<String>['scale-start', 'scale-update'])); expect(log, equals(<String>['scale-start', 'scale-update']));
log.clear(); log.clear();
......
...@@ -41,7 +41,7 @@ void main() { ...@@ -41,7 +41,7 @@ void main() {
position: const Offset(31.0, 29.0) position: const Offset(31.0, 29.0)
); );
// Down/move/up sequence 3: intervening motion // Down/move/up sequence 3: intervening motion, more than kTouchSlop. (~21px)
const PointerDownEvent down3 = const PointerDownEvent( const PointerDownEvent down3 = const PointerDownEvent(
pointer: 3, pointer: 3,
position: const Offset(10.0, 10.0) position: const Offset(10.0, 10.0)
...@@ -57,6 +57,22 @@ void main() { ...@@ -57,6 +57,22 @@ void main() {
position: const Offset(25.0, 25.0) position: const Offset(25.0, 25.0)
); );
// Down/move/up sequence 4: intervening motion, less than kTouchSlop. (~17px)
const PointerDownEvent down4 = const PointerDownEvent(
pointer: 4,
position: const Offset(10.0, 10.0)
);
const PointerMoveEvent move4 = const PointerMoveEvent(
pointer: 4,
position: const Offset(22.0, 22.0)
);
const PointerUpEvent up4 = const PointerUpEvent(
pointer: 4,
position: const Offset(22.0, 22.0)
);
testGesture('Should recognize tap', (GestureTester tester) { testGesture('Should recognize tap', (GestureTester tester) {
final TapGestureRecognizer tap = new TapGestureRecognizer(); final TapGestureRecognizer tap = new TapGestureRecognizer();
...@@ -179,6 +195,39 @@ void main() { ...@@ -179,6 +195,39 @@ void main() {
tap.dispose(); tap.dispose();
}); });
testGesture('Short distance does not cancel tap', (GestureTester tester) {
final TapGestureRecognizer tap = new TapGestureRecognizer();
bool tapRecognized = false;
tap.onTap = () {
tapRecognized = true;
};
bool tapCanceled = false;
tap.onTapCancel = () {
tapCanceled = true;
};
tap.addPointer(down4);
tester.closeArena(4);
expect(tapRecognized, isFalse);
expect(tapCanceled, isFalse);
tester.route(down4);
expect(tapRecognized, isFalse);
expect(tapCanceled, isFalse);
tester.route(move4);
expect(tapRecognized, isFalse);
expect(tapCanceled, isFalse);
tester.route(up4);
expect(tapRecognized, isTrue);
expect(tapCanceled, isFalse);
GestureBinding.instance.gestureArena.sweep(4);
expect(tapRecognized, isTrue);
expect(tapCanceled, isFalse);
tap.dispose();
});
testGesture('Timeout does not cancel tap', (GestureTester tester) { testGesture('Timeout does not cancel tap', (GestureTester tester) {
final TapGestureRecognizer tap = new TapGestureRecognizer(); final TapGestureRecognizer tap = new TapGestureRecognizer();
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
void main() { void main() {
testWidgets('Verify that a tap dismisses a modal BottomSheet', (WidgetTester tester) async { testWidgets('Verify that a tap dismisses a modal BottomSheet', (WidgetTester tester) async {
...@@ -102,7 +103,11 @@ void main() { ...@@ -102,7 +103,11 @@ void main() {
expect(showBottomSheetThenCalled, isFalse); expect(showBottomSheetThenCalled, isFalse);
expect(find.text('BottomSheet'), findsOneWidget); expect(find.text('BottomSheet'), findsOneWidget);
await tester.fling(find.text('BottomSheet'), const Offset(0.0, 30.0), 1000.0); // The fling below must be such that the velocity estimation examines an
// offset greater than the kTouchSlop. Too slow or too short a distance, and
// it won't trigger. Also, it musn't be so much that it drags the bottom
// sheet off the screen, or we won't see it after we pump!
await tester.fling(find.text('BottomSheet'), const Offset(0.0, 50.0), 2000.0);
await tester.pump(); // drain the microtask queue (Future completion callback) await tester.pump(); // drain the microtask queue (Future completion callback)
expect(showBottomSheetThenCalled, isTrue); expect(showBottomSheetThenCalled, isTrue);
......
...@@ -28,8 +28,7 @@ void main() { ...@@ -28,8 +28,7 @@ void main() {
transform, transform,
); );
expect(simple.name, equals('transform')); expect(simple.name, equals('transform'));
expect(simple.object, equals(transform)); expect(simple.value, same(transform));
expect(simple.hidden, isFalse);
expect( expect(
simple.toString(), simple.toString(),
equals( equals(
...@@ -46,8 +45,7 @@ void main() { ...@@ -46,8 +45,7 @@ void main() {
null, null,
); );
expect(nullProperty.name, equals('transform')); expect(nullProperty.name, equals('transform'));
expect(nullProperty.object, isNull); expect(nullProperty.value, isNull);
expect(nullProperty.hidden, isFalse);
expect(nullProperty.toString(), equals('transform: null')); expect(nullProperty.toString(), equals('transform: null'));
final TransformProperty hideNull = new TransformProperty( final TransformProperty hideNull = new TransformProperty(
...@@ -55,8 +53,7 @@ void main() { ...@@ -55,8 +53,7 @@ void main() {
null, null,
defaultValue: null, defaultValue: null,
); );
expect(hideNull.object, isNull); expect(hideNull.value, isNull);
expect(hideNull.hidden, isTrue);
expect(hideNull.toString(), equals('transform: null')); expect(hideNull.toString(), equals('transform: null'));
}); });
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
void main() { void main() {
testWidgets('Uncontested scrolls start immediately', (WidgetTester tester) async { testWidgets('Uncontested scrolls start immediately', (WidgetTester tester) async {
...@@ -59,13 +60,13 @@ void main() { ...@@ -59,13 +60,13 @@ void main() {
double dragDistance = 0.0; double dragDistance = 0.0;
final Offset downLocation = const Offset(10.0, 10.0); final Offset downLocation = const Offset(10.0, 10.0);
final Offset upLocation = const Offset(10.0, 20.0); final Offset upLocation = const Offset(10.0, 50.0); // must be far enough to be more than kTouchSlop
final Widget widget = new GestureDetector( final Widget widget = new GestureDetector(
onVerticalDragUpdate: (DragUpdateDetails details) { dragDistance += details.primaryDelta; }, onVerticalDragUpdate: (DragUpdateDetails details) { dragDistance += details.primaryDelta; },
onVerticalDragEnd: (DragEndDetails details) { gestureCount += 1; }, onVerticalDragEnd: (DragEndDetails details) { gestureCount += 1; },
onHorizontalDragUpdate: (DragUpdateDetails details) { fail("gesture should not match"); }, onHorizontalDragUpdate: (DragUpdateDetails details) { fail('gesture should not match'); },
onHorizontalDragEnd: (DragEndDetails details) { fail("gesture should not match"); }, onHorizontalDragEnd: (DragEndDetails details) { fail('gesture should not match'); },
child: new Container( child: new Container(
color: const Color(0xFF00FF00), color: const Color(0xFF00FF00),
) )
...@@ -81,7 +82,7 @@ void main() { ...@@ -81,7 +82,7 @@ void main() {
await gesture.up(); await gesture.up();
expect(gestureCount, 2); expect(gestureCount, 2);
expect(dragDistance, 20.0); expect(dragDistance, 40.0 * 2.0); // delta between down and up, twice
await tester.pumpWidget(new Container()); await tester.pumpWidget(new Container());
}); });
......
...@@ -35,7 +35,7 @@ void main() { ...@@ -35,7 +35,7 @@ void main() {
expect(find.text('Alaska'), findsNothing); expect(find.text('Alaska'), findsNothing);
await tester.drag(find.byType(PageView), const Offset(-10.0, 0.0)); await tester.drag(find.byType(PageView), const Offset(-20.0, 0.0));
await tester.pump(); await tester.pump();
expect(find.text('Alabama'), findsOneWidget); expect(find.text('Alabama'), findsOneWidget);
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
void main() { void main() {
...@@ -78,7 +79,7 @@ void main() { ...@@ -78,7 +79,7 @@ void main() {
final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0)); final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(-10.0, -10.0)); await gesture.moveBy(const Offset(-10.0, -40.0));
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
await gesture.up(); await gesture.up();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
......
...@@ -232,7 +232,12 @@ void main() { ...@@ -232,7 +232,12 @@ void main() {
), ),
); );
await tester.fling(find.byType(Slider), const Offset(-100.0, 0.0), 100.0); // The fling below must be such that the velocity estimation examines an
// offset greater than the kTouchSlop. Too slow or too short a distance, and
// it won't trigger. The actual distance moved doesn't matter since this is
// interpreted as a gesture by the semantics debugger and sent to the widget
// as a semantic action that always moves by 10% of the complete track.
await tester.fling(find.byType(Slider), const Offset(-100.0, 0.0), 2000.0);
expect(value, equals(0.65)); expect(value, equals(0.65));
}); });
......
...@@ -324,6 +324,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -324,6 +324,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
assert(Zone.current == _parentZone); assert(Zone.current == _parentZone);
assert(_currentTestCompleter != null); assert(_currentTestCompleter != null);
if (_pendingExceptionDetails != null) { if (_pendingExceptionDetails != null) {
debugPrint = debugPrintOverride; // just in case the test overrides it -- otherwise we won't see the error!
FlutterError.dumpErrorToConsole(_pendingExceptionDetails, forceReport: true); FlutterError.dumpErrorToConsole(_pendingExceptionDetails, forceReport: true);
// test_package.registerException actually just calls the current zone's error handler (that // test_package.registerException actually just calls the current zone's error handler (that
// is to say, _parentZone's handleUncaughtError function). FakeAsync doesn't add one of those, // is to say, _parentZone's handleUncaughtError function). FakeAsync doesn't add one of those,
...@@ -344,6 +345,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -344,6 +345,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
int _exceptionCount = 0; // number of un-taken exceptions int _exceptionCount = 0; // number of un-taken exceptions
FlutterError.onError = (FlutterErrorDetails details) { FlutterError.onError = (FlutterErrorDetails details) {
if (_pendingExceptionDetails != null) { if (_pendingExceptionDetails != null) {
debugPrint = debugPrintOverride; // just in case the test overrides it -- otherwise we won't see the errors!
if (_exceptionCount == 0) { if (_exceptionCount == 0) {
_exceptionCount = 2; _exceptionCount = 2;
FlutterError.dumpErrorToConsole(_pendingExceptionDetails, forceReport: true); FlutterError.dumpErrorToConsole(_pendingExceptionDetails, forceReport: true);
...@@ -369,6 +371,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -369,6 +371,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
// If we silently dropped these errors on the ground, nobody would ever know. So instead // If we silently dropped these errors on the ground, nobody would ever know. So instead
// we report them to the console. They don't cause test failures, but hopefully someone // we report them to the console. They don't cause test failures, but hopefully someone
// will see them in the logs at some point. // will see them in the logs at some point.
debugPrint = debugPrintOverride; // just in case the test overrides it -- otherwise we won't see the error!
FlutterError.dumpErrorToConsole(new FlutterErrorDetails( FlutterError.dumpErrorToConsole(new FlutterErrorDetails(
exception: exception, exception: exception,
stack: _unmangle(stack), stack: _unmangle(stack),
......
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