Commit fe40eed3 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Provide haptic/acoustic feedback for tap & long-press on Android (#10920)

* Provide haptic/acoustic feedback for tap & long-press on Android

* review comments

* fixed example code

* review comments

* comment fix
parent 9adb4a78
......@@ -42,6 +42,7 @@ export 'src/material/dropdown.dart';
export 'src/material/expand_icon.dart';
export 'src/material/expansion_panel.dart';
export 'src/material/expansion_tile.dart';
export 'src/material/feedback.dart';
export 'src/material/flat_button.dart';
export 'src/material/flexible_space_bar.dart';
export 'src/material/floating_action_button.dart';
......
......@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'debug.dart';
import 'feedback.dart';
import 'icons.dart';
import 'tooltip.dart';
......@@ -102,7 +103,7 @@ class Chip extends StatelessWidget {
if (deletable) {
rightPadding = 0.0;
children.add(new GestureDetector(
onTap: onDeleted,
onTap: Feedback.wrapForTap(onDeleted, context),
child: new Tooltip(
message: 'Delete "$label"',
child: new Container(
......
......@@ -17,6 +17,7 @@ import 'button_bar.dart';
import 'colors.dart';
import 'debug.dart';
import 'dialog.dart';
import 'feedback.dart';
import 'flat_button.dart';
import 'icon_button.dart';
import 'icons.dart';
......@@ -120,11 +121,11 @@ class _DatePickerHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new GestureDetector(
onTap: () => _handleChangeMode(_DatePickerMode.year),
onTap: Feedback.wrapForTap(() => _handleChangeMode(_DatePickerMode.year), context),
child: new Text(new DateFormat('yyyy').format(selectedDate), style: yearStyle),
),
new GestureDetector(
onTap: () => _handleChangeMode(_DatePickerMode.day),
onTap: Feedback.wrapForTap(() => _handleChangeMode(_DatePickerMode.day), context),
child: new Text(new DateFormat('E, MMM\u00a0d').format(selectedDate), style: dayStyle),
),
],
......
// Copyright 2017 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:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
/// Provides platform-specific acoustic and/or haptic feedback for certain
/// actions.
///
/// For example, to play the Android-typically click sound when a button is
/// tapped, call [forTap]. For the Android-specific vibration when long pressing
/// an element, call [forLongPress]. Alternatively, you can also wrap your
/// [onTap] or [onLongPress] callback in [wrapForTap] or [wrapForLongPress] to
/// achieve the same (see example code below).
///
/// Calling any of these methods is a no-op on iOS as actions on that platform
/// typically don't provide haptic or acoustic feedback.
///
/// All methods in this class are usually called from within a [build] method
/// or from a State's methods as you have to provide a [BuildContext].
///
/// ## Sample code
///
/// To trigger platform-specific feedback before executing the actual callback:
///
/// ```dart
/// class WidgetWithWrappedHandler extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return new GestureDetector(
/// onTap: Feedback.wrapForTap(_onTapHandler, context),
/// onLongPress: Feedback.wrapForLongPress(_onLongPressHandler, context),
/// child: const Text('X'),
/// );
/// }
///
/// void _onTapHandler() {
/// // Respond to tap.
/// }
///
/// void _onLongPressHandler() {
/// // Respond to long press.
/// }
/// }
/// ```
///
/// Alternatively, you can also call [forTap] or [forLongPress] directly within
/// your tap or long press handler:
///
/// ```dart
/// class WidgetWithExplicitCall extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return new GestureDetector(
/// onTap: () {
/// // Do some work (e.g. check if the tap is valid)
/// Feedback.forTap(context);
/// // Do more work (e.g. respond to the tap)
/// },
/// onLongPress: () {
/// // Do some work (e.g. check if the long press is valid)
/// Feedback.forLongPress(context);
/// // Do more work (e.g. respond to the long press)
/// },
/// child: const Text('X'),
/// );
/// }
/// }
/// ```
class Feedback {
Feedback._();
/// Provides platform-specific feedback for a tap.
///
/// On Android the click system sound is played. On iOS this is a no-op.
///
/// See also:
///
/// * [wrapForTap] to trigger platform-specific feedback before executing a
/// [GestureTapCallback].
static Future<Null> forTap(BuildContext context) async {
switch (_platform(context)) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return SystemSound.play(SystemSoundType.click);
default:
return new Future<Null>.value();
}
}
/// Wraps a [GestureTapCallback] to provide platform specific feedback for a
/// tap before the provided callback is executed.
///
/// On Android the platform-typical click system sound is played. On iOS this
/// is a no-op as that platform usually doesn't provide feedback for a tap.
///
/// See also:
///
/// * [forTap] to just trigger the platform-specific feedback without wrapping
/// a [GestureTapCallback].
static GestureTapCallback wrapForTap(GestureTapCallback callback, BuildContext context) {
if (callback == null)
return null;
return () {
Feedback.forTap(context);
callback();
};
}
/// Provides platform-specific feedback for a long press.
///
/// On Android the platform-typical vibration is triggered. On iOS this is a
/// no-op as that platform usually doesn't provide feedback for long presses.
///
/// See also:
///
/// * [wrapForLongPress] to trigger platform-specific feedback before
/// executing a [GestureLongPressCallback].
static Future<Null> forLongPress(BuildContext context) {
switch (_platform(context)) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return HapticFeedback.vibrate();
default:
return new Future<Null>.value();
}
}
/// Wraps a [GestureLongPressCallback] to provide platform specific feedback
/// for a long press before the provided callback is executed.
///
/// On Android the platform-typical vibration is triggered. On iOS this
/// is a no-op as that platform usually doesn't provide feedback for a long
/// press.
///
/// See also:
///
/// * [forLongPress] to just trigger the platform-specific feedback without
/// wrapping a [GestureLongPressCallback].
static GestureLongPressCallback wrapForLongPress(GestureLongPressCallback callback, BuildContext context) {
if (callback == null)
return null;
return () {
Feedback.forLongPress(context);
callback();
};
}
static TargetPlatform _platform(BuildContext context) => Theme.of(context).platform;
}
\ No newline at end of file
......@@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'debug.dart';
import 'feedback.dart';
import 'ink_highlight.dart';
import 'ink_splash.dart';
import 'material.dart';
......@@ -70,6 +71,7 @@ import 'theme.dart';
/// ```dart
/// assert(debugCheckHasMaterial(context));
/// ```
/// The parameter [enableFeedback] must not be `null`.
///
/// See also:
///
......@@ -93,7 +95,8 @@ class InkResponse extends StatefulWidget {
this.borderRadius: BorderRadius.zero,
this.highlightColor,
this.splashColor,
}) : super(key: key);
this.enableFeedback: true,
}) : assert(enableFeedback != null), super(key: key);
/// The widget below this widget in the tree.
final Widget child;
......@@ -179,6 +182,16 @@ class InkResponse extends StatefulWidget {
/// * [highlightColor], the color of the highlight.
final Color splashColor;
/// Whether detected gestures should provide acoustic and/or haptic feedback.
///
/// For example, on Android a tap will produce a clicking sound and a
/// long-press will produce a short vibration, when feedback is enabled.
///
/// See also:
///
/// * [Feedback] for providing platform-specific feedback to certain actions.
final bool enableFeedback;
/// The rectangle to use for the highlight effect and for clipping
/// the splash effects if [containedInkWell] is true.
///
......@@ -288,12 +301,15 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
updateHighlight(true);
}
void _handleTap() {
void _handleTap(BuildContext context) {
_currentSplash?.confirm();
_currentSplash = null;
updateHighlight(false);
if (widget.onTap != null)
if (widget.onTap != null) {
if (widget.enableFeedback)
Feedback.forTap(context);
widget.onTap();
}
}
void _handleTapCancel() {
......@@ -309,11 +325,14 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
widget.onDoubleTap();
}
void _handleLongPress() {
void _handleLongPress(BuildContext context) {
_currentSplash?.confirm();
_currentSplash = null;
if (widget.onLongPress != null)
if (widget.onLongPress != null) {
if (widget.enableFeedback)
Feedback.forLongPress(context);
widget.onLongPress();
}
}
@override
......@@ -340,10 +359,10 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
final bool enabled = widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null;
return new GestureDetector(
onTapDown: enabled ? _handleTapDown : null,
onTap: enabled ? _handleTap : null,
onTap: enabled ? () => _handleTap(context) : null,
onTapCancel: enabled ? _handleTapCancel : null,
onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
onLongPress: widget.onLongPress != null ? _handleLongPress : null,
onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null,
behavior: HitTestBehavior.opaque,
child: widget.child
);
......@@ -392,6 +411,7 @@ class InkWell extends InkResponse {
Color highlightColor,
Color splashColor,
BorderRadius borderRadius,
bool enableFeedback: true,
}) : super(
key: key,
child: child,
......@@ -404,5 +424,6 @@ class InkWell extends InkResponse {
highlightColor: highlightColor,
splashColor: splashColor,
borderRadius: borderRadius,
enableFeedback: enableFeedback,
);
}
......@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'feedback.dart';
import 'input_decorator.dart';
import 'text_selection.dart';
import 'theme.dart';
......@@ -220,6 +221,11 @@ class _TextFieldState extends State<TextField> {
_editableTextKey.currentState?.requestKeyboard();
}
void _onSelectionChanged(BuildContext context, bool longPress) {
if (longPress)
Feedback.forLongPress(context);
}
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
......@@ -243,6 +249,7 @@ class _TextFieldState extends State<TextField> {
selectionControls: materialTextSelectionControls,
onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted,
onSelectionChanged: (TextSelection _, bool longPress) => _onSelectionChanged(context, longPress),
inputFormatters: widget.inputFormatters,
),
);
......
......@@ -13,6 +13,7 @@ import 'button.dart';
import 'button_bar.dart';
import 'colors.dart';
import 'dialog.dart';
import 'feedback.dart';
import 'flat_button.dart';
import 'theme.dart';
import 'typography.dart';
......@@ -289,7 +290,7 @@ class _TimePickerHeader extends StatelessWidget {
);
final Widget dayPeriodPicker = new GestureDetector(
onTap: _handleChangeDayPeriod,
onTap: Feedback.wrapForTap(_handleChangeDayPeriod, context),
behavior: HitTestBehavior.opaque,
child: new Column(
mainAxisSize: MainAxisSize.min,
......@@ -302,12 +303,12 @@ class _TimePickerHeader extends StatelessWidget {
);
final Widget hour = new GestureDetector(
onTap: () => _handleChangeMode(_TimePickerMode.hour),
onTap: Feedback.wrapForTap(() => _handleChangeMode(_TimePickerMode.hour), context),
child: new Text(selectedTime.hourOfPeriodLabel, style: hourStyle),
);
final Widget minute = new GestureDetector(
onTap: () => _handleChangeMode(_TimePickerMode.minute),
onTap: Feedback.wrapForTap(() => _handleChangeMode(_TimePickerMode.minute), context),
child: new Text(selectedTime.minuteLabel, style: minuteStyle),
);
......
......@@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'feedback.dart';
import 'theme.dart';
import 'theme_data.dart';
......@@ -110,12 +111,15 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
_removeEntry();
}
void ensureTooltipVisible() {
/// Shows the tooltip if it is not already visible.
///
/// Returns `false` when the tooltip was already visible.
bool ensureTooltipVisible() {
if (_entry != null) {
_timer?.cancel();
_timer = null;
_controller.forward();
return; // Already visible.
return false; // Already visible.
}
final RenderBox box = context.findRenderObject();
final Offset target = box.localToGlobal(box.size.center(Offset.zero));
......@@ -138,6 +142,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
Overlay.of(context, debugRequiredFor: widget).insert(_entry);
GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent);
_controller.forward();
return true;
}
void _removeEntry() {
......@@ -177,7 +182,11 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
assert(Overlay.of(context, debugRequiredFor: widget) != null);
return new GestureDetector(
behavior: HitTestBehavior.opaque,
onLongPress: ensureTooltipVisible,
onLongPress: () {
final bool tooltipCreated = ensureTooltipVisible();
if (tooltipCreated)
Feedback.forLongPress(context);
},
excludeFromSemantics: true,
child: new Semantics(
label: widget.message,
......
......@@ -21,6 +21,10 @@ import 'text_selection.dart';
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType;
/// Signature for the callback that reports when the user changes the selection
/// (including the cursor location).
typedef void SelectionChangedCallback(TextSelection selection, bool longPress);
const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
/// A controller for an editable text field.
......@@ -150,6 +154,7 @@ class EditableText extends StatefulWidget {
this.keyboardType,
this.onChanged,
this.onSubmitted,
this.onSelectionChanged,
List<TextInputFormatter> inputFormatters,
}) : assert(controller != null),
assert(focusNode != null),
......@@ -226,6 +231,10 @@ class EditableText extends StatefulWidget {
/// Called when the user indicates that they are done editing the text in the field.
final ValueChanged<String> onSubmitted;
/// Called when the user changes the selection of text (including the cursor
/// location).
final SelectionChangedCallback onSelectionChanged;
/// Optional input validation and formatting overrides. Formatters are run
/// in the provided order when the text input changes.
final List<TextInputFormatter> inputFormatters;
......@@ -447,6 +456,8 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
_selectionOverlay.showHandles();
if (longPress)
_selectionOverlay.showToolbar();
if (widget.onSelectionChanged != null)
widget.onSelectionChanged(selection, longPress);
}
}
......
......@@ -5,8 +5,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'feedback_tester.dart';
void main() {
testWidgets('Chip control test', (WidgetTester tester) async {
final FeedbackTester feedback = new FeedbackTester();
bool didDeleteChip = false;
await tester.pumpWidget(
new MaterialApp(
......@@ -26,8 +29,15 @@ void main() {
)
);
expect(feedback.clickSoundCount, 0);
expect(didDeleteChip, isFalse);
await tester.tap(find.byType(Tooltip));
expect(didDeleteChip, isTrue);
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 1);
feedback.dispose();
});
}
......@@ -3,10 +3,11 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/intl.dart';
import 'feedback_tester.dart';
void main() {
DateTime firstDate;
DateTime lastDate;
......@@ -273,34 +274,31 @@ void main() {
group('haptic feedback', () {
const Duration kHapticFeedbackInterval = const Duration(milliseconds: 10);
int hapticFeedbackCount;
setUpAll(() {
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == "HapticFeedback.vibrate")
hapticFeedbackCount++;
});
});
FeedbackTester feedback;
setUp(() {
hapticFeedbackCount = 0;
feedback = new FeedbackTester();
initialDate = new DateTime(2017, DateTime.JANUARY, 16);
firstDate = new DateTime(2017, DateTime.JANUARY, 10);
lastDate = new DateTime(2018, DateTime.JANUARY, 20);
selectableDayPredicate = (DateTime date) => date.day.isEven;
});
tearDown(() {
feedback?.dispose();
});
testWidgets('tap-select date vibrates', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('10'));
await tester.pump(kHapticFeedbackInterval);
expect(hapticFeedbackCount, 1);
expect(feedback.hapticCount, 1);
await tester.tap(find.text('12'));
await tester.pump(kHapticFeedbackInterval);
expect(hapticFeedbackCount, 2);
expect(feedback.hapticCount, 2);
await tester.tap(find.text('14'));
await tester.pump(kHapticFeedbackInterval);
expect(hapticFeedbackCount, 3);
expect(feedback.hapticCount, 3);
});
});
......@@ -308,13 +306,13 @@ void main() {
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('11'));
await tester.pump(kHapticFeedbackInterval);
expect(hapticFeedbackCount, 0);
expect(feedback.hapticCount, 0);
await tester.tap(find.text('13'));
await tester.pump(kHapticFeedbackInterval);
expect(hapticFeedbackCount, 0);
expect(feedback.hapticCount, 0);
await tester.tap(find.text('15'));
await tester.pump(kHapticFeedbackInterval);
expect(hapticFeedbackCount, 0);
expect(feedback.hapticCount, 0);
});
});
......@@ -322,10 +320,10 @@ void main() {
await preparePicker(tester, (Future<DateTime> date) async {
await tester.tap(find.text('2017'));
await tester.pump(kHapticFeedbackInterval);
expect(hapticFeedbackCount, 1);
expect(feedback.hapticCount, 1);
await tester.tap(find.text('2018'));
await tester.pump(kHapticFeedbackInterval);
expect(hapticFeedbackCount, 2);
expect(feedback.hapticCount, 2);
});
});
});
......
// Copyright 2017 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'feedback_tester.dart';
void main () {
const Duration kWaitDuration = const Duration(seconds: 1);
FeedbackTester feedback;
setUp(() {
feedback = new FeedbackTester();
});
tearDown(() {
feedback?.dispose();
});
group('Feedback on Android', () {
testWidgets('forTap', (WidgetTester tester) async {
await tester.pumpWidget(new TestWidget(
tapHandler: (BuildContext context) {
return () => Feedback.forTap(context);
},
));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 0);
await tester.tap(find.text('X'));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 1);
});
testWidgets('forTap Wrapper', (WidgetTester tester) async {
int callbackCount = 0;
final VoidCallback callback = () {
callbackCount++;
};
await tester.pumpWidget(new TestWidget(
tapHandler: (BuildContext context) {
return Feedback.wrapForTap(callback, context);
},
));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 0);
expect(callbackCount, 0);
await tester.tap(find.text('X'));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 1);
expect(callbackCount, 1);
});
testWidgets('forLongPress', (WidgetTester tester) async {
await tester.pumpWidget(new TestWidget(
longPressHandler: (BuildContext context) {
return () => Feedback.forLongPress(context);
},
));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 0);
await tester.longPress(find.text('X'));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 1);
expect(feedback.clickSoundCount, 0);
});
testWidgets('forLongPress Wrapper', (WidgetTester tester) async {
int callbackCount = 0;
final VoidCallback callback = () {
callbackCount++;
};
await tester.pumpWidget(new TestWidget(
longPressHandler: (BuildContext context) {
return Feedback.wrapForLongPress(callback, context);
},
));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 0);
expect(callbackCount, 0);
await tester.longPress(find.text('X'));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 1);
expect(feedback.clickSoundCount, 0);
expect(callbackCount, 1);
});
});
group('Feedback on iOS', () {
testWidgets('forTap', (WidgetTester tester) async {
await tester.pumpWidget(new Theme(
data: new ThemeData(platform: TargetPlatform.iOS),
child: new TestWidget(
tapHandler: (BuildContext context) {
return () => Feedback.forTap(context);
},
),
));
await tester.tap(find.text('X'));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 0);
});
testWidgets('forLongPress', (WidgetTester tester) async {
await tester.pumpWidget(new Theme(
data: new ThemeData(platform: TargetPlatform.iOS),
child: new TestWidget(
longPressHandler: (BuildContext context) {
return () => Feedback.forLongPress(context);
},
),
));
await tester.longPress(find.text('X'));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 0);
});
});
}
class TestWidget extends StatelessWidget {
TestWidget({
this.tapHandler: nullHandler,
this.longPressHandler: nullHandler,
});
final HandlerCreator tapHandler;
final HandlerCreator longPressHandler;
static VoidCallback nullHandler(BuildContext context) => null;
@override
Widget build(BuildContext context) {
return new GestureDetector(
onTap: tapHandler(context),
onLongPress: longPressHandler(context),
child: const Text('X'),
);
}
}
typedef VoidCallback HandlerCreator(BuildContext context);
// Copyright 2017 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/services.dart';
/// Tracks how often feedback has been requested since its instantiation.
///
/// It replaces the MockMethodCallHandler of [SystemChannels.platform] and
/// cannot be used in combination with other classes that do the same.
class FeedbackTester {
FeedbackTester() {
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) {
if (methodCall.method == "HapticFeedback.vibrate")
_hapticCount++;
if (methodCall.method == "SystemSound.play" &&
methodCall.arguments == SystemSoundType.click.toString())
_clickSoundCount++;
});
}
/// Number of times haptic feedback was requested (vibration).
int get hapticCount => _hapticCount;
int _hapticCount = 0;
/// Number of times the click sound was requested to play.
int get clickSoundCount => _clickSoundCount;
int _clickSoundCount = 0;
/// Stops tracking.
void dispose() {
SystemChannels.platform.setMockMethodCallHandler(null);
}
}
\ No newline at end of file
......@@ -5,6 +5,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'feedback_tester.dart';
void main() {
testWidgets('InkWell gestures control test', (WidgetTester tester) async {
final List<String> log = <String>[];
......@@ -44,4 +46,74 @@ void main() {
expect(log, equals(<String>['long-press']));
});
testWidgets('long-press and tap on disabled should not throw', (WidgetTester tester) async {
await tester.pumpWidget(const Material(
child: const Center(
child: const InkWell(),
),
));
await tester.tap(find.byType(InkWell), pointer: 1);
await tester.pump(const Duration(seconds: 1));
await tester.longPress(find.byType(InkWell), pointer: 1);
await tester.pump(const Duration(seconds: 1));
});
group('feedback', () {
FeedbackTester feedback;
setUp(() {
feedback = new FeedbackTester();
});
tearDown(() {
feedback?.dispose();
});
testWidgets('enabled (default)', (WidgetTester tester) async {
await tester.pumpWidget(new Material(
child: new Center(
child: new InkWell(
onTap: () {},
onLongPress: () {},
),
),
));
await tester.tap(find.byType(InkWell), pointer: 1);
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 1);
expect(feedback.hapticCount, 0);
await tester.tap(find.byType(InkWell), pointer: 1);
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 2);
expect(feedback.hapticCount, 0);
await tester.longPress(find.byType(InkWell), pointer: 1);
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 2);
expect(feedback.hapticCount, 1);
});
testWidgets('disabled', (WidgetTester tester) async {
await tester.pumpWidget(new Material(
child: new Center(
child: new InkWell(
onTap: () {},
onLongPress: () {},
enableFeedback: false,
),
),
));
await tester.tap(find.byType(InkWell), pointer: 1);
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 0);
await tester.longPress(find.byType(InkWell), pointer: 1);
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 0);
});
});
}
......@@ -9,6 +9,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'feedback_tester.dart';
class MockClipboard {
Object _clipboardData = <String, dynamic>{
'text': null,
......@@ -1504,6 +1506,38 @@ void main() {
}
);
testWidgets('haptic feedback', (WidgetTester tester) async {
final FeedbackTester feedback = new FeedbackTester();
final TextEditingController controller = new TextEditingController();
Widget builder() {
return overlay(new Center(
child: new Material(
child: new Container(
width: 100.0,
child: new TextField(
controller: controller,
),
),
),
));
}
await tester.pumpWidget(builder());
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 0);
await tester.longPress(find.byType(TextField));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 1);
feedback.dispose();
});
testWidgets(
'Text field drops selection when losing focus',
(WidgetTester tester) async {
......
......@@ -3,9 +3,10 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'feedback_tester.dart';
class _TimePickerLauncher extends StatelessWidget {
const _TimePickerLauncher({ Key key, this.onChanged }) : super(key: key);
......@@ -115,24 +116,21 @@ void main() {
group('haptic feedback', () {
const Duration kFastFeedbackInterval = const Duration(milliseconds: 10);
const Duration kSlowFeedbackInterval = const Duration(milliseconds: 200);
int hapticFeedbackCount;
FeedbackTester feedback;
setUpAll(() {
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) {
if (methodCall.method == "HapticFeedback.vibrate")
hapticFeedbackCount++;
});
setUp(() {
feedback = new FeedbackTester();
});
setUp(() {
hapticFeedbackCount = 0;
tearDown(() {
feedback?.dispose();
});
testWidgets('tap-select vibrates once', (WidgetTester tester) async {
final Offset center = await startPicker(tester, (TimeOfDay time) { });
await tester.tapAt(new Offset(center.dx, center.dy - 50.0));
await finishPicker(tester);
expect(hapticFeedbackCount, 1);
expect(feedback.hapticCount, 1);
});
testWidgets('quick successive tap-selects vibrate once', (WidgetTester tester) async {
......@@ -141,7 +139,7 @@ void main() {
await tester.pump(kFastFeedbackInterval);
await tester.tapAt(new Offset(center.dx, center.dy + 50.0));
await finishPicker(tester);
expect(hapticFeedbackCount, 1);
expect(feedback.hapticCount, 1);
});
testWidgets('slow successive tap-selects vibrate once per tap', (WidgetTester tester) async {
......@@ -152,7 +150,7 @@ void main() {
await tester.pump(kSlowFeedbackInterval);
await tester.tapAt(new Offset(center.dx, center.dy - 50.0));
await finishPicker(tester);
expect(hapticFeedbackCount, 3);
expect(feedback.hapticCount, 3);
});
testWidgets('drag-select vibrates once', (WidgetTester tester) async {
......@@ -164,7 +162,7 @@ void main() {
await gesture.moveBy(hour0 - hour3);
await gesture.up();
await finishPicker(tester);
expect(hapticFeedbackCount, 1);
expect(feedback.hapticCount, 1);
});
testWidgets('quick drag-select vibrates once', (WidgetTester tester) async {
......@@ -180,7 +178,7 @@ void main() {
await gesture.moveBy(hour0 - hour3);
await gesture.up();
await finishPicker(tester);
expect(hapticFeedbackCount, 1);
expect(feedback.hapticCount, 1);
});
testWidgets('slow drag-select vibrates once', (WidgetTester tester) async {
......@@ -196,7 +194,7 @@ void main() {
await gesture.moveBy(hour0 - hour3);
await gesture.up();
await finishPicker(tester);
expect(hapticFeedbackCount, 3);
expect(feedback.hapticCount, 3);
});
});
}
......@@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
// This file uses "as dynamic" in a few places to defeat the static
// analysis. In general you want to avoid using this style in your
......@@ -501,4 +502,27 @@ void main() {
expect(find.text(tooltipText), findsNothing);
});
testWidgets('Haptic feedback', (WidgetTester tester) async {
final FeedbackTester feedback = new FeedbackTester();
await tester.pumpWidget(new MaterialApp(
home: new Center(
child: new Tooltip(
message: 'Foo',
child: new Container(
width: 100.0,
height: 100.0,
color: Colors.green[500],
)
)
)
)
);
await tester.longPress(find.byType(Tooltip));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(feedback.hapticCount, 1);
feedback.dispose();
});
}
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