Unverified Commit 3d8aec2b authored by jslavitz's avatar jslavitz Committed by GitHub

Adds force press gesture detector and recognizer (#24554)

* adds Force Press gesture detector and recognizer
parent bbddade1
......@@ -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';
......
......@@ -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,
......
This diff is collapsed.
......@@ -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,
......
This diff is collapsed.
......@@ -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);
});
}
......@@ -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);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment