Adds force press gesture detector and recognizer (#24554)

* adds Force Press gesture detector and recognizer
This commit is contained in:
jslavitz 2018-12-19 20:09:07 -08:00 committed by GitHub
parent bbddade17f
commit 3d8aec2b99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1027 additions and 2 deletions

View file

@ -16,6 +16,7 @@ export 'src/gestures/drag.dart';
export 'src/gestures/drag_details.dart';
export 'src/gestures/eager.dart';
export 'src/gestures/events.dart';
export 'src/gestures/force_press.dart';
export 'src/gestures/hit_test.dart';
export 'src/gestures/long_press.dart';
export 'src/gestures/lsq_solver.dart';

View file

@ -334,6 +334,7 @@ class PointerAddedEvent extends PointerEvent {
int device = 0,
Offset position = Offset.zero,
bool obscured = false,
double pressure = 0.0,
double pressureMin = 1.0,
double pressureMax = 1.0,
double distance = 0.0,
@ -348,6 +349,7 @@ class PointerAddedEvent extends PointerEvent {
device: device,
position: position,
obscured: obscured,
pressure: pressure,
pressureMin: pressureMin,
pressureMax: pressureMax,
distance: distance,
@ -372,6 +374,7 @@ class PointerRemovedEvent extends PointerEvent {
PointerDeviceKind kind = PointerDeviceKind.touch,
int device = 0,
bool obscured = false,
double pressure = 0.0,
double pressureMin = 1.0,
double pressureMax = 1.0,
double distanceMax = 0.0,
@ -383,11 +386,12 @@ class PointerRemovedEvent extends PointerEvent {
device: device,
position: null,
obscured: obscured,
pressure: pressure,
pressureMin: pressureMin,
pressureMax: pressureMax,
distanceMax: distanceMax,
radiusMin: radiusMin,
radiusMax: radiusMax
radiusMax: radiusMax,
);
}
@ -410,6 +414,7 @@ class PointerHoverEvent extends PointerEvent {
Offset delta = Offset.zero,
int buttons = 0,
bool obscured = false,
double pressure = 0.0,
double pressureMin = 1.0,
double pressureMax = 1.0,
double distance = 0.0,
@ -431,6 +436,7 @@ class PointerHoverEvent extends PointerEvent {
buttons: buttons,
down: false,
obscured: obscured,
pressure: pressure,
pressureMin: pressureMin,
pressureMax: pressureMax,
distance: distance,
@ -567,7 +573,7 @@ class PointerUpEvent extends PointerEvent {
Offset position = Offset.zero,
int buttons = 0,
bool obscured = false,
double pressure = 1.0,
double pressure = 0.0,
double pressureMin = 1.0,
double pressureMax = 1.0,
double distance = 0.0,
@ -616,6 +622,7 @@ class PointerCancelEvent extends PointerEvent {
Offset position = Offset.zero,
int buttons = 0,
bool obscured = false,
double pressure = 0.0,
double pressureMin = 1.0,
double pressureMax = 1.0,
double distance = 0.0,
@ -636,6 +643,7 @@ class PointerCancelEvent extends PointerEvent {
buttons: buttons,
down: false,
obscured: obscured,
pressure: pressure,
pressureMin: pressureMin,
pressureMax: pressureMax,
distance: distance,

View 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';
}

View file

@ -27,6 +27,10 @@ export 'package:flutter/gestures.dart' show
GestureScaleStartCallback,
GestureScaleUpdateCallback,
GestureScaleEndCallback,
GestureForcePressStartCallback,
GestureForcePressPeakCallback,
GestureForcePressEndCallback,
GestureForcePressUpdateCallback,
ScaleStartDetails,
ScaleUpdateDetails,
ScaleEndDetails,
@ -167,6 +171,10 @@ class GestureDetector extends StatelessWidget {
this.onHorizontalDragUpdate,
this.onHorizontalDragEnd,
this.onHorizontalDragCancel,
this.onForcePressStart,
this.onForcePressPeak,
this.onForcePressUpdate,
this.onForcePressEnd,
this.onPanDown,
this.onPanStart,
this.onPanUpdate,
@ -318,6 +326,37 @@ class GestureDetector extends StatelessWidget {
/// The pointers are no longer in contact with the screen.
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.
///
/// 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(
gestures: gestures,
behavior: behavior,

View 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);
});
}

View file

@ -343,4 +343,146 @@ void main() {
await longPress(kLongPressTimeout + const Duration(seconds: 1)); // To make sure the time for long press has occurred
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);
});
}

View file

@ -43,6 +43,25 @@ class TestPointer {
Offset get location => _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.
///
/// 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 HitTestResult _result;
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.
Future<void> moveBy(Offset offset, { Duration timeStamp = Duration.zero }) {
assert(_pointer._isDown);