mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Adds force press gesture detector and recognizer (#24554)
* adds Force Press gesture detector and recognizer
This commit is contained in:
parent
bbddade17f
commit
3d8aec2b99
|
@ -16,6 +16,7 @@ export 'src/gestures/drag.dart';
|
||||||
export 'src/gestures/drag_details.dart';
|
export 'src/gestures/drag_details.dart';
|
||||||
export 'src/gestures/eager.dart';
|
export 'src/gestures/eager.dart';
|
||||||
export 'src/gestures/events.dart';
|
export 'src/gestures/events.dart';
|
||||||
|
export 'src/gestures/force_press.dart';
|
||||||
export 'src/gestures/hit_test.dart';
|
export 'src/gestures/hit_test.dart';
|
||||||
export 'src/gestures/long_press.dart';
|
export 'src/gestures/long_press.dart';
|
||||||
export 'src/gestures/lsq_solver.dart';
|
export 'src/gestures/lsq_solver.dart';
|
||||||
|
|
|
@ -334,6 +334,7 @@ class PointerAddedEvent extends PointerEvent {
|
||||||
int device = 0,
|
int device = 0,
|
||||||
Offset position = Offset.zero,
|
Offset position = Offset.zero,
|
||||||
bool obscured = false,
|
bool obscured = false,
|
||||||
|
double pressure = 0.0,
|
||||||
double pressureMin = 1.0,
|
double pressureMin = 1.0,
|
||||||
double pressureMax = 1.0,
|
double pressureMax = 1.0,
|
||||||
double distance = 0.0,
|
double distance = 0.0,
|
||||||
|
@ -348,6 +349,7 @@ class PointerAddedEvent extends PointerEvent {
|
||||||
device: device,
|
device: device,
|
||||||
position: position,
|
position: position,
|
||||||
obscured: obscured,
|
obscured: obscured,
|
||||||
|
pressure: pressure,
|
||||||
pressureMin: pressureMin,
|
pressureMin: pressureMin,
|
||||||
pressureMax: pressureMax,
|
pressureMax: pressureMax,
|
||||||
distance: distance,
|
distance: distance,
|
||||||
|
@ -372,6 +374,7 @@ class PointerRemovedEvent extends PointerEvent {
|
||||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||||
int device = 0,
|
int device = 0,
|
||||||
bool obscured = false,
|
bool obscured = false,
|
||||||
|
double pressure = 0.0,
|
||||||
double pressureMin = 1.0,
|
double pressureMin = 1.0,
|
||||||
double pressureMax = 1.0,
|
double pressureMax = 1.0,
|
||||||
double distanceMax = 0.0,
|
double distanceMax = 0.0,
|
||||||
|
@ -383,11 +386,12 @@ class PointerRemovedEvent extends PointerEvent {
|
||||||
device: device,
|
device: device,
|
||||||
position: null,
|
position: null,
|
||||||
obscured: obscured,
|
obscured: obscured,
|
||||||
|
pressure: pressure,
|
||||||
pressureMin: pressureMin,
|
pressureMin: pressureMin,
|
||||||
pressureMax: pressureMax,
|
pressureMax: pressureMax,
|
||||||
distanceMax: distanceMax,
|
distanceMax: distanceMax,
|
||||||
radiusMin: radiusMin,
|
radiusMin: radiusMin,
|
||||||
radiusMax: radiusMax
|
radiusMax: radiusMax,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,6 +414,7 @@ class PointerHoverEvent extends PointerEvent {
|
||||||
Offset delta = Offset.zero,
|
Offset delta = Offset.zero,
|
||||||
int buttons = 0,
|
int buttons = 0,
|
||||||
bool obscured = false,
|
bool obscured = false,
|
||||||
|
double pressure = 0.0,
|
||||||
double pressureMin = 1.0,
|
double pressureMin = 1.0,
|
||||||
double pressureMax = 1.0,
|
double pressureMax = 1.0,
|
||||||
double distance = 0.0,
|
double distance = 0.0,
|
||||||
|
@ -431,6 +436,7 @@ class PointerHoverEvent extends PointerEvent {
|
||||||
buttons: buttons,
|
buttons: buttons,
|
||||||
down: false,
|
down: false,
|
||||||
obscured: obscured,
|
obscured: obscured,
|
||||||
|
pressure: pressure,
|
||||||
pressureMin: pressureMin,
|
pressureMin: pressureMin,
|
||||||
pressureMax: pressureMax,
|
pressureMax: pressureMax,
|
||||||
distance: distance,
|
distance: distance,
|
||||||
|
@ -567,7 +573,7 @@ class PointerUpEvent extends PointerEvent {
|
||||||
Offset position = Offset.zero,
|
Offset position = Offset.zero,
|
||||||
int buttons = 0,
|
int buttons = 0,
|
||||||
bool obscured = false,
|
bool obscured = false,
|
||||||
double pressure = 1.0,
|
double pressure = 0.0,
|
||||||
double pressureMin = 1.0,
|
double pressureMin = 1.0,
|
||||||
double pressureMax = 1.0,
|
double pressureMax = 1.0,
|
||||||
double distance = 0.0,
|
double distance = 0.0,
|
||||||
|
@ -616,6 +622,7 @@ class PointerCancelEvent extends PointerEvent {
|
||||||
Offset position = Offset.zero,
|
Offset position = Offset.zero,
|
||||||
int buttons = 0,
|
int buttons = 0,
|
||||||
bool obscured = false,
|
bool obscured = false,
|
||||||
|
double pressure = 0.0,
|
||||||
double pressureMin = 1.0,
|
double pressureMin = 1.0,
|
||||||
double pressureMax = 1.0,
|
double pressureMax = 1.0,
|
||||||
double distance = 0.0,
|
double distance = 0.0,
|
||||||
|
@ -636,6 +643,7 @@ class PointerCancelEvent extends PointerEvent {
|
||||||
buttons: buttons,
|
buttons: buttons,
|
||||||
down: false,
|
down: false,
|
||||||
obscured: obscured,
|
obscured: obscured,
|
||||||
|
pressure: pressure,
|
||||||
pressureMin: pressureMin,
|
pressureMin: pressureMin,
|
||||||
pressureMax: pressureMax,
|
pressureMax: pressureMax,
|
||||||
distance: distance,
|
distance: distance,
|
||||||
|
|
308
packages/flutter/lib/src/gestures/force_press.dart
Normal file
308
packages/flutter/lib/src/gestures/force_press.dart
Normal file
|
@ -0,0 +1,308 @@
|
||||||
|
// Copyright 2018 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 'dart:ui' show Offset;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'arena.dart';
|
||||||
|
import 'constants.dart';
|
||||||
|
import 'events.dart';
|
||||||
|
import 'recognizer.dart';
|
||||||
|
|
||||||
|
enum _ForceState {
|
||||||
|
// No pointer has touched down and the detector is ready for a pointer down to occur.
|
||||||
|
ready,
|
||||||
|
|
||||||
|
// A pointer has touched down, but a force press gesture has not yet been detected.
|
||||||
|
possible,
|
||||||
|
|
||||||
|
// A pointer is down and a force press gesture has been detected. However, if
|
||||||
|
// the ForcePressGestureRecognizer is the only recognizer in the arena, thus
|
||||||
|
// accepted as soon as the gesture state is possible, the gesture will not
|
||||||
|
// yet have started.
|
||||||
|
accepted,
|
||||||
|
|
||||||
|
// A pointer is down and the gesture has started, ie. the pressure of the pointer
|
||||||
|
// has just become greater than the ForcePressGestureRecognizer.startPressure.
|
||||||
|
started,
|
||||||
|
|
||||||
|
// A pointer is down and the pressure of the pointer has just become greater
|
||||||
|
// than the ForcePressGestureRecognizer.peakPressure. Even after a pointer
|
||||||
|
// crosses this threshold, onUpdate callbacks will still be sent.
|
||||||
|
peaked,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Details object for callbacks that use [GestureForcePressStartCallback],
|
||||||
|
/// [GestureForcePressPeakCallback], [GestureForcePressEndCallback] or
|
||||||
|
/// [GestureForcePressUpdateCallback].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [ForcePressGestureRecognizer.onStart], [ForcePressGestureRecognizer.onPeak],
|
||||||
|
/// [ForcePressGestureRecognizer.onEnd], and [ForcePressGestureRecognizer.onUpdate]
|
||||||
|
/// which use [ForcePressDetails].
|
||||||
|
/// * [ForcePressUpdateDetails], the details for [ForcePressUpdateCallback].
|
||||||
|
class ForcePressDetails {
|
||||||
|
/// Creates details for a [GestureForcePressStartCallback],
|
||||||
|
/// [GestureForcePressPeakCallback] or [GestureForcePressEndCallback].
|
||||||
|
///
|
||||||
|
/// The [globalPosition] argument must not be null.
|
||||||
|
ForcePressDetails({
|
||||||
|
@required this.globalPosition,
|
||||||
|
@required this.pressure,
|
||||||
|
}) : assert(globalPosition != null),
|
||||||
|
assert(pressure != null);
|
||||||
|
|
||||||
|
/// The global position at which the function was called.
|
||||||
|
final Offset globalPosition;
|
||||||
|
|
||||||
|
/// The pressure of the pointer on the screen.
|
||||||
|
final double pressure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signature used by a [ForcePressGestureRecognizer] for when a pointer has
|
||||||
|
/// pressed with at least [ForcePressGestureRecognizer.startPressure].
|
||||||
|
typedef GestureForcePressStartCallback = void Function(ForcePressDetails details);
|
||||||
|
|
||||||
|
/// Signature used by [ForcePressGestureRecognizer] for when a pointer that has
|
||||||
|
/// pressed with at least [ForcePressGestureRecognizer.peakPressure].
|
||||||
|
typedef GestureForcePressPeakCallback = void Function(ForcePressDetails details);
|
||||||
|
|
||||||
|
/// Signature used by [ForcePressGestureRecognizer] during the frames
|
||||||
|
/// after the triggering of a [ForcePressGestureRecognizer.onStart] callback.
|
||||||
|
typedef GestureForcePressUpdateCallback = void Function(ForcePressDetails details);
|
||||||
|
|
||||||
|
/// Signature for when the pointer that previously triggered a
|
||||||
|
/// [ForcePressGestureRecognizer.onStart] callback is no longer in contact
|
||||||
|
/// with the screen.
|
||||||
|
typedef GestureForcePressEndCallback = void Function(ForcePressDetails details);
|
||||||
|
|
||||||
|
/// Signature used by [ForcePressGestureRecognizer] for interpolating the raw
|
||||||
|
/// device pressure to a value in the range [0, 1] given the device's pressure
|
||||||
|
/// min and pressure max.
|
||||||
|
typedef GestureForceInterpolation = double Function(double pressureMin, double pressureMax, double pressure);
|
||||||
|
|
||||||
|
/// Recognizes a force press on devices that have force sensors.
|
||||||
|
///
|
||||||
|
/// Only the force from a single pointer is used to invoke events. A tap
|
||||||
|
/// recognizer will win against this recognizer on pointer up as long as the
|
||||||
|
/// pointer has not pressed with a force greater than
|
||||||
|
/// [ForcePressGestureRecognizer.startPressure]. A long press recognizer will
|
||||||
|
/// win when the press down time exceeds the threshold time as long as the
|
||||||
|
/// pointer's pressure was never greater than
|
||||||
|
/// [ForcePressGestureRecognizer.startPressure] in that duration.
|
||||||
|
///
|
||||||
|
/// As of November, 2018 iPhone devices of generation 6S and higher have
|
||||||
|
/// force touch functionality, with the exception of the iPhone XR. In addition,
|
||||||
|
/// a small handful of Android devices have this functionality as well.
|
||||||
|
///
|
||||||
|
/// Reported pressure will always be in the range [0.0, 1.0], where 1.0 is
|
||||||
|
/// maximum pressure and 0.0 is minimum pressure. If using a non-linear
|
||||||
|
/// interpolation equation, the pressure reported will correspond with the
|
||||||
|
/// custom curve. (ie. if the interpolation maps t(0.5) -> 0.1, a value of 0.1
|
||||||
|
/// will be reported at a device pressure value of 0.5).
|
||||||
|
///
|
||||||
|
class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
|
/// Creates a force press gesture recognizer.
|
||||||
|
///
|
||||||
|
/// The [startPressure] defaults to 0.4, and [peakPressure] defaults to 0.85
|
||||||
|
/// where a value of 0.0 is no pressure and a value of 1.0 is maximum pressure.
|
||||||
|
///
|
||||||
|
/// [startPressure], [peakPressure] and [interpolation] must not be null.
|
||||||
|
/// [peakPressure] must be greater than [startPressure]. [interpolation] must
|
||||||
|
/// always return a value in the range [0.0, 1.0] where
|
||||||
|
/// pressureMin <= pressure <= pressureMax.
|
||||||
|
ForcePressGestureRecognizer({
|
||||||
|
this.startPressure = 0.4,
|
||||||
|
this.peakPressure = 0.85,
|
||||||
|
this.interpolation = _inverseLerp,
|
||||||
|
Object debugOwner,
|
||||||
|
}) : assert(startPressure != null),
|
||||||
|
assert(peakPressure != null),
|
||||||
|
assert(interpolation != null),
|
||||||
|
assert(peakPressure > startPressure),
|
||||||
|
super(debugOwner: debugOwner);
|
||||||
|
|
||||||
|
/// A pointer is in contact with the screen and has just pressed with a force
|
||||||
|
/// exceeding the [startPressure]. Consequently, if there were other gesture
|
||||||
|
/// detectors, only the force press gesture will be detected and all others
|
||||||
|
/// will be rejected.
|
||||||
|
///
|
||||||
|
/// The position of the pointer is provided in the callback's `details`
|
||||||
|
/// argument, which is a [ForcePressDetails] object.
|
||||||
|
GestureForcePressStartCallback onStart;
|
||||||
|
|
||||||
|
/// A pointer is in contact with the screen and is either moving on the plane
|
||||||
|
/// of the screen, pressing the screen with varying forces or both
|
||||||
|
/// simultaneously.
|
||||||
|
///
|
||||||
|
/// This callback will be invoked for every pointer event after the invocation
|
||||||
|
/// of [onStart] and/or [onPeak] and before the invocation of [onEnd], no
|
||||||
|
/// matter what the pressure is during this time period. The position and
|
||||||
|
/// pressure of the pointer is provided in the callback's `details` argument,
|
||||||
|
/// which is a [ForcePressUpdateDetails] object.
|
||||||
|
GestureForcePressUpdateCallback onUpdate;
|
||||||
|
|
||||||
|
/// A pointer is in contact with the screen and has just pressed with a force
|
||||||
|
/// exceeding the [peakPressure]. This is an arbitrary second level action
|
||||||
|
/// threshold and isn't necessarily the maximum possible device pressure
|
||||||
|
/// (which is 1.0).
|
||||||
|
///
|
||||||
|
/// The position of the pointer is provided in the callback's `details`
|
||||||
|
/// argument, which is a [ForcePressDetails] object.
|
||||||
|
GestureForcePressPeakCallback onPeak;
|
||||||
|
|
||||||
|
/// A pointer is no longer in contact with the screen.
|
||||||
|
///
|
||||||
|
/// The position of the pointer is provided in the callback's `details`
|
||||||
|
/// argument, which is a [ForcePressDetails] object.
|
||||||
|
GestureForcePressEndCallback onEnd;
|
||||||
|
|
||||||
|
/// The pressure of the press required to initiate a force press.
|
||||||
|
///
|
||||||
|
/// A value of 0.0 is no pressure, and 1.0 is maximum pressure.
|
||||||
|
final double startPressure;
|
||||||
|
|
||||||
|
/// The pressure of the press required to peak a force press.
|
||||||
|
///
|
||||||
|
/// A value of 0.0 is no pressure, and 1.0 is maximum pressure. This value
|
||||||
|
/// must be greater than [startPressure].
|
||||||
|
final double peakPressure;
|
||||||
|
|
||||||
|
/// The function used to convert the raw device pressure values into a value
|
||||||
|
/// in the range [0, 1].
|
||||||
|
///
|
||||||
|
/// The function takes in the device's min, max and raw touch
|
||||||
|
/// pressure and returns a value in the range [0.0, 1.0] denoting the
|
||||||
|
/// interpolated touch pressure.
|
||||||
|
///
|
||||||
|
/// This function must always return values in the range [0, 1] when
|
||||||
|
/// pressureMin <= pressure <= pressureMax.
|
||||||
|
///
|
||||||
|
/// By default, the the function is a simple linear interpolation, however,
|
||||||
|
/// changing the function could be useful to accommodate variations in the way
|
||||||
|
/// different devices respond to pressure, change how animations from pressure
|
||||||
|
/// feedback are rendered or for other custom functionality.
|
||||||
|
///
|
||||||
|
/// For example, an ease in curve can be used to determine the interpolated
|
||||||
|
/// value:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// static double interpolateWithEasing(double min, double max, double t) {
|
||||||
|
/// final double lerp = (t - min) / (max - min);
|
||||||
|
/// return Curves.easeIn.transform(lerp);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
final GestureForceInterpolation interpolation;
|
||||||
|
|
||||||
|
Offset _lastPosition;
|
||||||
|
double _lastPressure;
|
||||||
|
_ForceState _state = _ForceState.ready;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addPointer(PointerEvent event) {
|
||||||
|
startTrackingPointer(event.pointer);
|
||||||
|
if (_state == _ForceState.ready) {
|
||||||
|
_state = _ForceState.possible;
|
||||||
|
_lastPosition = event.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void handleEvent(PointerEvent event) {
|
||||||
|
assert(_state != _ForceState.ready);
|
||||||
|
// A static pointer with changes in pressure creates PointerMoveEvent events.
|
||||||
|
if (event is PointerMoveEvent || event is PointerDownEvent) {
|
||||||
|
final double pressure = interpolation(event.pressureMin, event.pressureMax, event.pressure);
|
||||||
|
assert(pressure.isNaN ? true : (pressure <= 1.0 && pressure >= 0.0));
|
||||||
|
_lastPosition = event.position;
|
||||||
|
_lastPressure = pressure;
|
||||||
|
|
||||||
|
if (_state == _ForceState.possible) {
|
||||||
|
if (pressure > startPressure) {
|
||||||
|
_state = _ForceState.started;
|
||||||
|
resolve(GestureDisposition.accepted);
|
||||||
|
} else if (event.delta.distanceSquared > kTouchSlop) {
|
||||||
|
resolve(GestureDisposition.rejected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// In case this is the only gesture detector we still don't want to start
|
||||||
|
// the gesture until the pressure is greater than the startPressure.
|
||||||
|
if (pressure > startPressure && _state == _ForceState.accepted) {
|
||||||
|
_state = _ForceState.started;
|
||||||
|
if (onStart != null) {
|
||||||
|
invokeCallback<void>('onStart', () => onStart(ForcePressDetails(
|
||||||
|
pressure: pressure,
|
||||||
|
globalPosition: _lastPosition,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (onPeak != null && pressure > peakPressure &&
|
||||||
|
(_state == _ForceState.started)) {
|
||||||
|
_state = _ForceState.peaked;
|
||||||
|
if (onPeak != null) {
|
||||||
|
invokeCallback<void>('onPeak', () => onPeak(ForcePressDetails(
|
||||||
|
pressure: pressure,
|
||||||
|
globalPosition: event.position,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (onUpdate != null &&
|
||||||
|
(_state == _ForceState.started || _state == _ForceState.peaked)) {
|
||||||
|
if (onUpdate != null) {
|
||||||
|
invokeCallback<void>('onUpdate', () => onUpdate(ForcePressDetails(
|
||||||
|
pressure: pressure,
|
||||||
|
globalPosition: event.position,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopTrackingIfPointerNoLongerDown(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void acceptGesture(int pointer) {
|
||||||
|
if (_state == _ForceState.possible)
|
||||||
|
_state = _ForceState.accepted;
|
||||||
|
|
||||||
|
if (onStart != null && _state == _ForceState.started) {
|
||||||
|
invokeCallback<void>('onStart', () => onStart(ForcePressDetails(
|
||||||
|
pressure: _lastPressure,
|
||||||
|
globalPosition: _lastPosition,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didStopTrackingLastPointer(int pointer) {
|
||||||
|
final bool wasAccepted = _state == _ForceState.started || _state == _ForceState.peaked;
|
||||||
|
if (_state == _ForceState.possible) {
|
||||||
|
resolve(GestureDisposition.rejected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (wasAccepted && onEnd != null) {
|
||||||
|
if (onEnd != null) {
|
||||||
|
invokeCallback<void>('onEnd', () => onEnd(ForcePressDetails(
|
||||||
|
pressure: 0.0,
|
||||||
|
globalPosition: _lastPosition,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_state = _ForceState.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void rejectGesture(int pointer) {
|
||||||
|
stopTrackingPointer(pointer);
|
||||||
|
didStopTrackingLastPointer(pointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static double _inverseLerp(double min, double max, double t) {
|
||||||
|
return (t - min) / (max - min);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get debugDescription => 'force press';
|
||||||
|
}
|
|
@ -27,6 +27,10 @@ export 'package:flutter/gestures.dart' show
|
||||||
GestureScaleStartCallback,
|
GestureScaleStartCallback,
|
||||||
GestureScaleUpdateCallback,
|
GestureScaleUpdateCallback,
|
||||||
GestureScaleEndCallback,
|
GestureScaleEndCallback,
|
||||||
|
GestureForcePressStartCallback,
|
||||||
|
GestureForcePressPeakCallback,
|
||||||
|
GestureForcePressEndCallback,
|
||||||
|
GestureForcePressUpdateCallback,
|
||||||
ScaleStartDetails,
|
ScaleStartDetails,
|
||||||
ScaleUpdateDetails,
|
ScaleUpdateDetails,
|
||||||
ScaleEndDetails,
|
ScaleEndDetails,
|
||||||
|
@ -167,6 +171,10 @@ class GestureDetector extends StatelessWidget {
|
||||||
this.onHorizontalDragUpdate,
|
this.onHorizontalDragUpdate,
|
||||||
this.onHorizontalDragEnd,
|
this.onHorizontalDragEnd,
|
||||||
this.onHorizontalDragCancel,
|
this.onHorizontalDragCancel,
|
||||||
|
this.onForcePressStart,
|
||||||
|
this.onForcePressPeak,
|
||||||
|
this.onForcePressUpdate,
|
||||||
|
this.onForcePressEnd,
|
||||||
this.onPanDown,
|
this.onPanDown,
|
||||||
this.onPanStart,
|
this.onPanStart,
|
||||||
this.onPanUpdate,
|
this.onPanUpdate,
|
||||||
|
@ -318,6 +326,37 @@ class GestureDetector extends StatelessWidget {
|
||||||
/// The pointers are no longer in contact with the screen.
|
/// The pointers are no longer in contact with the screen.
|
||||||
final GestureScaleEndCallback onScaleEnd;
|
final GestureScaleEndCallback onScaleEnd;
|
||||||
|
|
||||||
|
/// The pointer is in contact with the screen and has pressed with sufficient
|
||||||
|
/// force to initiate a force press. The amount of force is at least
|
||||||
|
/// [ForcePressGestureRecognizer.startPressure].
|
||||||
|
///
|
||||||
|
/// Note that this callback will only be fired on devices with pressure
|
||||||
|
/// detecting screens.
|
||||||
|
final GestureForcePressStartCallback onForcePressStart;
|
||||||
|
|
||||||
|
/// The pointer is in contact with the screen and has pressed with the maximum
|
||||||
|
/// force. The amount of force is at least
|
||||||
|
/// [ForcePressGestureRecognizer.peakPressure].
|
||||||
|
///
|
||||||
|
/// Note that this callback will only be fired on devices with pressure
|
||||||
|
/// detecting screens.
|
||||||
|
final GestureForcePressPeakCallback onForcePressPeak;
|
||||||
|
|
||||||
|
/// A pointer is in contact with the screen, has previously passed the
|
||||||
|
/// [ForcePressGestureRecognizer.startPressure] and is either moving on the
|
||||||
|
/// plane of the screen, pressing the screen with varying forces or both
|
||||||
|
/// simultaneously.
|
||||||
|
///
|
||||||
|
/// Note that this callback will only be fired on devices with pressure
|
||||||
|
/// detecting screens.
|
||||||
|
final GestureForcePressUpdateCallback onForcePressUpdate;
|
||||||
|
|
||||||
|
/// The pointer is no longer in contact with the screen.
|
||||||
|
///
|
||||||
|
/// Note that this callback will only be fired on devices with pressure
|
||||||
|
/// detecting screens.
|
||||||
|
final GestureForcePressEndCallback onForcePressEnd;
|
||||||
|
|
||||||
/// How this gesture detector should behave during hit testing.
|
/// How this gesture detector should behave during hit testing.
|
||||||
///
|
///
|
||||||
/// This defaults to [HitTestBehavior.deferToChild] if [child] is not null and
|
/// This defaults to [HitTestBehavior.deferToChild] if [child] is not null and
|
||||||
|
@ -435,6 +474,22 @@ class GestureDetector extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onForcePressStart != null ||
|
||||||
|
onForcePressPeak != null ||
|
||||||
|
onForcePressUpdate != null ||
|
||||||
|
onForcePressEnd != null) {
|
||||||
|
gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
|
||||||
|
() => ForcePressGestureRecognizer(debugOwner: this),
|
||||||
|
(ForcePressGestureRecognizer instance) {
|
||||||
|
instance
|
||||||
|
..onStart = onForcePressStart
|
||||||
|
..onPeak = onForcePressPeak
|
||||||
|
..onUpdate = onForcePressUpdate
|
||||||
|
..onEnd = onForcePressEnd;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return RawGestureDetector(
|
return RawGestureDetector(
|
||||||
gestures: gestures,
|
gestures: gestures,
|
||||||
behavior: behavior,
|
behavior: behavior,
|
||||||
|
|
452
packages/flutter/test/gestures/force_press_test.dart
Normal file
452
packages/flutter/test/gestures/force_press_test.dart
Normal file
|
@ -0,0 +1,452 @@
|
||||||
|
// Copyright 2018 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_test/flutter_test.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'gesture_tester.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUp(ensureGestureBinding);
|
||||||
|
|
||||||
|
testGesture('A force press can be recognized', (GestureTester tester) {
|
||||||
|
|
||||||
|
// Device specific constants that represent those from the iPhone X
|
||||||
|
const double pressureMin = 0;
|
||||||
|
const double pressureMax = 6.66;
|
||||||
|
|
||||||
|
// Interpolated Flutter pressure values.
|
||||||
|
const double startPressure = 0.4; // = Device pressure of 2.66.
|
||||||
|
const double peakPressure = 0.85; // = Device pressure of 5.66.
|
||||||
|
|
||||||
|
int started = 0;
|
||||||
|
int peaked = 0;
|
||||||
|
int updated = 0;
|
||||||
|
int ended = 0;
|
||||||
|
|
||||||
|
Offset startGlobalPosition;
|
||||||
|
|
||||||
|
void onStart(ForcePressDetails details) {
|
||||||
|
startGlobalPosition = details.globalPosition;
|
||||||
|
started += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ForcePressGestureRecognizer force = ForcePressGestureRecognizer(startPressure: startPressure, peakPressure: peakPressure);
|
||||||
|
|
||||||
|
force.onStart = onStart;
|
||||||
|
force.onPeak = (ForcePressDetails details) => peaked += 1;
|
||||||
|
force.onUpdate = (ForcePressDetails details) => updated += 1;
|
||||||
|
force.onEnd = (ForcePressDetails details) => ended += 1;
|
||||||
|
|
||||||
|
const int pointerValue = 1;
|
||||||
|
final TestPointer pointer = TestPointer(pointerValue);
|
||||||
|
const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 0, pressureMin: pressureMin, pressureMax: pressureMax);
|
||||||
|
pointer.setDownInfo(down, const Offset(10.0, 10.0));
|
||||||
|
force.addPointer(down);
|
||||||
|
tester.closeArena(pointerValue);
|
||||||
|
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
// Pressure fed into the test environment simulates the values received directly from the device.
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.5, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// We have not hit the start pressure, so no events should be true.
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.8, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// We have just hit the start pressure so just the start event should be triggered and one update call should have occurred.
|
||||||
|
expect(started, 1);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 1);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.3, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 5.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 1.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// We have exceeded the start pressure so update should be greater than 0.
|
||||||
|
expect(started, 1);
|
||||||
|
expect(updated, 5);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
expect(startGlobalPosition, const Offset(10.0, 10.0));
|
||||||
|
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 6.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// We have exceeded the peak pressure so peak pressure should be true.
|
||||||
|
expect(started, 1);
|
||||||
|
expect(updated, 6);
|
||||||
|
expect(peaked, 1);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.3, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 5.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 1.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// Update is still called.
|
||||||
|
expect(started, 1);
|
||||||
|
expect(updated, 10);
|
||||||
|
expect(peaked, 1);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
tester.route(pointer.up());
|
||||||
|
|
||||||
|
// We have ended the gesture so ended should be true.
|
||||||
|
expect(started, 1);
|
||||||
|
expect(updated, 10);
|
||||||
|
expect(peaked, 1);
|
||||||
|
expect(ended, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testGesture('If minimum pressure is not reached, start and end callbacks are not called', (GestureTester tester) {
|
||||||
|
// Device specific constants that represent those from the iPhone X
|
||||||
|
const double pressureMin = 0;
|
||||||
|
const double pressureMax = 6.66;
|
||||||
|
|
||||||
|
// Interpolated Flutter pressure values.
|
||||||
|
const double startPressure = 0.4; // = Device pressure of 2.66.
|
||||||
|
const double peakPressure = 0.85; // = Device pressure of 5.66.
|
||||||
|
|
||||||
|
int started = 0;
|
||||||
|
int peaked = 0;
|
||||||
|
int updated = 0;
|
||||||
|
int ended = 0;
|
||||||
|
|
||||||
|
final ForcePressGestureRecognizer force = ForcePressGestureRecognizer(startPressure: startPressure, peakPressure: peakPressure);
|
||||||
|
|
||||||
|
force.onStart = (_) => started += 1;
|
||||||
|
force.onPeak = (_) => peaked += 1;
|
||||||
|
force.onUpdate = (_) => updated += 1;
|
||||||
|
force.onEnd = (_) => ended += 1;
|
||||||
|
|
||||||
|
const int pointerValue = 1;
|
||||||
|
final TestPointer pointer = TestPointer(pointerValue);
|
||||||
|
const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 0, pressureMin: pressureMin, pressureMax: pressureMax);
|
||||||
|
pointer.setDownInfo(down, const Offset(10.0, 10.0));
|
||||||
|
force.addPointer(down);
|
||||||
|
tester.closeArena(1);
|
||||||
|
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
// Pressure fed into the test environment simulates the values received directly from the device.
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.5, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// We have not hit the start pressure, so no events should be true.
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
tester.route(pointer.up());
|
||||||
|
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testGesture('Should recognize drag and not force touch if there is a drag recognizer', (GestureTester tester) {
|
||||||
|
final PanGestureRecognizer drag = PanGestureRecognizer();
|
||||||
|
|
||||||
|
// Device specific constants that represent those from the iPhone X
|
||||||
|
const double pressureMin = 0;
|
||||||
|
const double pressureMax = 6.66;
|
||||||
|
|
||||||
|
// Interpolated Flutter pressure values.
|
||||||
|
const double startPressure = 0.4; // = Device pressure of 2.66.
|
||||||
|
const double peakPressure = 0.85; // = Device pressure of 5.66.
|
||||||
|
|
||||||
|
int started = 0;
|
||||||
|
int peaked = 0;
|
||||||
|
int updated = 0;
|
||||||
|
int ended = 0;
|
||||||
|
|
||||||
|
final ForcePressGestureRecognizer force = ForcePressGestureRecognizer(startPressure: startPressure, peakPressure: peakPressure);
|
||||||
|
|
||||||
|
force.onStart = (_) => started += 1;
|
||||||
|
force.onPeak = (_) => peaked += 1;
|
||||||
|
force.onUpdate = (_) => updated += 1;
|
||||||
|
force.onEnd = (_) => ended += 1;
|
||||||
|
|
||||||
|
int didStartPan = 0;
|
||||||
|
drag.onStart = (_) => didStartPan += 1;
|
||||||
|
|
||||||
|
const int pointerValue = 1;
|
||||||
|
final TestPointer pointer = TestPointer(pointerValue);
|
||||||
|
const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 1.0, pressureMin: pressureMin, pressureMax: pressureMax);
|
||||||
|
pointer.setDownInfo(down, const Offset(10.0, 10.0));
|
||||||
|
force.addPointer(down);
|
||||||
|
drag.addPointer(down);
|
||||||
|
tester.closeArena(1);
|
||||||
|
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
expect(didStartPan, 0);
|
||||||
|
|
||||||
|
tester.route(pointer.move(const Offset(30.0, 30.0))); // moved 20 horizontally and 20 vertically which is 28 total
|
||||||
|
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
expect(didStartPan, 1);
|
||||||
|
|
||||||
|
// Pressure fed into the test environment simulates the values received directly from the device.
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.5, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// We have not hit the start pressure, so no events should be true.
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
expect(didStartPan, 1);
|
||||||
|
|
||||||
|
// We don't expect any events from the force press recognizer.
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
expect(didStartPan, 1);
|
||||||
|
|
||||||
|
tester.route(pointer.up());
|
||||||
|
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
expect(didStartPan, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testGesture('Should not call ended on pointer up if the gesture was never accepted', (GestureTester tester) {
|
||||||
|
final PanGestureRecognizer drag = PanGestureRecognizer();
|
||||||
|
|
||||||
|
// Interpolated Flutter pressure values.
|
||||||
|
const double startPressure = 0.4; // = Device pressure of 2.66.
|
||||||
|
const double peakPressure = 0.85; // = Device pressure of 5.66.
|
||||||
|
|
||||||
|
// Device specific constants that represent those from the iPhone X
|
||||||
|
const double pressureMin = 0;
|
||||||
|
const double pressureMax = 6.66;
|
||||||
|
|
||||||
|
int started = 0;
|
||||||
|
int peaked = 0;
|
||||||
|
int updated = 0;
|
||||||
|
int ended = 0;
|
||||||
|
|
||||||
|
final ForcePressGestureRecognizer force = ForcePressGestureRecognizer(startPressure: startPressure, peakPressure: peakPressure);
|
||||||
|
|
||||||
|
force.onStart = (_) => started += 1;
|
||||||
|
force.onPeak = (_) => peaked += 1;
|
||||||
|
force.onUpdate = (_) => updated += 1;
|
||||||
|
force.onEnd = (_) => ended += 1;
|
||||||
|
|
||||||
|
int didStartPan = 0;
|
||||||
|
drag.onStart = (_) => didStartPan += 1;
|
||||||
|
|
||||||
|
const int pointerValue = 1;
|
||||||
|
final TestPointer pointer = TestPointer(pointerValue);
|
||||||
|
const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 1.0, pressureMin: pressureMin, pressureMax: pressureMax);
|
||||||
|
pointer.setDownInfo(down, const Offset(10.0, 10.0));
|
||||||
|
force.addPointer(down);
|
||||||
|
drag.addPointer(down);
|
||||||
|
tester.closeArena(1);
|
||||||
|
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
expect(didStartPan, 0);
|
||||||
|
|
||||||
|
tester.route(pointer.up());
|
||||||
|
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
expect(didStartPan, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testGesture('Should call start only once if there is a competing gesture recognizer', (GestureTester tester) {
|
||||||
|
final PanGestureRecognizer drag = PanGestureRecognizer();
|
||||||
|
|
||||||
|
// Interpolated Flutter pressure values.
|
||||||
|
const double startPressure = 0.4; // = Device pressure of 2.66.
|
||||||
|
const double peakPressure = 0.85; // = Device pressure of 5.66.
|
||||||
|
|
||||||
|
// Device specific constants that represent those from the iPhone X
|
||||||
|
const double pressureMin = 0;
|
||||||
|
const double pressureMax = 6.66;
|
||||||
|
|
||||||
|
int started = 0;
|
||||||
|
int peaked = 0;
|
||||||
|
int updated = 0;
|
||||||
|
int ended = 0;
|
||||||
|
|
||||||
|
final ForcePressGestureRecognizer force = ForcePressGestureRecognizer(startPressure: startPressure, peakPressure: peakPressure);
|
||||||
|
|
||||||
|
force.onStart = (_) => started += 1;
|
||||||
|
force.onPeak = (_) => peaked += 1;
|
||||||
|
force.onUpdate = (_) => updated += 1;
|
||||||
|
force.onEnd = (_) => ended += 1;
|
||||||
|
|
||||||
|
int didStartPan = 0;
|
||||||
|
drag.onStart = (_) => didStartPan += 1;
|
||||||
|
|
||||||
|
const int pointerValue = 1;
|
||||||
|
final TestPointer pointer = TestPointer(pointerValue);
|
||||||
|
const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 1.0, pressureMin: pressureMin, pressureMax: pressureMax);
|
||||||
|
pointer.setDownInfo(down, const Offset(10.0, 10.0));
|
||||||
|
force.addPointer(down);
|
||||||
|
drag.addPointer(down);
|
||||||
|
tester.closeArena(1);
|
||||||
|
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
expect(didStartPan, 0);
|
||||||
|
|
||||||
|
// Pressure fed into the test environment simulates the values received directly from the device.
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// We have not hit the start pressure, so no events should be true.
|
||||||
|
expect(started, 1);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 1);
|
||||||
|
expect(ended, 0);
|
||||||
|
expect(didStartPan, 0);
|
||||||
|
|
||||||
|
tester.route(pointer.up());
|
||||||
|
|
||||||
|
expect(started, 1);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 1);
|
||||||
|
expect(ended, 1);
|
||||||
|
expect(didStartPan, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testGesture('A force press can be recognized with a custom interpolation function', (GestureTester tester) {
|
||||||
|
|
||||||
|
// Device specific constants that represent those from the iPhone X
|
||||||
|
const double pressureMin = 0;
|
||||||
|
const double pressureMax = 6.66;
|
||||||
|
|
||||||
|
// Interpolated Flutter pressure values.
|
||||||
|
const double startPressure = 0.4; // = Device pressure of 2.66.
|
||||||
|
const double peakPressure = 0.85; // = Device pressure of 5.66.
|
||||||
|
|
||||||
|
int started = 0;
|
||||||
|
int peaked = 0;
|
||||||
|
int updated = 0;
|
||||||
|
int ended = 0;
|
||||||
|
|
||||||
|
Offset startGlobalPosition;
|
||||||
|
|
||||||
|
void onStart(ForcePressDetails details) {
|
||||||
|
startGlobalPosition = details.globalPosition;
|
||||||
|
started += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
double interpolateWithEasing(double min, double max, double t) {
|
||||||
|
final double lerp = (t - min) / (max - min);
|
||||||
|
return Curves.easeIn.transform(lerp);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ForcePressGestureRecognizer force = ForcePressGestureRecognizer(startPressure: startPressure, peakPressure: peakPressure, interpolation: interpolateWithEasing);
|
||||||
|
|
||||||
|
force.onStart = onStart;
|
||||||
|
force.onPeak = (ForcePressDetails details) => peaked += 1;
|
||||||
|
force.onUpdate = (ForcePressDetails details) => updated += 1;
|
||||||
|
force.onEnd = (ForcePressDetails details) => ended += 1;
|
||||||
|
|
||||||
|
const int pointerValue = 1;
|
||||||
|
final TestPointer pointer = TestPointer(pointerValue);
|
||||||
|
const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 0, pressureMin: pressureMin, pressureMax: pressureMax);
|
||||||
|
pointer.setDownInfo(down, const Offset(10.0, 10.0));
|
||||||
|
force.addPointer(down);
|
||||||
|
tester.closeArena(pointerValue);
|
||||||
|
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
// Pressure fed into the test environment simulates the values received directly from the device.
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.5, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// We have not hit the start pressure, so no events should be true.
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.8, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// We have just hit the start pressure so just the start event should be triggered and one update call should have occurred.
|
||||||
|
expect(started, 0);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(updated, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.3, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
expect(started, 0);
|
||||||
|
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
expect(started, 1);
|
||||||
|
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 5.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 1.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// We have exceeded the start pressure so update should be greater than 0.
|
||||||
|
expect(started, 1);
|
||||||
|
expect(updated, 3);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
expect(startGlobalPosition, const Offset(10.0, 10.0));
|
||||||
|
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 6.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// We have exceeded the peak pressure so peak pressure should be true.
|
||||||
|
expect(started, 1);
|
||||||
|
expect(updated, 4);
|
||||||
|
expect(peaked, 0);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.3, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 6.5, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 1.0, pressureMin: pressureMin, pressureMax: pressureMax));
|
||||||
|
|
||||||
|
// Update is still called.
|
||||||
|
expect(started, 1);
|
||||||
|
expect(updated, 8);
|
||||||
|
expect(peaked, 1);
|
||||||
|
expect(ended, 0);
|
||||||
|
|
||||||
|
tester.route(pointer.up());
|
||||||
|
|
||||||
|
// We have ended the gesture so ended should be true.
|
||||||
|
expect(started, 1);
|
||||||
|
expect(updated, 8);
|
||||||
|
expect(peaked, 1);
|
||||||
|
expect(ended, 1);
|
||||||
|
});
|
||||||
|
}
|
|
@ -343,4 +343,146 @@ void main() {
|
||||||
await longPress(kLongPressTimeout + const Duration(seconds: 1)); // To make sure the time for long press has occurred
|
await longPress(kLongPressTimeout + const Duration(seconds: 1)); // To make sure the time for long press has occurred
|
||||||
expect(longPressUp, 1);
|
expect(longPressUp, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Force Press Callback called after force press', (WidgetTester tester) async {
|
||||||
|
int forcePressStart = 0;
|
||||||
|
int forcePressPeaked = 0;
|
||||||
|
int forcePressUpdate = 0;
|
||||||
|
int forcePressEnded = 0;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
height: 100.0,
|
||||||
|
color: const Color(0xFF00FF00),
|
||||||
|
child: GestureDetector(
|
||||||
|
onForcePressStart: (_) => forcePressStart += 1,
|
||||||
|
onForcePressEnd: (_) => forcePressEnded += 1,
|
||||||
|
onForcePressPeak: (_) => forcePressPeaked += 1,
|
||||||
|
onForcePressUpdate: (_) => forcePressUpdate += 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final TestGesture gesture = await tester.startGesture(const Offset(400.0, 50.0));
|
||||||
|
const int pointerValue = 1;
|
||||||
|
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.3, pressureMin: 0, pressureMax: 1));
|
||||||
|
|
||||||
|
expect(forcePressStart, 0);
|
||||||
|
expect(forcePressPeaked, 0);
|
||||||
|
expect(forcePressUpdate, 0);
|
||||||
|
expect(forcePressEnded, 0);
|
||||||
|
|
||||||
|
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.5, pressureMin: 0, pressureMax: 1));
|
||||||
|
|
||||||
|
expect(forcePressStart, 1);
|
||||||
|
expect(forcePressPeaked, 0);
|
||||||
|
expect(forcePressUpdate, 1);
|
||||||
|
expect(forcePressEnded, 0);
|
||||||
|
|
||||||
|
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.6, pressureMin: 0, pressureMax: 1));
|
||||||
|
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.7, pressureMin: 0, pressureMax: 1));
|
||||||
|
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.2, pressureMin: 0, pressureMax: 1));
|
||||||
|
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.3, pressureMin: 0, pressureMax: 1));
|
||||||
|
|
||||||
|
expect(forcePressStart, 1);
|
||||||
|
expect(forcePressPeaked, 0);
|
||||||
|
expect(forcePressUpdate, 5);
|
||||||
|
expect(forcePressEnded, 0);
|
||||||
|
|
||||||
|
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.9, pressureMin: 0, pressureMax: 1));
|
||||||
|
|
||||||
|
expect(forcePressStart, 1);
|
||||||
|
expect(forcePressPeaked, 1);
|
||||||
|
expect(forcePressUpdate, 6);
|
||||||
|
expect(forcePressEnded, 0);
|
||||||
|
|
||||||
|
await gesture.up();
|
||||||
|
|
||||||
|
expect(forcePressStart, 1);
|
||||||
|
expect(forcePressPeaked, 1);
|
||||||
|
expect(forcePressUpdate, 6);
|
||||||
|
expect(forcePressEnded, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Force Press Callback not called if long press triggered before force press', (WidgetTester tester) async {
|
||||||
|
int forcePressStart = 0;
|
||||||
|
int longPressTimes = 0;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
height: 100.0,
|
||||||
|
color: const Color(0xFF00FF00),
|
||||||
|
child: GestureDetector(
|
||||||
|
onForcePressStart: (_) => forcePressStart += 1,
|
||||||
|
onLongPress: () => longPressTimes += 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final TestGesture gesture = await tester.startGesture(const Offset(400.0, 50.0));
|
||||||
|
const int pointerValue = 1;
|
||||||
|
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(400.0, 50.0), pressure: 0.3, pressureMin: 0, pressureMax: 1));
|
||||||
|
|
||||||
|
expect(forcePressStart, 0);
|
||||||
|
expect(longPressTimes, 0);
|
||||||
|
|
||||||
|
// Trigger the long press.
|
||||||
|
await tester.pump(kLongPressTimeout + const Duration(seconds: 1));
|
||||||
|
|
||||||
|
expect(longPressTimes, 1);
|
||||||
|
expect(forcePressStart, 0);
|
||||||
|
|
||||||
|
// Failed attempt to trigger the force press.
|
||||||
|
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(400.0, 50.0), pressure: 0.5, pressureMin: 0, pressureMax: 1));
|
||||||
|
|
||||||
|
expect(longPressTimes, 1);
|
||||||
|
expect(forcePressStart, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Force Press Callback not called if drag triggered before force press', (WidgetTester tester) async {
|
||||||
|
int forcePressStart = 0;
|
||||||
|
int horizontalDragStart = 0;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
height: 100.0,
|
||||||
|
color: const Color(0xFF00FF00),
|
||||||
|
child: GestureDetector(
|
||||||
|
onForcePressStart: (_) => forcePressStart += 1,
|
||||||
|
onHorizontalDragStart: (_) => horizontalDragStart += 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
|
||||||
|
const int pointerValue = 1;
|
||||||
|
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.3, pressureMin: 0, pressureMax: 1));
|
||||||
|
|
||||||
|
expect(forcePressStart, 0);
|
||||||
|
expect(horizontalDragStart, 0);
|
||||||
|
|
||||||
|
// Trigger horizontal drag.
|
||||||
|
await gesture.moveBy(const Offset(100, 0));
|
||||||
|
|
||||||
|
expect(horizontalDragStart, 1);
|
||||||
|
expect(forcePressStart, 0);
|
||||||
|
|
||||||
|
// Failed attempt to trigger the force press.
|
||||||
|
await gesture.updateWithCustomEvent(const PointerMoveEvent(pointer: pointerValue, position: Offset(0.0, 0.0), pressure: 0.5, pressureMin: 0, pressureMax: 1));
|
||||||
|
|
||||||
|
expect(horizontalDragStart, 1);
|
||||||
|
expect(forcePressStart, 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,25 @@ class TestPointer {
|
||||||
Offset get location => _location;
|
Offset get location => _location;
|
||||||
Offset _location;
|
Offset _location;
|
||||||
|
|
||||||
|
/// If a custom event is created outside of this class, this function is used
|
||||||
|
/// to set the [isDown].
|
||||||
|
bool setDownInfo(PointerEvent event, Offset newLocation) {
|
||||||
|
_location = newLocation;
|
||||||
|
switch (event.runtimeType) {
|
||||||
|
case PointerDownEvent:
|
||||||
|
assert(!isDown);
|
||||||
|
_isDown = true;
|
||||||
|
break;
|
||||||
|
case PointerUpEvent:
|
||||||
|
case PointerCancelEvent:
|
||||||
|
assert(isDown);
|
||||||
|
_isDown = false;
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return isDown;
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a [PointerDownEvent] at the given location.
|
/// Create a [PointerDownEvent] at the given location.
|
||||||
///
|
///
|
||||||
/// By default, the time stamp on the event is [Duration.zero]. You
|
/// By default, the time stamp on the event is [Duration.zero]. You
|
||||||
|
@ -157,10 +176,50 @@ class TestGesture {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a [TestGesture] by starting with a custom [PointerDownEvent] at the
|
||||||
|
/// given point.
|
||||||
|
///
|
||||||
|
/// By default, the pointer identifier used is 1. This can be overridden by
|
||||||
|
/// providing the `pointer` argument.
|
||||||
|
///
|
||||||
|
/// A function to use for hit testing should be provided via the `hitTester`
|
||||||
|
/// argument, and a function to use for dispatching events should be provided
|
||||||
|
/// via the `dispatcher` argument.
|
||||||
|
static Future<TestGesture> downWithCustomEvent(Offset downLocation, PointerDownEvent downEvent, {
|
||||||
|
int pointer = 1,
|
||||||
|
@required HitTester hitTester,
|
||||||
|
@required EventDispatcher dispatcher,
|
||||||
|
}) async {
|
||||||
|
assert(hitTester != null);
|
||||||
|
assert(dispatcher != null);
|
||||||
|
TestGesture result;
|
||||||
|
return TestAsyncUtils.guard<void>(() async {
|
||||||
|
// dispatch down event
|
||||||
|
final HitTestResult hitTestResult = hitTester(downLocation);
|
||||||
|
final TestPointer testPointer = TestPointer(pointer);
|
||||||
|
testPointer.setDownInfo(downEvent, downLocation);
|
||||||
|
await dispatcher(downEvent, hitTestResult);
|
||||||
|
// create a TestGesture
|
||||||
|
result = TestGesture._(dispatcher, hitTestResult, testPointer);
|
||||||
|
}).then<TestGesture>((void value) {
|
||||||
|
return result;
|
||||||
|
}, onError: (dynamic error, StackTrace stack) {
|
||||||
|
return Future<TestGesture>.error(error, stack);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
final EventDispatcher _dispatcher;
|
final EventDispatcher _dispatcher;
|
||||||
final HitTestResult _result;
|
final HitTestResult _result;
|
||||||
final TestPointer _pointer;
|
final TestPointer _pointer;
|
||||||
|
|
||||||
|
/// Send a move event moving the pointer by the given offset.
|
||||||
|
Future<void> updateWithCustomEvent(PointerEvent event, { Duration timeStamp = Duration.zero }) {
|
||||||
|
_pointer.setDownInfo(event, event.position);
|
||||||
|
return TestAsyncUtils.guard<void>(() {
|
||||||
|
return _dispatcher(event, _result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Send a move event moving the pointer by the given offset.
|
/// Send a move event moving the pointer by the given offset.
|
||||||
Future<void> moveBy(Offset offset, { Duration timeStamp = Duration.zero }) {
|
Future<void> moveBy(Offset offset, { Duration timeStamp = Duration.zero }) {
|
||||||
assert(_pointer._isDown);
|
assert(_pointer._isDown);
|
||||||
|
|
Loading…
Reference in a new issue