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
// bool _first;
// bool _lights;
// bool _visible;
// bool inherit;
// class Cat { }
// double _volume;
// dynamic _calculation;
......
......@@ -430,7 +430,7 @@ class _CupertinoEdgeShadowDecoration extends Decoration {
}) {
return new DiagnosticsNode.lazy(
name: name,
object: this,
value: this,
style: style,
description: '$runtimeType',
fillProperties: (List<DiagnosticsNode> properties) {
......
......@@ -4,6 +4,10 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'debug.dart';
/// Whether the gesture was accepted or rejected.
enum GestureDisposition {
/// This gesture was accepted as the interpretation of the user's input.
......@@ -42,7 +46,8 @@ class GestureArenaEntry {
/// 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) {
_arena._resolve(_pointer, _member, disposition);
}
......@@ -64,19 +69,47 @@ class _GestureArena {
assert(isOpen);
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.
///
/// See [https://flutter.io/gestures/#gesture-disambiguation] for more
/// information about the role this class plays in the gesture system.
///
/// To debug problems with gestures, consider using
/// [debugPrintGestureArenaDiagnostics].
class GestureArenaManager {
final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
/// Adds a new member (e.g., gesture recognizer) to the arena.
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);
assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
return new GestureArenaEntry._(this, pointer, member);
}
......@@ -88,6 +121,7 @@ class GestureArenaManager {
if (state == null)
return; // This arena either never existed or has been resolved.
state.isOpen = false;
assert(_debugLogDiagnostic(pointer, 'Closing', state));
_tryToResolveArena(pointer, state);
}
......@@ -111,13 +145,16 @@ class GestureArenaManager {
assert(!state.isOpen);
if (state.isHeld) {
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);
if (state.members.isNotEmpty) {
// First member wins
// First member wins.
assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
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++)
state.members[i].rejectGesture(pointer);
}
......@@ -140,6 +177,7 @@ class GestureArenaManager {
if (state == null)
return; // This arena either never existed or has been resolved.
state.isHeld = true;
assert(_debugLogDiagnostic(pointer, 'Holding', state));
}
/// Releases a hold, allowing the arena to be swept.
......@@ -156,37 +194,19 @@ class GestureArenaManager {
if (state == null)
return; // This arena either never existed or has been resolved.
state.isHeld = false;
assert(_debugLogDiagnostic(pointer, 'Releasing', state));
if (state.hasPendingSweep)
sweep(pointer);
}
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);
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);
}
}
/// Reject or accept a gesture recognizer.
///
/// This is called by calling [GestureArenaEntry.resolve] on the object returned from [add].
void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
final _GestureArena state = _arenas[pointer];
if (state == null)
return; // This arena has already resolved.
assert(_debugLogDiagnostic(pointer, '${ disposition == GestureDisposition.accepted ? "Accepting" : "Rejecting" }: $member'));
assert(state.members.contains(member));
if (disposition == GestureDisposition.rejected) {
state.members.remove(member);
......@@ -198,11 +218,38 @@ class GestureArenaManager {
if (state.isOpen) {
state.eagerWinner ??= member;
} else {
assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $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) {
assert(state == _arenas[pointer]);
assert(state != null);
......@@ -215,4 +262,16 @@ class GestureArenaManager {
}
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
/// displayed on the screen, from the moment they were last requested.
const Duration kZoomControlsTimeout = const Duration(milliseconds: 3000);
/// The distance a touch has to travel for us to be confident that the gesture
/// is a scroll gesture.
const double kTouchSlop = 8.0; // Logical pixels
/// The distance a touch has to travel for us to be confident that the gesture
/// is a paging gesture. (Currently not used, because paging uses a regular drag
/// gesture, which uses kTouchSlop.)
/// The distance a touch has to travel for the framework to be confident that
/// the gesture is a scroll gesture, or, inversely, the maximum distance that a
/// touch can travel before the framework becomes confident that it is not a
/// tap.
// This value was empirically derived. We started at 8.0 and increased it to
// 18.0 after getting complaints that it was too difficult to hit targets.
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
// paging, which use this constant.
const double kPagingTouchSlop = kTouchSlop * 2.0; // Logical pixels
/// The distance a touch has to travel for us to be confident that the gesture
/// is a panning gesture.
/// The distance a touch has to travel for the framework to be confident that
/// the gesture is a panning gesture.
const double kPanSlop = kTouchSlop * 2.0; // Logical pixels
/// The distance a touch has to travel for us to be confident that the gesture
/// is a scale gesture.
/// The distance a touch has to travel for the framework to be confident that
/// the gesture is a scale gesture.
const double kScaleSlop = kTouchSlop; // Logical pixels
/// The margin around a dialog, popup menu, or other window-like widget inside
......
......@@ -15,6 +15,31 @@ import 'package:flutter/foundation.dart';
/// This has no effect in release builds.
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.
///
/// This function is used by the test framework to ensure that debug variables
......@@ -24,7 +49,9 @@ bool debugPrintHitTestResults = false;
/// a complete list.
bool debugAssertAllGesturesVarsUnset(String reason) {
assert(() {
if (debugPrintHitTestResults)
if (debugPrintHitTestResults ||
debugPrintGestureArenaDiagnostics ||
debugPrintRecognizerCallbacksTrace)
throw new FlutterError(reason);
return true;
});
......
......@@ -17,7 +17,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// Creates a long-press gesture recognizer.
///
/// 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.
GestureLongPressCallback onLongPress;
......
......@@ -43,10 +43,13 @@ typedef void GestureDragCancelCallback();
///
/// See also:
///
/// * [HorizontalDragGestureRecognizer]
/// * [VerticalDragGestureRecognizer]
/// * [PanGestureRecognizer]
/// * [HorizontalDragGestureRecognizer], for left and right drags.
/// * [VerticalDragGestureRecognizer], for up and down drags.
/// * [PanGestureRecognizer], for drags that are not locked to a single axis.
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.
///
/// The position of the pointer is provided in the callback's `details`
......@@ -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
velocity: velocity,
primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond),
)));
)), debugReport: () {
return '$estimate; fling at $velocity.';
});
} else {
invokeCallback<Null>('onEnd', () => onEnd(new DragEndDetails( // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
velocity: Velocity.zero,
primaryVelocity: 0.0,
)));
)), debugReport: () {
if (estimate == null)
return 'Could not estimate velocity.';
return '$estimate; judged to not be a fling.';
});
}
}
_velocityTrackers.clear();
......@@ -218,8 +227,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
///
/// 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 {
/// Create a gesture recognizer for interactions in the vertical axis.
VerticalDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override
bool _isFlingGesture(VelocityEstimate estimate) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
......@@ -246,8 +261,14 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
///
/// 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 {
/// Create a gesture recognizer for interactions in the horizontal axis.
HorizontalDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override
bool _isFlingGesture(VelocityEstimate estimate) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
......@@ -272,15 +293,21 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
///
/// See also:
///
/// * [ImmediateMultiDragGestureRecognizer]
/// * [DelayedMultiDragGestureRecognizer]
/// * [ImmediateMultiDragGestureRecognizer], for a similar recognizer that
/// 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 {
/// Create a gesture recognizer for tracking movement on a plane.
PanGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override
bool _isFlingGesture(VelocityEstimate estimate) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
final double minDistance = minFlingDistance ?? kTouchSlop;
return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity &&
estimate.offset.distanceSquared > minDistance * minDistance;
return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity
&& estimate.offset.distanceSquared > minDistance * minDistance;
}
@override
......
......@@ -169,11 +169,18 @@ abstract class MultiDragPointerState {
///
/// See also:
///
/// * [HorizontalMultiDragGestureRecognizer]
/// * [VerticalMultiDragGestureRecognizer]
/// * [ImmediateMultiDragGestureRecognizer]
/// * [DelayedMultiDragGestureRecognizer]
/// * [ImmediateMultiDragGestureRecognizer], the most straight-forward variant
/// of multi-pointer drag gesture recognizer.
/// * [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.
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.
///
/// The remaining notifications for this drag gesture are delivered to the
......@@ -308,9 +315,18 @@ class _ImmediatePointerState extends MultiDragPointerState {
///
/// See also:
///
/// * [PanGestureRecognizer]
/// * [DelayedMultiDragGestureRecognizer]
/// * [PanGestureRecognizer], which recognizes only one drag gesture at a time,
/// 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> {
/// Create a gesture recognizer for tracking multiple pointers at once.
ImmediateMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override
_ImmediatePointerState createNewPointerState(PointerDownEvent event) {
return new _ImmediatePointerState(event.position);
......@@ -346,8 +362,17 @@ class _HorizontalPointerState extends MultiDragPointerState {
///
/// 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> {
/// Create a gesture recognizer for tracking multiple pointers at once
/// but only if they first move horizontally.
HorizontalMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override
_HorizontalPointerState createNewPointerState(PointerDownEvent event) {
return new _HorizontalPointerState(event.position);
......@@ -383,8 +408,17 @@ class _VerticalPointerState extends MultiDragPointerState {
///
/// 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> {
/// Create a gesture recognizer for tracking multiple pointers at once
/// but only if they first move vertically.
VerticalMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override
_VerticalPointerState createNewPointerState(PointerDownEvent event) {
return new _VerticalPointerState(event.position);
......@@ -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],
/// [DelayedMultiDragGestureRecognizer] waits for a [delay] before recognizing
......@@ -470,8 +505,10 @@ class _DelayedPointerState extends MultiDragPointerState {
///
/// See also:
///
/// * [PanGestureRecognizer]
/// * [ImmediateMultiDragGestureRecognizer]
/// * [ImmediateMultiDragGestureRecognizer], a similar recognizer but without
/// the delay.
/// * [PanGestureRecognizer], which recognizes only one drag gesture at a time,
/// regardless of how many fingers are involved.
class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_DelayedPointerState> {
/// Creates a drag recognizer that works on a per-pointer basis after a delay.
///
......@@ -480,8 +517,10 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Dela
/// defaults to [kLongPressTimeout] to match [LongPressGestureRecognizer] but
/// can be changed for specific behaviors.
DelayedMultiDragGestureRecognizer({
this.delay: kLongPressTimeout
}) : assert(delay != null);
this.delay: kLongPressTimeout,
Object debugOwner,
}) : assert(delay != null),
super(debugOwner: debugOwner);
/// The amount of time the pointer must remain in the same place for the drag
/// to be recognized.
......
......@@ -68,6 +68,9 @@ class _TapTracker {
/// Recognizes when the user has tapped the screen at the same location twice in
/// quick succession.
class DoubleTapGestureRecognizer extends GestureRecognizer {
/// Create a gesture recognizer for double taps.
DoubleTapGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
// Implementation notes:
// 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
......@@ -315,8 +318,9 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
/// The [longTapDelay] defaults to [Duration.ZERO], which means
/// [onLongTapDown] is called immediately after [onTapDown].
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
/// location.
......
......@@ -11,6 +11,7 @@ import 'package:flutter/foundation.dart';
import 'arena.dart';
import 'binding.dart';
import 'constants.dart';
import 'debug.dart';
import 'events.dart';
import 'pointer_router.dart';
import 'team.dart';
......@@ -32,7 +33,21 @@ typedef T RecognizerCallback<T>();
/// See also:
///
/// * [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
/// detector.
///
......@@ -58,16 +73,32 @@ 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();
String toStringShort();
/// Invoke a callback provided by the application, catching and logging any
/// 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
T invokeCallback<T>(String name, RecognizerCallback<T> callback) {
T invokeCallback<T>(String name, RecognizerCallback<T> callback, { String debugReport() }) {
assert(callback != null);
T result;
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();
} catch (exception, stack) {
FlutterError.reportError(new FlutterErrorDetails(
......@@ -86,7 +117,21 @@ abstract class GestureRecognizer extends GestureArenaMember {
}
@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
......@@ -98,6 +143,9 @@ abstract class GestureRecognizer extends GestureArenaMember {
/// which manages each pointer independently and can consider multiple
/// simultaneous touches to each result in a separate tap.
abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
/// Initialize the object.
OneSequenceGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
final Set<int> _trackedPointers = new HashSet<int>();
......@@ -227,9 +275,15 @@ enum GestureRecognizerState {
}
/// 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 {
/// 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
/// amount of time has elapsed since starting to track the primary pointer.
......@@ -321,5 +375,8 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
}
@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) {
/// change, the recognizer calls [onUpdate]. When the pointers are no longer in
/// contact with the screen, the recognizer calls [onEnd].
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
/// initial scale of 1.0.
GestureScaleStartCallback onStart;
......
......@@ -64,7 +64,7 @@ typedef void GestureTapCancelCallback();
/// * [MultiTapGestureRecognizer]
class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// 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
/// location.
......
......@@ -121,7 +121,7 @@ class VelocityEstimate {
final Offset offset;
@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 {
......
......@@ -1605,7 +1605,7 @@ class BoxDecoration extends Decoration {
DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style: DiagnosticsTreeStyle.whitespace }) {
return new DiagnosticsNode.lazy(
name: name,
object: this,
value: this,
description: '',
style: style,
emptyBodyDescription: '<no decorations specified>',
......
......@@ -215,7 +215,7 @@ class FlutterLogoDecoration extends Decoration {
return new DiagnosticsNode.lazy(
name: name,
description: '$runtimeType',
object: this,
value: this,
style: style,
fillProperties: (List<DiagnosticsNode> properties) {
properties.add(new DiagnosticsNode.message('$lightColor/$darkColor on $textColor'));
......
......@@ -344,7 +344,7 @@ class TextSpan implements TreeDiagnostics {
}) {
return new DiagnosticsNode.lazy(
name: name,
object: this,
value: this,
description: '$runtimeType',
style: style,
fillProperties: (List<DiagnosticsNode> properties) {
......
......@@ -462,7 +462,7 @@ class TextStyle extends TreeDiagnostics {
}) {
return new DiagnosticsNode.lazy(
name: name,
object: this,
value: this,
style: style,
description: '$runtimeType',
fillProperties: debugFillProperties,
......
......@@ -169,6 +169,7 @@ List<String> debugDescribeTransform(Matrix4 transform) {
/// Property which handles [Matrix4] that represent transforms.
class TransformProperty extends DiagnosticsProperty<Matrix4> {
/// Create a diagnostics property for [Matrix4] objects.
TransformProperty(String name, Matrix4 value, {
Object defaultValue: kNoDefaultValue,
}) : super(
......
......@@ -118,11 +118,11 @@ class RenderEditable extends RenderBox {
_offset = offset {
assert(_showCursor != null);
assert(!_showCursor.value || cursorColor != null);
_tap = new TapGestureRecognizer()
_tap = new TapGestureRecognizer(debugOwner: this)
..onTapDown = _handleTapDown
..onTap = _handleTap
..onTapCancel = _handleTapCancel;
_longPress = new LongPressGestureRecognizer()
_longPress = new LongPressGestureRecognizer(debugOwner: this)
..onLongPress = _handleLongPress;
}
......
......@@ -636,7 +636,7 @@ class SliverGeometry implements TreeDiagnostics {
}) {
return new DiagnosticsNode.lazy(
name: name,
object: this,
value: this,
description: 'SliverGeometry',
style: style,
emptyBodyDescription: '<no decorations specified>',
......
......@@ -3341,7 +3341,7 @@ abstract class Element implements BuildContext, TreeDiagnostics {
DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style }) {
return new DiagnosticsNode.lazy(
name: name,
object: this,
value: this,
getChildren: () {
final List<DiagnosticsNode> children = <DiagnosticsNode>[];
visitChildren((Element child) {
......
......@@ -296,7 +296,7 @@ class GestureDetector extends StatelessWidget {
if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) {
gestures[TapGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => new TapGestureRecognizer(),
() => new TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onTapDown = onTapDown
......@@ -309,7 +309,7 @@ class GestureDetector extends StatelessWidget {
if (onDoubleTap != null) {
gestures[DoubleTapGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => new DoubleTapGestureRecognizer(),
() => new DoubleTapGestureRecognizer(debugOwner: this),
(DoubleTapGestureRecognizer instance) {
instance
..onDoubleTap = onDoubleTap;
......@@ -319,7 +319,7 @@ class GestureDetector extends StatelessWidget {
if (onLongPress != null) {
gestures[LongPressGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => new LongPressGestureRecognizer(),
() => new LongPressGestureRecognizer(debugOwner: this),
(LongPressGestureRecognizer instance) {
instance
..onLongPress = onLongPress;
......@@ -333,7 +333,7 @@ class GestureDetector extends StatelessWidget {
onVerticalDragEnd != null ||
onVerticalDragCancel != null) {
gestures[VerticalDragGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => new VerticalDragGestureRecognizer(),
() => new VerticalDragGestureRecognizer(debugOwner: this),
(VerticalDragGestureRecognizer instance) {
instance
..onDown = onVerticalDragDown
......@@ -351,7 +351,7 @@ class GestureDetector extends StatelessWidget {
onHorizontalDragEnd != null ||
onHorizontalDragCancel != null) {
gestures[HorizontalDragGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => new HorizontalDragGestureRecognizer(),
() => new HorizontalDragGestureRecognizer(debugOwner: this),
(HorizontalDragGestureRecognizer instance) {
instance
..onDown = onHorizontalDragDown
......@@ -369,7 +369,7 @@ class GestureDetector extends StatelessWidget {
onPanEnd != null ||
onPanCancel != null) {
gestures[PanGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => new PanGestureRecognizer(),
() => new PanGestureRecognizer(debugOwner: this),
(PanGestureRecognizer instance) {
instance
..onDown = onPanDown
......@@ -383,7 +383,7 @@ class GestureDetector extends StatelessWidget {
if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) {
gestures[ScaleGestureRecognizer] = new GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(
() => new ScaleGestureRecognizer(),
() => new ScaleGestureRecognizer(debugOwner: this),
(ScaleGestureRecognizer instance) {
instance
..onStart = onScaleStart
......@@ -397,7 +397,7 @@ class GestureDetector extends StatelessWidget {
gestures: gestures,
behavior: behavior,
excludeFromSemantics: excludeFromSemantics,
child: child
child: child,
);
}
}
......@@ -622,6 +622,7 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
if (gestures.isEmpty)
gestures.add('<none>');
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));
}
......
......@@ -85,6 +85,12 @@ abstract class TextSelectionControls {
/// Returns the size of the selection handle.
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) {
final TextEditingValue value = delegate.textEditingValue;
Clipboard.setData(new ClipboardData(
......@@ -100,6 +106,12 @@ abstract class TextSelectionControls {
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) {
final TextEditingValue value = delegate.textEditingValue;
Clipboard.setData(new ClipboardData(
......@@ -112,6 +124,17 @@ abstract class TextSelectionControls {
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 {
final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`.
final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
......@@ -128,6 +151,13 @@ abstract class TextSelectionControls {
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) {
delegate.textEditingValue = new TextEditingValue(
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() {
expect(didEndPan, isFalse);
expect(didTap, isFalse);
tester.route(pointer.move(const Offset(20.0, 20.0)));
expect(didStartPan, isTrue);
// touch should give up when it hits kTouchSlop, which was 18.0 when this test was last updated.
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;
expect(updatedScrollDelta, const Offset(10.0, 10.0));
expect(updatedScrollDelta, const Offset(10.0, 20.0));
updatedScrollDelta = null;
expect(didEndPan, isFalse);
expect(didTap, isFalse);
tester.route(pointer.move(const Offset(20.0, 25.0)));
expect(didStartPan, isFalse);
expect(updatedScrollDelta, const Offset(0.0, 5.0));
expect(updatedScrollDelta, const Offset(0.0, -5.0));
updatedScrollDelta = null;
expect(didEndPan, isFalse);
expect(didTap, isFalse);
......
......@@ -7,13 +7,12 @@ import 'package:flutter/gestures.dart';
import 'gesture_tester.dart';
class TestDrag extends Drag {
}
class TestDrag extends Drag { }
void main() {
setUp(ensureGestureBinding);
testGesture('MultiDrag control test', (GestureTester tester) {
testGesture('MultiDrag: moving before delay rejects', (GestureTester tester) {
final DelayedMultiDragGestureRecognizer drag = new DelayedMultiDragGestureRecognizer();
bool didStartDrag = false;
......@@ -29,12 +28,37 @@ void main() {
expect(didStartDrag, isFalse);
tester.async.flushMicrotasks();
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);
tester.async.elapse(kLongPressTimeout * 2);
tester.async.flushMicrotasks();
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);
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();
});
}
......@@ -60,7 +60,7 @@ void main() {
expect(log, <String>['long-tap-down 6']);
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']);
log.clear();
......
......@@ -6,22 +6,24 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
class TestGestureRecognizer extends GestureRecognizer {
TestGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override
String toString() => 'toString content';
String toStringShort() => 'toStringShort content';
@override
void addPointer(PointerDownEvent event) {}
void addPointer(PointerDownEvent event) { }
@override
void acceptGesture(int pointer) {}
void acceptGesture(int pointer) { }
@override
void rejectGesture(int pointer) {}
void rejectGesture(int pointer) { }
}
void main() {
test('GestureRecognizer.toStringShort defaults to toString', () {
final TestGestureRecognizer recognizer = new TestGestureRecognizer();
expect(recognizer.toStringShort(), equals(recognizer.toString()));
test('GestureRecognizer smoketest', () {
final TestGestureRecognizer recognizer = new TestGestureRecognizer(debugOwner: 0);
expect(recognizer, hasAGoodToStringDeep);
});
}
......@@ -39,7 +39,7 @@ void main() {
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);
tap.addPointer(down);
......@@ -211,7 +211,9 @@ void main() {
tester.route(down);
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']));
log.clear();
......@@ -240,7 +242,10 @@ void main() {
expect(log, isEmpty);
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 PointerDownEvent down3 = pointer3.down(const Offset(30.0, 30.0));
scale.addPointer(down3);
......@@ -250,7 +255,7 @@ void main() {
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']));
log.clear();
......
......@@ -41,7 +41,7 @@ void main() {
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(
pointer: 3,
position: const Offset(10.0, 10.0)
......@@ -57,6 +57,22 @@ void main() {
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) {
final TapGestureRecognizer tap = new TapGestureRecognizer();
......@@ -179,6 +195,39 @@ void main() {
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) {
final TapGestureRecognizer tap = new TapGestureRecognizer();
......
......@@ -5,6 +5,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
void main() {
testWidgets('Verify that a tap dismisses a modal BottomSheet', (WidgetTester tester) async {
......@@ -102,7 +103,11 @@ void main() {
expect(showBottomSheetThenCalled, isFalse);
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)
expect(showBottomSheetThenCalled, isTrue);
......
......@@ -28,8 +28,7 @@ void main() {
transform,
);
expect(simple.name, equals('transform'));
expect(simple.object, equals(transform));
expect(simple.hidden, isFalse);
expect(simple.value, same(transform));
expect(
simple.toString(),
equals(
......@@ -46,8 +45,7 @@ void main() {
null,
);
expect(nullProperty.name, equals('transform'));
expect(nullProperty.object, isNull);
expect(nullProperty.hidden, isFalse);
expect(nullProperty.value, isNull);
expect(nullProperty.toString(), equals('transform: null'));
final TransformProperty hideNull = new TransformProperty(
......@@ -55,8 +53,7 @@ void main() {
null,
defaultValue: null,
);
expect(hideNull.object, isNull);
expect(hideNull.hidden, isTrue);
expect(hideNull.value, isNull);
expect(hideNull.toString(), equals('transform: null'));
});
......
......@@ -4,6 +4,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
void main() {
testWidgets('Uncontested scrolls start immediately', (WidgetTester tester) async {
......@@ -59,13 +60,13 @@ void main() {
double dragDistance = 0.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(
onVerticalDragUpdate: (DragUpdateDetails details) { dragDistance += details.primaryDelta; },
onVerticalDragEnd: (DragEndDetails details) { gestureCount += 1; },
onHorizontalDragUpdate: (DragUpdateDetails details) { fail("gesture should not match"); },
onHorizontalDragEnd: (DragEndDetails details) { fail("gesture should not match"); },
onHorizontalDragUpdate: (DragUpdateDetails details) { fail('gesture should not match'); },
onHorizontalDragEnd: (DragEndDetails details) { fail('gesture should not match'); },
child: new Container(
color: const Color(0xFF00FF00),
)
......@@ -81,7 +82,7 @@ void main() {
await gesture.up();
expect(gestureCount, 2);
expect(dragDistance, 20.0);
expect(dragDistance, 40.0 * 2.0); // delta between down and up, twice
await tester.pumpWidget(new Container());
});
......
......@@ -35,7 +35,7 @@ void main() {
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();
expect(find.text('Alabama'), findsOneWidget);
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
void main() {
......@@ -78,7 +79,7 @@ void main() {
final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
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 gesture.up();
await tester.pump(const Duration(seconds: 1));
......
......@@ -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));
});
......
......@@ -324,6 +324,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
assert(Zone.current == _parentZone);
assert(_currentTestCompleter != 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);
// 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,
......@@ -344,6 +345,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
int _exceptionCount = 0; // number of un-taken exceptions
FlutterError.onError = (FlutterErrorDetails details) {
if (_pendingExceptionDetails != null) {
debugPrint = debugPrintOverride; // just in case the test overrides it -- otherwise we won't see the errors!
if (_exceptionCount == 0) {
_exceptionCount = 2;
FlutterError.dumpErrorToConsole(_pendingExceptionDetails, forceReport: true);
......@@ -369,6 +371,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
// 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
// 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(
exception: exception,
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