From b09e64e142aeed2833b2df2be8edb701e4243af6 Mon Sep 17 00:00:00 2001 From: xster Date: Tue, 26 Feb 2019 14:16:25 -0800 Subject: [PATCH] Support iOS devices reporting pressure data of 0 (#28478) --- .../flutter/lib/src/gestures/force_press.dart | 10 +- .../test/cupertino/text_field_test.dart | 106 +++++++++----- .../test/gestures/force_press_test.dart | 130 ++++++------------ .../test/material/text_field_test.dart | 67 +++++++-- 4 files changed, 179 insertions(+), 134 deletions(-) diff --git a/packages/flutter/lib/src/gestures/force_press.dart b/packages/flutter/lib/src/gestures/force_press.dart index 39b00183be..6296a3f1f1 100644 --- a/packages/flutter/lib/src/gestures/force_press.dart +++ b/packages/flutter/lib/src/gestures/force_press.dart @@ -206,12 +206,10 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer { @override void addPointer(PointerEvent event) { - assert(event.pressureMax >= 1.0); - // If the device has a maximum pressure of less than or equal to 1, - // indicating a faux pressure sensor on this device or a device without a - // pressure sensor (ie. on a non iOS device) we want do not want any - // callbacks to be called. - if (!(event is PointerUpEvent) && event.pressureMax == 1.0) { + // If the device has a maximum pressure of less than or equal to 1, it + // doesn't have touch pressure sensing capabilities. Do not participate + // in the gesture arena. + if (!(event is PointerUpEvent) && event.pressureMax <= 1.0) { resolve(GestureDisposition.rejected); } else { startTrackingPointer(event.pointer); diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index f6b9775939..566eb4fab2 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -1537,6 +1537,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12), ); + // Shows toolbar. expect(find.byType(CupertinoButton), findsNWidgets(3)); }, ); @@ -1607,44 +1608,85 @@ void main() { ); testWidgets('force press selects word', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'Atwater Peel Sherbrooke Bonaventure', - ); - await tester.pumpWidget( - CupertinoApp( - home: Center( - child: CupertinoTextField( - controller: controller, - ), + final TextEditingController controller = TextEditingController( + text: 'Atwater Peel Sherbrooke Bonaventure', + ); + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, ), ), - ); + ), + ); - final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); - const int pointerValue = 1; - final TestGesture gesture = await tester.createGesture(); - await gesture.downWithCustomEvent( - textfieldStart + const Offset(150.0, 5.0), - PointerDownEvent( - pointer: pointerValue, - position: textfieldStart + const Offset(150.0, 5.0), - pressure: 3.0, - pressureMax: 6.0, - pressureMin: 0.0 + const int pointerValue = 1; + final TestGesture gesture = await tester.createGesture(); + await gesture.downWithCustomEvent( + textfieldStart + const Offset(150.0, 5.0), + PointerDownEvent( + pointer: pointerValue, + position: textfieldStart + const Offset(150.0, 5.0), + pressure: 3.0, + pressureMax: 6.0, + pressureMin: 0.0 + ), + ); + // We expect the force press to select a word at the given location. + expect( + controller.selection, + const TextSelection(baseOffset: 8, extentOffset: 12), + ); + + await gesture.up(); + await tester.pump(); + // Shows toolbar. + expect(find.byType(CupertinoButton), findsNWidgets(3)); + }); + + testWidgets('force press on unsupported devices falls back to tap', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'Atwater Peel Sherbrooke Bonaventure', + ); + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + ), ), - ); - // We expect the force press to select a word at the given location. - expect( - controller.selection, - const TextSelection(baseOffset: 8, extentOffset: 12), - ); + ), + ); - await gesture.up(); - await tester.pumpAndSettle(); - expect(find.byType(CupertinoButton), findsNWidgets(3)); - }, - ); + final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + + const int pointerValue = 1; + final TestGesture gesture = await tester.createGesture(); + await gesture.downWithCustomEvent( + textfieldStart + const Offset(150.0, 5.0), + PointerDownEvent( + pointer: pointerValue, + position: textfieldStart + const Offset(150.0, 5.0), + // iPhone 6 and below report 0 across the board. + pressure: 0, + pressureMax: 0, + pressureMin: 0, + ), + ); + await gesture.up(); + // Fall back to a single tap which selects the edge of the word. + expect( + controller.selection, + const TextSelection.collapsed(offset: 8), + ); + + await tester.pump(); + // Falling back to a single tap doesn't trigger a toolbar. + expect(find.byType(CupertinoButton), findsNothing); + }); testWidgets( 'text field respects theme', diff --git a/packages/flutter/test/gestures/force_press_test.dart b/packages/flutter/test/gestures/force_press_test.dart index 8cfb830ee5..770eccb962 100644 --- a/packages/flutter/test/gestures/force_press_test.dart +++ b/packages/flutter/test/gestures/force_press_test.dart @@ -109,97 +109,55 @@ void main() { expect(ended, 1); }); - testGesture('Force presses are not recognized on devices with low maxmium pressure', (GestureTester tester) { - // Device specific constants that represent those from the iPhone X - const double pressureMin = 0; - const double pressureMax = 1.0; + testGesture('Invalid pressure ranges capabilities are not recognized', (GestureTester tester) { + void testGestureWithMaxPressure(double pressureMax) { + int started = 0; + int peaked = 0; + int updated = 0; + int ended = 0; - // Interpolated Flutter pressure values. - const double startPressure = 0.4; // = Device pressure of 2.66. - const double peakPressure = 0.85; // = Device pressure of 5.66. + final ForcePressGestureRecognizer force = ForcePressGestureRecognizer(); - int started = 0; - int peaked = 0; - int updated = 0; - int ended = 0; + force.onStart = (ForcePressDetails details) => started += 1; + force.onPeak = (ForcePressDetails details) => peaked += 1; + force.onUpdate = (ForcePressDetails details) => updated += 1; + force.onEnd = (ForcePressDetails details) => ended += 1; - void onStart(ForcePressDetails details) { - started += 1; + const int pointerValue = 1; + final TestPointer pointer = TestPointer(pointerValue); + final PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: const Offset(10.0, 10.0), pressure: 0, pressureMin: 0, 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(PointerMoveEvent(pointer: pointerValue, position: const Offset(10.0, 10.0), pressure: 10, pressureMin: 0, pressureMax: pressureMax)); + + // Regardless of current pressure, this recognizer shouldn't participate or + // trigger any callbacks. + expect(started, 0); + expect(peaked, 0); + expect(updated, 0); + expect(ended, 0); + + tester.route(pointer.up()); + + // There should still be no callbacks. + expect(started, 0); + expect(updated, 0); + expect(peaked, 0); + expect(ended, 0); } - 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, 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)); - 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, 0); - expect(updated, 0); - expect(peaked, 0); - expect(ended, 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, 0); - expect(updated, 0); - 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: 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, 0); - expect(updated, 0); - expect(peaked, 0); - expect(ended, 0); - - tester.route(pointer.up()); - - // We have ended the gesture so ended should be true. - expect(started, 0); - expect(updated, 0); - expect(peaked, 0); - expect(ended, 0); + testGestureWithMaxPressure(0); + testGestureWithMaxPressure(1); + testGestureWithMaxPressure(-1); + testGestureWithMaxPressure(0.5); }); testGesture('If minimum pressure is not reached, start and end callbacks are not called', (GestureTester tester) { diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 7642cfbc69..8477b3d1bd 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -4083,6 +4083,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12), ); + // The toolbar is still showing. expect(find.byType(CupertinoButton), findsNWidgets(3)); }, ); @@ -4272,6 +4273,7 @@ void main() { controller.selection, const TextSelection.collapsed(offset: 3, affinity: TextAffinity.downstream), ); + // Cursor move doesn't trigger a toolbar initially. expect(find.byType(CupertinoButton), findsNothing); await gesture.moveBy(const Offset(50, 0)); @@ -4282,6 +4284,7 @@ void main() { controller.selection, const TextSelection.collapsed(offset: 6, affinity: TextAffinity.downstream), ); + // Still no toolbar. expect(find.byType(CupertinoButton), findsNothing); await gesture.moveBy(const Offset(50, 0)); @@ -4292,6 +4295,7 @@ void main() { controller.selection, const TextSelection.collapsed(offset: 9, affinity: TextAffinity.downstream), ); + // Still no toolbar. expect(find.byType(CupertinoButton), findsNothing); await gesture.up(); @@ -4554,7 +4558,6 @@ void main() { ); testWidgets('force press does not select a word on (android)', (WidgetTester tester) async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); @@ -4588,27 +4591,26 @@ void main() { expect(controller.selection, const TextSelection.collapsed(offset: -1)); await gesture.up(); - await tester.pumpAndSettle(); + await tester.pump(); expect(find.byType(FlatButton), findsNothing); - debugDefaultTargetPlatformOverride = null; }); testWidgets('force press selects word (iOS)', (WidgetTester tester) async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( - CupertinoApp( - home: Center( - child: CupertinoTextField( + MaterialApp( + theme: ThemeData(platform: TargetPlatform.iOS), + home: Material( + child: TextField( controller: controller, ), ), ), ); - final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); const int pointerValue = 1; final Offset offset = textfieldStart + const Offset(150.0, 5.0); @@ -4632,9 +4634,54 @@ void main() { ); await gesture.up(); - await tester.pumpAndSettle(); + await tester.pump(); expect(find.byType(CupertinoButton), findsNWidgets(3)); - debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('tap on non-force-press-supported devices work (iOS)', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'Atwater Peel Sherbrooke Bonaventure', + ); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(platform: TargetPlatform.iOS), + home: Material( + child: TextField( + controller: controller, + ), + ), + ), + ); + + final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + + const int pointerValue = 1; + final Offset offset = textfieldStart + const Offset(150.0, 5.0); + final TestGesture gesture = await tester.createGesture(); + await gesture.downWithCustomEvent( + offset, + PointerDownEvent( + pointer: pointerValue, + position: offset, + // iPhone 6 and below report 0 across the board. + pressure: 0, + pressureMax: 0, + pressureMin: 0, + ), + ); + + await gesture.updateWithCustomEvent(PointerMoveEvent(pointer: pointerValue, position: textfieldStart + const Offset(150.0, 5.0), pressure: 0.5, pressureMin: 0, pressureMax: 1)); + await gesture.up(); + // The event should fallback to a normal tap and move the cursor. + // Single taps selects the edge of the word. + expect( + controller.selection, + const TextSelection.collapsed(offset: 8), + ); + + await tester.pump(); + // Single taps shouldn't trigger the toolbar. + expect(find.byType(CupertinoButton), findsNothing); }); testWidgets('default TextField debugFillProperties', (WidgetTester tester) async {