mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add long-press-move support for text fields 2 (#28242)
This commit is contained in:
parent
47724f97fa
commit
ec00e974d0
@ -482,8 +482,21 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
_requestKeyboard();
|
||||
}
|
||||
|
||||
void _handleSingleLongTapDown() {
|
||||
_renderEditable.selectPosition(cause: SelectionChangedCause.longPress);
|
||||
void _handleSingleLongTapStart(LongPressStartDetails details) {
|
||||
_renderEditable.selectPositionAt(
|
||||
from: details.globalPosition,
|
||||
cause: SelectionChangedCause.longPress,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
|
||||
_renderEditable.selectPositionAt(
|
||||
from: details.globalPosition,
|
||||
cause: SelectionChangedCause.longPress,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSingleLongTapEnd(LongPressEndDetails details) {
|
||||
_editableTextKey.currentState.showToolbar();
|
||||
}
|
||||
|
||||
@ -492,6 +505,12 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
_editableTextKey.currentState.showToolbar();
|
||||
}
|
||||
|
||||
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
|
||||
if (cause == SelectionChangedCause.longPress) {
|
||||
_editableTextKey.currentState?.bringIntoView(selection.base);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => _controller?.text?.isNotEmpty == true;
|
||||
|
||||
@ -646,6 +665,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
selectionColor: _kSelectionHighlightColor,
|
||||
selectionControls: cupertinoTextSelectionControls,
|
||||
onChanged: widget.onChanged,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
onEditingComplete: widget.onEditingComplete,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
inputFormatters: formatters,
|
||||
@ -686,7 +706,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
onForcePressStart: _handleForcePressStarted,
|
||||
onForcePressEnd: _handleForcePressEnded,
|
||||
onSingleTapUp: _handleSingleTapUp,
|
||||
onSingleLongTapDown: _handleSingleLongTapDown,
|
||||
onSingleLongTapStart: _handleSingleLongTapStart,
|
||||
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
|
||||
onSingleLongTapEnd: _handleSingleLongTapEnd,
|
||||
onDoubleTapDown: _handleDoubleTapDown,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: _addTextDependentAttachments(paddedEditable, textStyle),
|
||||
|
||||
@ -7,54 +7,221 @@ import 'constants.dart';
|
||||
import 'events.dart';
|
||||
import 'recognizer.dart';
|
||||
|
||||
/// Signature for when a pointer has remained in contact with the screen at the
|
||||
/// Callback signature for [LongPressGestureRecognizer.onLongPress].
|
||||
///
|
||||
/// Called when a pointer has remained in contact with the screen at the
|
||||
/// same location for a long period of time.
|
||||
typedef GestureLongPressCallback = void Function();
|
||||
|
||||
/// Signature for when a pointer stops contacting the screen after a long press gesture was detected.
|
||||
/// Callback signature for [LongPressGestureRecognizer.onLongPressUp].
|
||||
///
|
||||
/// Called when a pointer stops contacting the screen after a long press
|
||||
/// gesture was detected.
|
||||
typedef GestureLongPressUpCallback = void Function();
|
||||
|
||||
/// Callback signature for [LongPressGestureRecognizer.onLongPressStart].
|
||||
///
|
||||
/// Called when a pointer has remained in contact with the screen at the
|
||||
/// same location for a long period of time. Also reports the long press down
|
||||
/// position.
|
||||
typedef GestureLongPressStartCallback = void Function(LongPressStartDetails details);
|
||||
|
||||
/// Callback signature for [LongPressGestureRecognizer.onLongPressMoveUpdate].
|
||||
///
|
||||
/// Called when a pointer is moving after being held in contact at the same
|
||||
/// location for a long period of time. Reports the new position and its offset
|
||||
/// from the original down position.
|
||||
typedef GestureLongPressMoveUpdateCallback = void Function(LongPressMoveUpdateDetails details);
|
||||
|
||||
/// Callback signature for [LongPressGestureRecognizer.onLongPressEnd].
|
||||
///
|
||||
/// Called when a pointer stops contacting the screen after a long press
|
||||
/// gesture was detected. Also reports the position where the pointer stopped
|
||||
/// contacting the screen.
|
||||
typedef GestureLongPressEndCallback = void Function(LongPressEndDetails details);
|
||||
|
||||
/// Details for callbacks that use [GestureLongPressStartCallback].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [LongPressGestureRecognizer.onLongPressStart], which uses [GestureLongPressStartCallback].
|
||||
/// * [LongPressMoveUpdateDetails], the details for [GestureLongPressMoveUpdateCallback]
|
||||
/// * [LongPressEndDetails], the details for [GestureLongPressEndCallback].
|
||||
class LongPressStartDetails {
|
||||
/// Creates the details for a [GestureLongPressStartCallback].
|
||||
///
|
||||
/// The [globalPosition] argument must not be null.
|
||||
const LongPressStartDetails({ this.globalPosition = Offset.zero })
|
||||
: assert(globalPosition != null);
|
||||
|
||||
/// The global position at which the pointer contacted the screen.
|
||||
final Offset globalPosition;
|
||||
}
|
||||
|
||||
/// Details for callbacks that use [GestureLongPressMoveUpdateCallback].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [LongPressGestureRecognizer.onLongPressMoveUpdate], which uses [GestureLongPressMoveUpdateCallback].
|
||||
/// * [LongPressEndDetails], the details for [GestureLongPressEndCallback]
|
||||
/// * [LongPressStartDetails], the details for [GestureLongPressStartCallback].
|
||||
class LongPressMoveUpdateDetails {
|
||||
/// Creates the details for a [GestureLongPressMoveUpdateCallback].
|
||||
///
|
||||
/// The [globalPosition] and [offsetFromOrigin] arguments must not be null.
|
||||
const LongPressMoveUpdateDetails({
|
||||
this.globalPosition = Offset.zero,
|
||||
this.offsetFromOrigin = Offset.zero,
|
||||
}) : assert(globalPosition != null),
|
||||
assert(offsetFromOrigin != null);
|
||||
|
||||
/// The global position of the pointer when it triggered this update.
|
||||
final Offset globalPosition;
|
||||
|
||||
/// A delta offset from the point where the long press drag initially contacted
|
||||
/// the screen to the point where the pointer is currently located (the
|
||||
/// present [globalPosition]) when this callback is triggered.
|
||||
final Offset offsetFromOrigin;
|
||||
}
|
||||
|
||||
/// Details for callbacks that use [GestureLongPressEndCallback].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [LongPressGestureRecognizer.onLongPressEnd], which uses [GestureLongPressEndCallback].
|
||||
/// * [LongPressMoveUpdateDetails], the details for [GestureLongPressMoveUpdateCallback]
|
||||
/// * [LongPressStartDetails], the details for [GestureLongPressStartCallback].
|
||||
class LongPressEndDetails {
|
||||
/// Creates the details for a [GestureLongPressEndCallback].
|
||||
///
|
||||
/// The [globalPosition] argument must not be null.
|
||||
const LongPressEndDetails({ this.globalPosition = Offset.zero })
|
||||
: assert(globalPosition != null);
|
||||
|
||||
/// The global position at which the pointer lifted from the screen.
|
||||
final Offset globalPosition;
|
||||
}
|
||||
|
||||
/// Recognizes when the user has pressed down at the same location for a long
|
||||
/// period of time.
|
||||
///
|
||||
/// The gesture must not deviate in position from its touch down point for 500ms
|
||||
/// until it's recognized. Once the gesture is accepted, the finger can be
|
||||
/// moved, triggering [onLongPressMoveUpdate] callbacks, unless the
|
||||
/// [postAcceptSlopTolerance] constructor argument is specified.
|
||||
class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
/// Creates a long-press gesture recognizer.
|
||||
///
|
||||
/// Consider assigning the [onLongPress] callback after creating this object.
|
||||
LongPressGestureRecognizer({ Object debugOwner })
|
||||
: super(deadline: kLongPressTimeout, debugOwner: debugOwner);
|
||||
/// Consider assigning the [onLongPressStart] callback after creating this
|
||||
/// object.
|
||||
///
|
||||
/// The [postAcceptSlopTolerance] argument can be used to specify a maximum
|
||||
/// allowed distance for the gesture to deviate from the starting point once
|
||||
/// the long press has triggered. If the gesture deviates past that point,
|
||||
/// subsequent callbacks ([onLongPressMoveUpdate], [onLongPressUp],
|
||||
/// [onLongPressEnd]) will stop. Defaults to null, which means the gesture
|
||||
/// can be moved without limit once the long press is accepted.
|
||||
LongPressGestureRecognizer({
|
||||
double postAcceptSlopTolerance,
|
||||
Object debugOwner,
|
||||
}) : super(
|
||||
deadline: kLongPressTimeout,
|
||||
postAcceptSlopTolerance: postAcceptSlopTolerance,
|
||||
debugOwner: debugOwner,
|
||||
);
|
||||
|
||||
bool _longPressAccepted = false;
|
||||
|
||||
Offset _longPressOrigin;
|
||||
|
||||
/// Called when a long press gesture has been recognized.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onLongPressStart], which has the same timing but has data for the
|
||||
/// press location.
|
||||
GestureLongPressCallback onLongPress;
|
||||
|
||||
/// Called when the pointer stops contacting the screen after the long-press gesture has been recognized.
|
||||
/// Callback for long press start with gesture location.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onLongPress], which has the same timing but without the location data.
|
||||
GestureLongPressStartCallback onLongPressStart;
|
||||
|
||||
/// Callback for moving the gesture after the lang press is recognized.
|
||||
GestureLongPressMoveUpdateCallback onLongPressMoveUpdate;
|
||||
|
||||
/// Called when the pointer stops contacting the screen after the long-press.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onLongPressEnd], which has the same timing but has data for the up
|
||||
/// gesture location.
|
||||
GestureLongPressUpCallback onLongPressUp;
|
||||
|
||||
/// Callback for long press end with gesture location.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onLongPressUp], which has the same timing but without the location data.
|
||||
GestureLongPressEndCallback onLongPressEnd;
|
||||
|
||||
@override
|
||||
void didExceedDeadline() {
|
||||
resolve(GestureDisposition.accepted);
|
||||
_longPressAccepted = true;
|
||||
super.acceptGesture(primaryPointer);
|
||||
if (onLongPress != null) {
|
||||
invokeCallback<void>('onLongPress', onLongPress);
|
||||
}
|
||||
if (onLongPressStart != null) {
|
||||
invokeCallback<void>('onLongPressStart', () {
|
||||
onLongPressStart(LongPressStartDetails(
|
||||
globalPosition: _longPressOrigin,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handlePrimaryPointer(PointerEvent event) {
|
||||
if (event is PointerUpEvent) {
|
||||
if (_longPressAccepted == true && onLongPressUp != null) {
|
||||
if (_longPressAccepted == true) {
|
||||
if (onLongPressUp != null) {
|
||||
invokeCallback<void>('onLongPressUp', onLongPressUp);
|
||||
}
|
||||
if (onLongPressEnd != null) {
|
||||
invokeCallback<void>('onLongPressEnd', () {
|
||||
onLongPressEnd(LongPressEndDetails(
|
||||
globalPosition: event.position,
|
||||
));
|
||||
});
|
||||
}
|
||||
_longPressAccepted = false;
|
||||
invokeCallback<void>('onLongPressUp', onLongPressUp);
|
||||
} else {
|
||||
resolve(GestureDisposition.rejected);
|
||||
}
|
||||
} else if (event is PointerDownEvent || event is PointerCancelEvent) {
|
||||
// the first touch, initialize the flag with false
|
||||
// The first touch.
|
||||
_longPressAccepted = false;
|
||||
_longPressOrigin = event.position;
|
||||
} else if (event is PointerMoveEvent && _longPressAccepted && onLongPressMoveUpdate != null) {
|
||||
invokeCallback<void>('onLongPressMoveUpdate', () {
|
||||
onLongPressMoveUpdate(LongPressMoveUpdateDetails(
|
||||
globalPosition: event.position,
|
||||
offsetFromOrigin: event.position - _longPressOrigin,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void acceptGesture(int pointer) {
|
||||
// Winning the arena isn't important here since it may happen from a sweep.
|
||||
// Explicitly exceeding the deadline puts the gesture in accepted state.
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'long press';
|
||||
}
|
||||
|
||||
@ -261,11 +261,11 @@ abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
|
||||
|
||||
/// The possible states of a [PrimaryPointerGestureRecognizer].
|
||||
///
|
||||
/// The recognizer advances from [ready] to [possible] when starts tracking a
|
||||
/// primary pointer. When the primary pointer is resolve (either accepted or
|
||||
/// or rejected), the recognizers advances to [defunct]. Once the recognizer
|
||||
/// has stopped tracking any remaining pointers, the recognizer returns to
|
||||
/// [ready].
|
||||
/// The recognizer advances from [ready] to [possible] when it starts tracking a
|
||||
/// primary pointer. When the primary pointer is resolved in the gesture
|
||||
/// arena (either accepted or rejected), the recognizers advances to [defunct].
|
||||
/// Once the recognizer has stopped tracking any remaining pointers, the
|
||||
/// recognizer returns to [ready].
|
||||
enum GestureRecognizerState {
|
||||
/// The recognizer is ready to start recognizing a gesture.
|
||||
ready,
|
||||
@ -283,19 +283,52 @@ 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.
|
||||
/// Gestures based on this class will stop tracking the gesture if the primary
|
||||
/// pointer travels beyond [preAcceptSlopTolerance] or [postAcceptSlopTolerance]
|
||||
/// pixels from the original contact point of the gesture.
|
||||
///
|
||||
/// If the [preAcceptSlopTolerance] was breached before the gesture was accepted
|
||||
/// in the gesture arena, the gesture will be rejected.
|
||||
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
/// Initializes the [deadline] field during construction of subclasses.
|
||||
PrimaryPointerGestureRecognizer({
|
||||
this.deadline,
|
||||
this.preAcceptSlopTolerance = kTouchSlop,
|
||||
this.postAcceptSlopTolerance = kTouchSlop,
|
||||
Object debugOwner,
|
||||
}) : super(debugOwner: debugOwner);
|
||||
}) : assert(
|
||||
preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
|
||||
'The preAcceptSlopTolerance must be positive or null',
|
||||
),
|
||||
assert(
|
||||
postAcceptSlopTolerance == null || postAcceptSlopTolerance >= 0,
|
||||
'The postAcceptSlopTolerance must be positive or null',
|
||||
),
|
||||
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.
|
||||
final Duration deadline;
|
||||
|
||||
/// The maximum distance in logical pixels the gesture is allowed to drift
|
||||
/// from the initial touch down position before the gesture is accepted.
|
||||
///
|
||||
/// Drifting past the allowed slop amount causes the gesture to be rejected.
|
||||
///
|
||||
/// Can be null to indicate that the gesture can drift for any distance.
|
||||
/// Defaults to 18 logical pixels.
|
||||
final double preAcceptSlopTolerance;
|
||||
|
||||
/// The maximum distance in logical pixels the gesture is allowed to drift
|
||||
/// after the gesture has been accepted.
|
||||
///
|
||||
/// Drifting past the allowed slop amount causes the gesture to stop tracking
|
||||
/// and signaling subsequent callbacks.
|
||||
///
|
||||
/// Can be null to indicate that the gesture can drift for any distance.
|
||||
/// Defaults to 18 logical pixels.
|
||||
final double postAcceptSlopTolerance;
|
||||
|
||||
/// The current state of the recognizer.
|
||||
///
|
||||
/// See [GestureRecognizerState] for a description of the states.
|
||||
@ -307,6 +340,9 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
|
||||
/// The global location at which the primary pointer contacted the screen.
|
||||
Offset initialPosition;
|
||||
|
||||
// Whether this pointer is accepted by winning the arena or as defined by
|
||||
// a subclass calling acceptGesture.
|
||||
bool _gestureAccepted = false;
|
||||
Timer _timer;
|
||||
|
||||
@override
|
||||
@ -325,8 +361,16 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
|
||||
void handleEvent(PointerEvent event) {
|
||||
assert(state != GestureRecognizerState.ready);
|
||||
if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
|
||||
// TODO(abarth): Maybe factor the slop handling out into a separate class?
|
||||
if (event is PointerMoveEvent && _getDistance(event) > kTouchSlop) {
|
||||
final bool isPreAcceptSlopPastTolerance =
|
||||
!_gestureAccepted &&
|
||||
preAcceptSlopTolerance != null &&
|
||||
_getDistance(event) > preAcceptSlopTolerance;
|
||||
final bool isPostAcceptSlopPastTolerance =
|
||||
_gestureAccepted &&
|
||||
postAcceptSlopTolerance != null &&
|
||||
_getDistance(event) > postAcceptSlopTolerance;
|
||||
|
||||
if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
|
||||
resolve(GestureDisposition.rejected);
|
||||
stopTrackingPointer(primaryPointer);
|
||||
} else {
|
||||
@ -348,6 +392,11 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
|
||||
assert(deadline == null);
|
||||
}
|
||||
|
||||
@override
|
||||
void acceptGesture(int pointer) {
|
||||
_gestureAccepted = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
if (pointer == primaryPointer && state == GestureRecognizerState.possible) {
|
||||
|
||||
@ -568,6 +568,21 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
_editableTextKey.currentState?.requestKeyboard();
|
||||
}
|
||||
|
||||
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
|
||||
// iOS cursor doesn't move via a selection handle. The scroll happens
|
||||
// directly from new text selection changes.
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.iOS:
|
||||
if (cause == SelectionChangedCause.longPress) {
|
||||
_editableTextKey.currentState?.bringIntoView(selection.base);
|
||||
}
|
||||
return;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveInkFeature _createInkFeature(TapDownDetails details) {
|
||||
final MaterialInkController inkController = Material.of(context);
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
@ -641,11 +656,14 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
_cancelCurrentSplash();
|
||||
}
|
||||
|
||||
void _handleSingleLongTapDown() {
|
||||
void _handleSingleLongTapStart(LongPressStartDetails details) {
|
||||
if (widget.selectionEnabled) {
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.iOS:
|
||||
_renderEditable.selectPosition(cause: SelectionChangedCause.longPress);
|
||||
_renderEditable.selectPositionAt(
|
||||
from: details.globalPosition,
|
||||
cause: SelectionChangedCause.longPress,
|
||||
);
|
||||
break;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
@ -653,11 +671,35 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
Feedback.forLongPress(context);
|
||||
break;
|
||||
}
|
||||
_editableTextKey.currentState.showToolbar();
|
||||
}
|
||||
_confirmCurrentSplash();
|
||||
}
|
||||
|
||||
void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
|
||||
if (widget.selectionEnabled) {
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.iOS:
|
||||
_renderEditable.selectPositionAt(
|
||||
from: details.globalPosition,
|
||||
cause: SelectionChangedCause.longPress,
|
||||
);
|
||||
break;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
_renderEditable.selectWordsInRange(
|
||||
from: details.globalPosition - details.offsetFromOrigin,
|
||||
to: details.globalPosition,
|
||||
cause: SelectionChangedCause.longPress,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSingleLongTapEnd(LongPressEndDetails details) {
|
||||
_editableTextKey.currentState.showToolbar();
|
||||
}
|
||||
|
||||
void _handleDoubleTapDown(TapDownDetails details) {
|
||||
if (widget.selectionEnabled) {
|
||||
_renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
|
||||
@ -777,6 +819,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
selectionColor: themeData.textSelectionColor,
|
||||
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
|
||||
onChanged: widget.onChanged,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
onEditingComplete: widget.onEditingComplete,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
inputFormatters: formatters,
|
||||
@ -825,7 +868,9 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
onForcePressStart: forcePressEnabled ? _handleForcePressStarted : null,
|
||||
onSingleTapUp: _handleSingleTapUp,
|
||||
onSingleTapCancel: _handleSingleTapCancel,
|
||||
onSingleLongTapDown: _handleSingleLongTapDown,
|
||||
onSingleLongTapStart: _handleSingleLongTapStart,
|
||||
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
|
||||
onSingleLongTapEnd: _handleSingleLongTapEnd,
|
||||
onDoubleTapDown: _handleDoubleTapDown,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: child,
|
||||
|
||||
@ -1203,7 +1203,7 @@ class RenderEditable extends RenderBox {
|
||||
/// When [ignorePointer] is true, an ancestor widget must respond to tap
|
||||
/// down events by calling this method.
|
||||
void handleTapDown(TapDownDetails details) {
|
||||
_lastTapDownPosition = details.globalPosition + -_paintOffset;
|
||||
_lastTapDownPosition = details.globalPosition;
|
||||
}
|
||||
void _handleTapDown(TapDownDetails details) {
|
||||
assert(!ignorePointer);
|
||||
@ -1259,12 +1259,28 @@ class RenderEditable extends RenderBox {
|
||||
/// programmatically manipulate its `value` or `selection` directly.
|
||||
/// {@endtemplate}
|
||||
void selectPosition({ @required SelectionChangedCause cause }) {
|
||||
selectPositionAt(from: _lastTapDownPosition, cause: cause);
|
||||
}
|
||||
|
||||
/// Select text between the global positions [from] and [to].
|
||||
void selectPositionAt({ @required Offset from, Offset to, @required SelectionChangedCause cause }) {
|
||||
assert(cause != null);
|
||||
assert(from != null);
|
||||
_layoutText(constraints.maxWidth);
|
||||
assert(_lastTapDownPosition != null);
|
||||
if (onSelectionChanged != null) {
|
||||
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition));
|
||||
onSelectionChanged(TextSelection.fromPosition(position), this, cause);
|
||||
final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
|
||||
final TextPosition toPosition = to == null
|
||||
? null
|
||||
: _textPainter.getPositionForOffset(globalToLocal(to - _paintOffset));
|
||||
onSelectionChanged(
|
||||
TextSelection(
|
||||
baseOffset: fromPosition.offset,
|
||||
extentOffset: toPosition?.offset ?? fromPosition.offset,
|
||||
affinity: fromPosition.affinity,
|
||||
),
|
||||
this,
|
||||
cause,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1283,12 +1299,13 @@ class RenderEditable extends RenderBox {
|
||||
/// {@macro flutter.rendering.editable.select}
|
||||
void selectWordsInRange({ @required Offset from, Offset to, @required SelectionChangedCause cause }) {
|
||||
assert(cause != null);
|
||||
assert(from != null);
|
||||
_layoutText(constraints.maxWidth);
|
||||
if (onSelectionChanged != null) {
|
||||
final TextPosition firstPosition = _textPainter.getPositionForOffset(globalToLocal(from + -_paintOffset));
|
||||
final TextPosition firstPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
|
||||
final TextSelection firstWord = _selectWordAtOffset(firstPosition);
|
||||
final TextSelection lastWord = to == null ?
|
||||
firstWord : _selectWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to + -_paintOffset)));
|
||||
firstWord : _selectWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)));
|
||||
|
||||
onSelectionChanged(
|
||||
TextSelection(
|
||||
@ -1308,7 +1325,7 @@ class RenderEditable extends RenderBox {
|
||||
_layoutText(constraints.maxWidth);
|
||||
assert(_lastTapDownPosition != null);
|
||||
if (onSelectionChanged != null) {
|
||||
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition));
|
||||
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition - _paintOffset));
|
||||
final TextRange word = _textPainter.getWordBoundary(position);
|
||||
if (position.offset - word.start <= 1) {
|
||||
onSelectionChanged(
|
||||
|
||||
@ -19,6 +19,10 @@ export 'package:flutter/gestures.dart' show
|
||||
GestureTapCallback,
|
||||
GestureTapCancelCallback,
|
||||
GestureLongPressCallback,
|
||||
GestureLongPressStartCallback,
|
||||
GestureLongPressMoveUpdateCallback,
|
||||
GestureLongPressUpCallback,
|
||||
GestureLongPressEndCallback,
|
||||
GestureDragDownCallback,
|
||||
GestureDragStartCallback,
|
||||
GestureDragUpdateCallback,
|
||||
@ -31,6 +35,9 @@ export 'package:flutter/gestures.dart' show
|
||||
GestureForcePressPeakCallback,
|
||||
GestureForcePressEndCallback,
|
||||
GestureForcePressUpdateCallback,
|
||||
LongPressStartDetails,
|
||||
LongPressMoveUpdateDetails,
|
||||
LongPressEndDetails,
|
||||
ScaleStartDetails,
|
||||
ScaleUpdateDetails,
|
||||
ScaleEndDetails,
|
||||
@ -161,7 +168,10 @@ class GestureDetector extends StatelessWidget {
|
||||
this.onTapCancel,
|
||||
this.onDoubleTap,
|
||||
this.onLongPress,
|
||||
this.onLongPressStart,
|
||||
this.onLongPressMoveUpdate,
|
||||
this.onLongPressUp,
|
||||
this.onLongPressEnd,
|
||||
this.onVerticalDragDown,
|
||||
this.onVerticalDragStart,
|
||||
this.onVerticalDragUpdate,
|
||||
@ -256,13 +266,45 @@ class GestureDetector extends StatelessWidget {
|
||||
/// succession.
|
||||
final GestureTapCallback onDoubleTap;
|
||||
|
||||
/// A pointer has remained in contact with the screen at the same location for
|
||||
/// a long period of time.
|
||||
/// Called when a long press gesture has been recognized.
|
||||
///
|
||||
/// Triggered when a pointer has remained in contact with the screen at the
|
||||
/// same location for a long period of time.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onLongPressStart], which has the same timing but has data for the
|
||||
/// press location.
|
||||
final GestureLongPressCallback onLongPress;
|
||||
|
||||
/// Callback for long press start with gesture location.
|
||||
///
|
||||
/// Triggered when a pointer has remained in contact with the screen at the
|
||||
/// same location for a long period of time.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onLongPress], which has the same timing but without the location data.
|
||||
final GestureLongPressStartCallback onLongPressStart;
|
||||
|
||||
/// A pointer has been drag-moved after a long press.
|
||||
final GestureLongPressMoveUpdateCallback onLongPressMoveUpdate;
|
||||
|
||||
/// A pointer that has triggered a long-press has stopped contacting the screen.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onLongPressEnd], which has the same timing but has data for the up
|
||||
/// gesture location.
|
||||
final GestureLongPressUpCallback onLongPressUp;
|
||||
|
||||
/// A pointer that has triggered a long-press has stopped contacting the screen.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onLongPressUp], which has the same timing but without the location data.
|
||||
final GestureLongPressEndCallback onLongPressEnd;
|
||||
|
||||
/// A pointer has contacted the screen and might begin to move vertically.
|
||||
final GestureDragDownCallback onVerticalDragDown;
|
||||
|
||||
@ -421,12 +463,19 @@ class GestureDetector extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (onLongPress != null || onLongPressUp !=null) {
|
||||
if (onLongPress != null ||
|
||||
onLongPressUp != null ||
|
||||
onLongPressStart != null ||
|
||||
onLongPressMoveUpdate != null ||
|
||||
onLongPressEnd != null) {
|
||||
gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||
() => LongPressGestureRecognizer(debugOwner: this),
|
||||
(LongPressGestureRecognizer instance) {
|
||||
instance
|
||||
..onLongPress = onLongPress
|
||||
..onLongPressStart = onLongPressStart
|
||||
..onLongPressMoveUpdate = onLongPressMoveUpdate
|
||||
..onLongPressEnd =onLongPressEnd
|
||||
..onLongPressUp = onLongPressUp;
|
||||
},
|
||||
);
|
||||
|
||||
@ -616,7 +616,9 @@ class TextSelectionGestureDetector extends StatefulWidget {
|
||||
this.onForcePressEnd,
|
||||
this.onSingleTapUp,
|
||||
this.onSingleTapCancel,
|
||||
this.onSingleLongTapDown,
|
||||
this.onSingleLongTapStart,
|
||||
this.onSingleLongTapMoveUpdate,
|
||||
this.onSingleLongTapEnd,
|
||||
this.onDoubleTapDown,
|
||||
this.behavior,
|
||||
@required this.child,
|
||||
@ -650,7 +652,13 @@ class TextSelectionGestureDetector extends StatefulWidget {
|
||||
/// Called for a single long tap that's sustained for longer than
|
||||
/// [kLongPressTimeout] but not necessarily lifted. Not called for a
|
||||
/// double-tap-hold, which calls [onDoubleTapDown] instead.
|
||||
final GestureLongPressCallback onSingleLongTapDown;
|
||||
final GestureLongPressStartCallback onSingleLongTapStart;
|
||||
|
||||
/// Called after [onSingleLongTapStart] when the pointer is dragged.
|
||||
final GestureLongPressMoveUpdateCallback onSingleLongTapMoveUpdate;
|
||||
|
||||
/// Called after [onSingleLongTapStart] when the pointer is lifted.
|
||||
final GestureLongPressEndCallback onSingleLongTapEnd;
|
||||
|
||||
/// Called after a momentary hold or a short tap that is close in space and
|
||||
/// time (within [kDoubleTapTimeout]) to a previous short tap.
|
||||
@ -734,9 +742,21 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
|
||||
widget.onForcePressEnd(details);
|
||||
}
|
||||
|
||||
void _handleLongPress() {
|
||||
if (!_isDoubleTap && widget.onSingleLongTapDown != null) {
|
||||
widget.onSingleLongTapDown();
|
||||
void _handleLongPressStart(LongPressStartDetails details) {
|
||||
if (!_isDoubleTap && widget.onSingleLongTapStart != null) {
|
||||
widget.onSingleLongTapStart(details);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
|
||||
if (!_isDoubleTap && widget.onSingleLongTapMoveUpdate != null) {
|
||||
widget.onSingleLongTapMoveUpdate(details);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleLongPressUp(LongPressEndDetails details) {
|
||||
if (!_isDoubleTap && widget.onSingleLongTapEnd != null) {
|
||||
widget.onSingleLongTapEnd(details);
|
||||
}
|
||||
_isDoubleTap = false;
|
||||
}
|
||||
@ -764,7 +784,9 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
|
||||
onForcePressStart: widget.onForcePressStart != null ? _forcePressStarted : null,
|
||||
onForcePressEnd: widget.onForcePressEnd != null ? _forcePressEnded : null,
|
||||
onTapCancel: _handleTapCancel,
|
||||
onLongPress: _handleLongPress,
|
||||
onLongPressStart: _handleLongPressStart,
|
||||
onLongPressMoveUpdate: _handleLongPressMoveUpdate,
|
||||
onLongPressEnd: _handleLongPressUp,
|
||||
excludeFromSemantics: true,
|
||||
behavior: widget.behavior,
|
||||
child: widget.child,
|
||||
|
||||
@ -1272,7 +1272,7 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'long press tap is not a double tap',
|
||||
'long press tap cannot initiate a double tap',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
@ -1302,11 +1302,163 @@ void main() {
|
||||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||||
);
|
||||
|
||||
// Collapsed toolbar shows 2 buttons.
|
||||
// The toolbar from the long press is now dismissed by the second tap.
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'long press drag moves the cursor under the drag and shows toolbar on lift',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
final TestGesture gesture =
|
||||
await tester.startGesture(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// Long press on iOS shows collapsed selection cursor.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 3, affinity: TextAffinity.upstream),
|
||||
);
|
||||
// Toolbar only shows up on long press up.
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
|
||||
await gesture.moveBy(const Offset(50, 0));
|
||||
await tester.pump();
|
||||
|
||||
// The selection position is now moved with the drag.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 6, affinity: TextAffinity.upstream),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
|
||||
await gesture.moveBy(const Offset(50, 0));
|
||||
await tester.pump();
|
||||
|
||||
// The selection position is now moved with the drag.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 9, affinity: TextAffinity.upstream),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// The selection isn't affected by the gesture lift.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 9, affinity: TextAffinity.upstream),
|
||||
);
|
||||
// The toolbar now shows up.
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(2));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('long press drag can edge scroll', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final RenderEditable renderEditable = tester.renderObject<RenderEditable>(
|
||||
find.byElementPredicate((Element element) => element.renderObject is RenderEditable)
|
||||
);
|
||||
|
||||
List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||||
);
|
||||
|
||||
expect(lastCharEndpoint.length, 1);
|
||||
// Just testing the test and making sure that the last character is off
|
||||
// the right side of the screen.
|
||||
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(1094.73486328125));
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
final TestGesture gesture =
|
||||
await tester.startGesture(textfieldStart + const Offset(300, 5));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 18, affinity: TextAffinity.upstream),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
|
||||
await gesture.moveBy(const Offset(600, 0));
|
||||
// To the edge of the screen basically.
|
||||
await tester.pump();
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 54, affinity: TextAffinity.upstream),
|
||||
);
|
||||
// Keep moving out.
|
||||
await gesture.moveBy(const Offset(1, 0));
|
||||
await tester.pump();
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 61, affinity: TextAffinity.upstream),
|
||||
);
|
||||
await gesture.moveBy(const Offset(1, 0));
|
||||
await tester.pump();
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
|
||||
); // We're at the edge now.
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// The selection isn't affected by the gesture lift.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
|
||||
);
|
||||
// The toolbar now shows up.
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(2));
|
||||
|
||||
lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||||
);
|
||||
|
||||
expect(lastCharEndpoint.length, 1);
|
||||
// The last character is now on screen.
|
||||
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(786.73486328125));
|
||||
|
||||
final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection(
|
||||
const TextSelection.collapsed(offset: 0), // First character's position.
|
||||
);
|
||||
expect(firstCharEndpoint.length, 1);
|
||||
// The first character is now offscreen to the left.
|
||||
expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-308.20499999821186));
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'long tap after a double tap select is not affected',
|
||||
(WidgetTester tester) async {
|
||||
|
||||
@ -9,153 +9,273 @@ import 'gesture_tester.dart';
|
||||
|
||||
const PointerDownEvent down = PointerDownEvent(
|
||||
pointer: 5,
|
||||
position: Offset(10.0, 10.0)
|
||||
position: Offset(10, 10),
|
||||
);
|
||||
|
||||
const PointerUpEvent up = PointerUpEvent(
|
||||
pointer: 5,
|
||||
position: Offset(11.0, 9.0)
|
||||
position: Offset(11, 9),
|
||||
);
|
||||
|
||||
const PointerMoveEvent move = PointerMoveEvent(
|
||||
pointer: 5,
|
||||
position: Offset(100, 200),
|
||||
);
|
||||
|
||||
void main() {
|
||||
setUp(ensureGestureBinding);
|
||||
|
||||
testGesture('Should recognize long press', (GestureTester tester) {
|
||||
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer();
|
||||
group('Long press', () {
|
||||
LongPressGestureRecognizer longPress;
|
||||
bool longPressDown;
|
||||
bool longPressUp;
|
||||
|
||||
bool longPressRecognized = false;
|
||||
longPress.onLongPress = () {
|
||||
longPressRecognized = true;
|
||||
};
|
||||
setUp(() {
|
||||
longPress = LongPressGestureRecognizer();
|
||||
longPressDown = false;
|
||||
longPress.onLongPress = () {
|
||||
longPressDown = true;
|
||||
};
|
||||
longPressUp = false;
|
||||
longPress.onLongPressUp = () {
|
||||
longPressUp = true;
|
||||
};
|
||||
});
|
||||
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.route(down);
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 700));
|
||||
expect(longPressRecognized, isTrue);
|
||||
testGesture('Should recognize long press', (GestureTester tester) {
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.route(down);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressDown, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 700));
|
||||
expect(longPressDown, isTrue);
|
||||
|
||||
longPress.dispose();
|
||||
longPress.dispose();
|
||||
});
|
||||
|
||||
testGesture('Up cancels long press', (GestureTester tester) {
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.route(down);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressDown, isFalse);
|
||||
tester.route(up);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.async.elapse(const Duration(seconds: 1));
|
||||
expect(longPressDown, isFalse);
|
||||
|
||||
longPress.dispose();
|
||||
});
|
||||
|
||||
testGesture('Moving before accept cancels', (GestureTester tester) {
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.route(down);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressDown, isFalse);
|
||||
tester.route(move);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.async.elapse(const Duration(seconds: 1));
|
||||
tester.route(up);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressDown, isFalse);
|
||||
expect(longPressUp, isFalse);
|
||||
|
||||
longPress.dispose();
|
||||
});
|
||||
|
||||
testGesture('Moving after accept is ok', (GestureTester tester) {
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.route(down);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.async.elapse(const Duration(seconds: 1));
|
||||
expect(longPressDown, isTrue);
|
||||
tester.route(move);
|
||||
tester.route(up);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressDown, isTrue);
|
||||
expect(longPressUp, isTrue);
|
||||
|
||||
longPress.dispose();
|
||||
});
|
||||
|
||||
testGesture('Should recognize both tap down and long press', (GestureTester tester) {
|
||||
final TapGestureRecognizer tap = TapGestureRecognizer();
|
||||
|
||||
bool tapDownRecognized = false;
|
||||
tap.onTapDown = (_) {
|
||||
tapDownRecognized = true;
|
||||
};
|
||||
|
||||
tap.addPointer(down);
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(tapDownRecognized, isFalse);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.route(down);
|
||||
expect(tapDownRecognized, isFalse);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(tapDownRecognized, isTrue);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 700));
|
||||
expect(tapDownRecognized, isTrue);
|
||||
expect(longPressDown, isTrue);
|
||||
|
||||
tap.dispose();
|
||||
longPress.dispose();
|
||||
});
|
||||
|
||||
testGesture('Drag start delayed by microtask', (GestureTester tester) {
|
||||
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
|
||||
|
||||
bool isDangerousStack = false;
|
||||
|
||||
bool dragStartRecognized = false;
|
||||
drag.onStart = (DragStartDetails details) {
|
||||
expect(isDangerousStack, isFalse);
|
||||
dragStartRecognized = true;
|
||||
};
|
||||
|
||||
drag.addPointer(down);
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(dragStartRecognized, isFalse);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.route(down);
|
||||
expect(dragStartRecognized, isFalse);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(dragStartRecognized, isFalse);
|
||||
expect(longPressDown, isFalse);
|
||||
isDangerousStack = true;
|
||||
longPress.dispose();
|
||||
isDangerousStack = false;
|
||||
expect(dragStartRecognized, isFalse);
|
||||
expect(longPressDown, isFalse);
|
||||
tester.async.flushMicrotasks();
|
||||
expect(dragStartRecognized, isTrue);
|
||||
expect(longPressDown, isFalse);
|
||||
drag.dispose();
|
||||
});
|
||||
|
||||
testGesture('Should recognize long press up', (GestureTester tester) {
|
||||
bool longPressUpRecognized = false;
|
||||
longPress.onLongPressUp = () {
|
||||
longPressUpRecognized = true;
|
||||
};
|
||||
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressUpRecognized, isFalse);
|
||||
tester.route(down); // kLongPressTimeout = 500;
|
||||
expect(longPressUpRecognized, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressUpRecognized, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 700));
|
||||
tester.route(up);
|
||||
expect(longPressUpRecognized, isTrue);
|
||||
|
||||
longPress.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
testGesture('Up cancels long press', (GestureTester tester) {
|
||||
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer();
|
||||
group('long press drag', () {
|
||||
LongPressGestureRecognizer longPressDrag;
|
||||
bool longPressStart;
|
||||
bool longPressUp;
|
||||
Offset longPressDragUpdate;
|
||||
|
||||
bool longPressRecognized = false;
|
||||
longPress.onLongPress = () {
|
||||
longPressRecognized = true;
|
||||
};
|
||||
setUp(() {
|
||||
longPressDrag = LongPressGestureRecognizer();
|
||||
longPressStart = false;
|
||||
longPressDrag.onLongPressStart = (LongPressStartDetails details) {
|
||||
longPressStart = true;
|
||||
};
|
||||
longPressUp = false;
|
||||
longPressDrag.onLongPressEnd = (LongPressEndDetails details) {
|
||||
longPressUp = true;
|
||||
};
|
||||
longPressDragUpdate = null;
|
||||
longPressDrag.onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
|
||||
longPressDragUpdate = details.globalPosition;
|
||||
};
|
||||
});
|
||||
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.route(down);
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.route(up);
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.async.elapse(const Duration(seconds: 1));
|
||||
expect(longPressRecognized, isFalse);
|
||||
testGesture('Should recognize long press down', (GestureTester tester) {
|
||||
longPressDrag.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressStart, isFalse);
|
||||
tester.route(down);
|
||||
expect(longPressStart, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressStart, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 700));
|
||||
expect(longPressStart, isTrue);
|
||||
|
||||
longPress.dispose();
|
||||
});
|
||||
longPressDrag.dispose();
|
||||
});
|
||||
|
||||
testGesture('Should recognize both tap down and long press', (GestureTester tester) {
|
||||
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer();
|
||||
final TapGestureRecognizer tap = TapGestureRecognizer();
|
||||
testGesture('Short up cancels long press', (GestureTester tester) {
|
||||
longPressDrag.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressStart, isFalse);
|
||||
tester.route(down);
|
||||
expect(longPressStart, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressStart, isFalse);
|
||||
tester.route(up);
|
||||
expect(longPressStart, isFalse);
|
||||
tester.async.elapse(const Duration(seconds: 1));
|
||||
expect(longPressStart, isFalse);
|
||||
|
||||
bool tapDownRecognized = false;
|
||||
tap.onTapDown = (_) {
|
||||
tapDownRecognized = true;
|
||||
};
|
||||
longPressDrag.dispose();
|
||||
});
|
||||
|
||||
bool longPressRecognized = false;
|
||||
longPress.onLongPress = () {
|
||||
longPressRecognized = true;
|
||||
};
|
||||
testGesture('Moving before accept cancels', (GestureTester tester) {
|
||||
longPressDrag.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressStart, isFalse);
|
||||
tester.route(down);
|
||||
expect(longPressStart, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressStart, isFalse);
|
||||
tester.route(move);
|
||||
expect(longPressStart, isFalse);
|
||||
tester.async.elapse(const Duration(seconds: 1));
|
||||
tester.route(up);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressStart, isFalse);
|
||||
expect(longPressUp, isFalse);
|
||||
|
||||
tap.addPointer(down);
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(tapDownRecognized, isFalse);
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.route(down);
|
||||
expect(tapDownRecognized, isFalse);
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(tapDownRecognized, isTrue);
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 700));
|
||||
expect(tapDownRecognized, isTrue);
|
||||
expect(longPressRecognized, isTrue);
|
||||
longPressDrag.dispose();
|
||||
});
|
||||
|
||||
tap.dispose();
|
||||
longPress.dispose();
|
||||
});
|
||||
testGesture('Moving after accept does not cancel', (GestureTester tester) {
|
||||
longPressDrag.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressStart, isFalse);
|
||||
tester.route(down);
|
||||
expect(longPressStart, isFalse);
|
||||
tester.async.elapse(const Duration(seconds: 1));
|
||||
expect(longPressStart, isTrue);
|
||||
tester.route(move);
|
||||
expect(longPressDragUpdate, const Offset(100, 200));
|
||||
tester.route(up);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressStart, isTrue);
|
||||
expect(longPressUp, isTrue);
|
||||
|
||||
testGesture('Drag start delayed by microtask', (GestureTester tester) {
|
||||
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer();
|
||||
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
|
||||
|
||||
bool isDangerousStack = false;
|
||||
|
||||
bool dragStartRecognized = false;
|
||||
drag.onStart = (DragStartDetails details) {
|
||||
expect(isDangerousStack, isFalse);
|
||||
dragStartRecognized = true;
|
||||
};
|
||||
|
||||
bool longPressRecognized = false;
|
||||
longPress.onLongPress = () {
|
||||
expect(isDangerousStack, isFalse);
|
||||
longPressRecognized = true;
|
||||
};
|
||||
|
||||
drag.addPointer(down);
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(dragStartRecognized, isFalse);
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.route(down);
|
||||
expect(dragStartRecognized, isFalse);
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(dragStartRecognized, isFalse);
|
||||
expect(longPressRecognized, isFalse);
|
||||
isDangerousStack = true;
|
||||
longPress.dispose();
|
||||
isDangerousStack = false;
|
||||
expect(dragStartRecognized, isFalse);
|
||||
expect(longPressRecognized, isFalse);
|
||||
tester.async.flushMicrotasks();
|
||||
expect(dragStartRecognized, isTrue);
|
||||
expect(longPressRecognized, isFalse);
|
||||
drag.dispose();
|
||||
});
|
||||
|
||||
testGesture('Should recognize long press up', (GestureTester tester) {
|
||||
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer();
|
||||
|
||||
bool longPressUpRecognized = false;
|
||||
longPress.onLongPressUp = () {
|
||||
longPressUpRecognized = true;
|
||||
};
|
||||
|
||||
longPress.addPointer(down);
|
||||
tester.closeArena(5);
|
||||
expect(longPressUpRecognized, isFalse);
|
||||
tester.route(down); // kLongPressTimeout = 500;
|
||||
expect(longPressUpRecognized, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 300));
|
||||
expect(longPressUpRecognized, isFalse);
|
||||
tester.async.elapse(const Duration(milliseconds: 700));
|
||||
tester.route(up);
|
||||
expect(longPressUpRecognized, isTrue);
|
||||
|
||||
longPress.dispose();
|
||||
longPressDrag.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -1026,10 +1026,10 @@ void main() {
|
||||
expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue);
|
||||
|
||||
// Now try scrolling by dragging the selection handle.
|
||||
// Long press the 'i' in 'Fourth line' to select the word.
|
||||
// Long press the middle of the word "won't" in the fourth line.
|
||||
final Offset selectedWordPos = textOffsetToPosition(
|
||||
tester,
|
||||
kMoreThanFourLines.indexOf('Fourth line') + 8,
|
||||
kMoreThanFourLines.indexOf('Fourth line') + 14,
|
||||
);
|
||||
|
||||
gesture = await tester.startGesture(selectedWordPos, pointer: 7);
|
||||
@ -1038,8 +1038,13 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(controller.selection.base.offset, 91);
|
||||
expect(controller.selection.extent.offset, 94);
|
||||
expect(controller.selection.base.offset, 77);
|
||||
expect(controller.selection.extent.offset, 82);
|
||||
// Sanity check for the word selected is the intended one.
|
||||
expect(
|
||||
controller.text.substring(controller.selection.baseOffset, controller.selection.extentOffset),
|
||||
"won't",
|
||||
);
|
||||
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints = globalize(
|
||||
@ -4199,7 +4204,7 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'long press tap is not a double tap (iOS)',
|
||||
'long press tap cannot initiate a double tap (iOS)',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
@ -4237,6 +4242,161 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'long press drag moves the cursor under the drag and shows toolbar on lift (iOS)',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(platform: TargetPlatform.iOS),
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||||
|
||||
final TestGesture gesture =
|
||||
await tester.startGesture(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// Long press on iOS shows collapsed selection cursor.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 3, affinity: TextAffinity.downstream),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
|
||||
await gesture.moveBy(const Offset(50, 0));
|
||||
await tester.pump();
|
||||
|
||||
// The selection position is now moved with the drag.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 6, affinity: TextAffinity.downstream),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
|
||||
await gesture.moveBy(const Offset(50, 0));
|
||||
await tester.pump();
|
||||
|
||||
// The selection position is now moved with the drag.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 9, affinity: TextAffinity.downstream),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// The selection isn't affected by the gesture lift.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 9, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// The toolbar now shows up.
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(2));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('long press drag can edge scroll (iOS)', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(platform: TargetPlatform.iOS),
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
|
||||
List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||||
);
|
||||
|
||||
expect(lastCharEndpoint.length, 1);
|
||||
// Just testing the test and making sure that the last character is off
|
||||
// the right side of the screen.
|
||||
expect(lastCharEndpoint[0].point.dx, 1056);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||||
|
||||
final TestGesture gesture =
|
||||
await tester.startGesture(textfieldStart + const Offset(300, 5));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 19, affinity: TextAffinity.upstream),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
|
||||
await gesture.moveBy(const Offset(600, 0));
|
||||
// To the edge of the screen basically.
|
||||
await tester.pump();
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 56, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// Keep moving out.
|
||||
await gesture.moveBy(const Offset(1, 0));
|
||||
await tester.pump();
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 62, affinity: TextAffinity.downstream),
|
||||
);
|
||||
await gesture.moveBy(const Offset(1, 0));
|
||||
await tester.pump();
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
|
||||
); // We're at the edge now.
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// The selection isn't affected by the gesture lift.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
|
||||
);
|
||||
// The toolbar now shows up.
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(2));
|
||||
|
||||
lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||||
);
|
||||
|
||||
expect(lastCharEndpoint.length, 1);
|
||||
// The last character is now on screen near the right edge.
|
||||
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(798, epsilon: 1));
|
||||
|
||||
final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection(
|
||||
const TextSelection.collapsed(offset: 0), // First character's position.
|
||||
);
|
||||
expect(firstCharEndpoint.length, 1);
|
||||
// The first character is now offscreen to the left.
|
||||
expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257, epsilon: 1));
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'long tap after a double tap select is not affected (iOS)',
|
||||
(WidgetTester tester) async {
|
||||
|
||||
@ -254,4 +254,84 @@ void main() {
|
||||
);
|
||||
expect(editable, paintsExactlyCountTimes(#drawRect, 1));
|
||||
});
|
||||
|
||||
test('selects correct place with offsets', () {
|
||||
final TextSelectionDelegate delegate = FakeEditableTextState();
|
||||
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||||
TextSelection currentSelection;
|
||||
final RenderEditable editable = RenderEditable(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
selectionColor: Colors.black,
|
||||
textDirection: TextDirection.ltr,
|
||||
cursorColor: Colors.red,
|
||||
offset: viewportOffset,
|
||||
// This makes the scroll axis vertical.
|
||||
maxLines: 2,
|
||||
textSelectionDelegate: delegate,
|
||||
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
|
||||
currentSelection = selection;
|
||||
},
|
||||
text: const TextSpan(
|
||||
text: 'test\ntest',
|
||||
style: TextStyle(
|
||||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
layout(editable);
|
||||
|
||||
expect(
|
||||
editable,
|
||||
paints..paragraph(offset: Offset.zero),
|
||||
);
|
||||
|
||||
editable.selectPositionAt(from: const Offset(0, 2), cause: SelectionChangedCause.tap);
|
||||
pumpFrame();
|
||||
|
||||
expect(currentSelection.isCollapsed, true);
|
||||
expect(currentSelection.baseOffset, 0);
|
||||
|
||||
viewportOffset.correctBy(10);
|
||||
|
||||
pumpFrame();
|
||||
|
||||
expect(
|
||||
editable,
|
||||
paints..paragraph(offset: const Offset(0, -10)),
|
||||
);
|
||||
|
||||
// Tap the same place. But because the offset is scrolled up, the second line
|
||||
// gets tapped instead.
|
||||
editable.selectPositionAt(from: const Offset(0, 2), cause: SelectionChangedCause.tap);
|
||||
pumpFrame();
|
||||
|
||||
expect(currentSelection.isCollapsed, true);
|
||||
expect(currentSelection.baseOffset, 5);
|
||||
|
||||
// Test the other selection methods.
|
||||
// Move over by one character.
|
||||
editable.handleTapDown(TapDownDetails(globalPosition: const Offset(10, 2)));
|
||||
pumpFrame();
|
||||
editable.selectPosition(cause:SelectionChangedCause.tap);
|
||||
pumpFrame();
|
||||
expect(currentSelection.isCollapsed, true);
|
||||
expect(currentSelection.baseOffset, 6);
|
||||
|
||||
editable.handleTapDown(TapDownDetails(globalPosition: const Offset(20, 2)));
|
||||
pumpFrame();
|
||||
editable.selectWord(cause:SelectionChangedCause.longPress);
|
||||
pumpFrame();
|
||||
expect(currentSelection.isCollapsed, false);
|
||||
expect(currentSelection.baseOffset, 5);
|
||||
expect(currentSelection.extentOffset, 9);
|
||||
|
||||
// Select one more character down but since it's still part of the same
|
||||
// word, the same word is selected.
|
||||
editable.selectWordsInRange(from: const Offset(30, 2), cause:SelectionChangedCause.longPress);
|
||||
pumpFrame();
|
||||
expect(currentSelection.isCollapsed, false);
|
||||
expect(currentSelection.baseOffset, 5);
|
||||
expect(currentSelection.extentOffset, 9);
|
||||
});
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ void main() {
|
||||
int tapCount;
|
||||
int singleTapUpCount;
|
||||
int singleTapCancelCount;
|
||||
int singleLongTapDownCount;
|
||||
int singleLongTapStartCount;
|
||||
int doubleTapDownCount;
|
||||
int forcePressStartCount;
|
||||
int forcePressEndCount;
|
||||
@ -19,7 +19,7 @@ void main() {
|
||||
void _handleTapDown(TapDownDetails details) { tapCount++; }
|
||||
void _handleSingleTapUp(TapUpDetails details) { singleTapUpCount++; }
|
||||
void _handleSingleTapCancel() { singleTapCancelCount++; }
|
||||
void _handleSingleLongTapDown() { singleLongTapDownCount++; }
|
||||
void _handleSingleLongTapStart(LongPressStartDetails details) { singleLongTapStartCount++; }
|
||||
void _handleDoubleTapDown(TapDownDetails details) { doubleTapDownCount++; }
|
||||
void _handleForcePressStart(ForcePressDetails details) { forcePressStartCount++; }
|
||||
void _handleForcePressEnd(ForcePressDetails details) { forcePressEndCount++; }
|
||||
@ -28,7 +28,7 @@ void main() {
|
||||
tapCount = 0;
|
||||
singleTapUpCount = 0;
|
||||
singleTapCancelCount = 0;
|
||||
singleLongTapDownCount = 0;
|
||||
singleLongTapStartCount = 0;
|
||||
doubleTapDownCount = 0;
|
||||
forcePressStartCount = 0;
|
||||
forcePressEndCount = 0;
|
||||
@ -41,7 +41,7 @@ void main() {
|
||||
onTapDown: _handleTapDown,
|
||||
onSingleTapUp: _handleSingleTapUp,
|
||||
onSingleTapCancel: _handleSingleTapCancel,
|
||||
onSingleLongTapDown: _handleSingleLongTapDown,
|
||||
onSingleLongTapStart: _handleSingleLongTapStart,
|
||||
onDoubleTapDown: _handleDoubleTapDown,
|
||||
onForcePressStart: _handleForcePressStart,
|
||||
onForcePressEnd: _handleForcePressEnd,
|
||||
@ -108,7 +108,7 @@ void main() {
|
||||
expect(singleTapCancelCount, 0);
|
||||
expect(doubleTapDownCount, 1);
|
||||
// The double tap down hold supersedes the single tap down.
|
||||
expect(singleLongTapDownCount, 0);
|
||||
expect(singleLongTapStartCount, 0);
|
||||
|
||||
await gesture.up();
|
||||
// Nothing else happens on up.
|
||||
@ -116,7 +116,7 @@ void main() {
|
||||
expect(tapCount, 2);
|
||||
expect(singleTapCancelCount, 0);
|
||||
expect(doubleTapDownCount, 1);
|
||||
expect(singleLongTapDownCount, 0);
|
||||
expect(singleLongTapStartCount, 0);
|
||||
});
|
||||
|
||||
testWidgets('a very quick swipe is just a canceled tap', (WidgetTester tester) async {
|
||||
@ -129,7 +129,7 @@ void main() {
|
||||
expect(tapCount, 0);
|
||||
expect(singleTapCancelCount, 1);
|
||||
expect(doubleTapDownCount, 0);
|
||||
expect(singleLongTapDownCount, 0);
|
||||
expect(singleLongTapStartCount, 0);
|
||||
|
||||
await gesture.up();
|
||||
// Nothing else happens on up.
|
||||
@ -137,7 +137,7 @@ void main() {
|
||||
expect(tapCount, 0);
|
||||
expect(singleTapCancelCount, 1);
|
||||
expect(doubleTapDownCount, 0);
|
||||
expect(singleLongTapDownCount, 0);
|
||||
expect(singleLongTapStartCount, 0);
|
||||
});
|
||||
|
||||
testWidgets('a slower swipe has a tap down and a canceled tap', (WidgetTester tester) async {
|
||||
@ -150,7 +150,7 @@ void main() {
|
||||
expect(tapCount, 1);
|
||||
expect(singleTapCancelCount, 1);
|
||||
expect(doubleTapDownCount, 0);
|
||||
expect(singleLongTapDownCount, 0);
|
||||
expect(singleLongTapStartCount, 0);
|
||||
});
|
||||
|
||||
testWidgets('a force press intiates a force press', (WidgetTester tester) async {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user