diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index 649d6054fc..136e8bb061 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -22,6 +22,7 @@ const double _kTwoPi = 2 * math.PI; const int _kHoursPerDay = 24; const int _kHoursPerPeriod = 12; const int _kMinutesPerHour = 60; +const Duration _kVibrateCommitDelay = const Duration(milliseconds: 100); /// Whether the [TimeOfDay] is before or after noon. enum DayPeriod { @@ -644,12 +645,17 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { _TimePickerMode _mode = _TimePickerMode.hour; TimeOfDay _selectedTime; + Timer _vibrateTimer; void _vibrate() { switch (Theme.of(context).platform) { case TargetPlatform.android: case TargetPlatform.fuchsia: - HapticFeedback.vibrate(); + _vibrateTimer?.cancel(); + _vibrateTimer = new Timer(_kVibrateCommitDelay, () { + HapticFeedback.vibrate(); + _vibrateTimer = null; + }); break; case TargetPlatform.iOS: break; @@ -664,6 +670,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { } void _handleTimeChanged(TimeOfDay value) { + _vibrate(); setState(() { _selectedTime = value; }); @@ -759,6 +766,13 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ) ); } + + @override + void dispose() { + _vibrateTimer?.cancel(); + _vibrateTimer = null; + super.dispose(); + } } /// Shows a dialog containing a material design time picker. diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index da56398273..b6dfd004ea 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; class _TimePickerLauncher extends StatelessWidget { @@ -110,4 +111,92 @@ void main() { await finishPicker(tester); expect(result.hour, equals(9)); }); + + group('haptic feedback', () { + const Duration kFastFeedbackInteral = const Duration(milliseconds: 10); + const Duration kSlowFeedbackInteral = const Duration(milliseconds: 200); + int hapticFeedbackCount; + + setUpAll(() { + PlatformMessages.setMockJSONMessageHandler('flutter/platform', (dynamic message) { + if (message['method'] == "HapticFeedback.vibrate") + hapticFeedbackCount++; + }); + }); + + setUp(() { + hapticFeedbackCount = 0; + }); + + testWidgets('tap-select vibrates once', (WidgetTester tester) async { + Point center = await startPicker(tester, (TimeOfDay time) { }); + await tester.tapAt(new Point(center.x, center.y - 50.0)); + await finishPicker(tester); + expect(hapticFeedbackCount, 1); + }); + + testWidgets('quick successive tap-selects vibrate once', (WidgetTester tester) async { + Point center = await startPicker(tester, (TimeOfDay time) { }); + await tester.tapAt(new Point(center.x, center.y - 50.0)); + await tester.pump(kFastFeedbackInteral); + await tester.tapAt(new Point(center.x, center.y + 50.0)); + await finishPicker(tester); + expect(hapticFeedbackCount, 1); + }); + + testWidgets('slow successive tap-selects vibrate once per tap', (WidgetTester tester) async { + Point center = await startPicker(tester, (TimeOfDay time) { }); + await tester.tapAt(new Point(center.x, center.y - 50.0)); + await tester.pump(kSlowFeedbackInteral); + await tester.tapAt(new Point(center.x, center.y + 50.0)); + await tester.pump(kSlowFeedbackInteral); + await tester.tapAt(new Point(center.x, center.y - 50.0)); + await finishPicker(tester); + expect(hapticFeedbackCount, 3); + }); + + testWidgets('drag-select vibrates once', (WidgetTester tester) async { + Point center = await startPicker(tester, (TimeOfDay time) { }); + Point hour0 = new Point(center.x, center.y - 50.0); + Point hour3 = new Point(center.x + 50.0, center.y); + + TestGesture gesture = await tester.startGesture(hour3); + await gesture.moveBy(hour0 - hour3); + await gesture.up(); + await finishPicker(tester); + expect(hapticFeedbackCount, 1); + }); + + testWidgets('quick drag-select vibrates once', (WidgetTester tester) async { + Point center = await startPicker(tester, (TimeOfDay time) { }); + Point hour0 = new Point(center.x, center.y - 50.0); + Point hour3 = new Point(center.x + 50.0, center.y); + + TestGesture gesture = await tester.startGesture(hour3); + await gesture.moveBy(hour0 - hour3); + await tester.pump(kFastFeedbackInteral); + await gesture.moveBy(hour3 - hour0); + await tester.pump(kFastFeedbackInteral); + await gesture.moveBy(hour0 - hour3); + await gesture.up(); + await finishPicker(tester); + expect(hapticFeedbackCount, 1); + }); + + testWidgets('slow drag-select vibrates once', (WidgetTester tester) async { + Point center = await startPicker(tester, (TimeOfDay time) { }); + Point hour0 = new Point(center.x, center.y - 50.0); + Point hour3 = new Point(center.x + 50.0, center.y); + + TestGesture gesture = await tester.startGesture(hour3); + await gesture.moveBy(hour0 - hour3); + await tester.pump(kSlowFeedbackInteral); + await gesture.moveBy(hour3 - hour0); + await tester.pump(kSlowFeedbackInteral); + await gesture.moveBy(hour0 - hour3); + await gesture.up(); + await finishPicker(tester); + expect(hapticFeedbackCount, 3); + }); + }); }