diff --git a/packages/flutter/lib/src/cupertino/dialog.dart b/packages/flutter/lib/src/cupertino/dialog.dart index edd50f7809c..bc2f630386b 100644 --- a/packages/flutter/lib/src/cupertino/dialog.dart +++ b/packages/flutter/lib/src/cupertino/dialog.dart @@ -14,6 +14,7 @@ import 'dart:ui' show ImageFilter, lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; @@ -595,7 +596,10 @@ abstract class _ActionSheetSlideTarget { // * The point has contacted the screen in this region. In this case, this // method is called as soon as the pointer down event occurs regardless of // whether the gesture wins the arena immediately. - void didEnter(); + // + // The `fromPointerDown` should be true if this callback is triggered by a + // PointerDownEvent, i.e. the second case from the list above. + void didEnter({required bool fromPointerDown}); // A pointer has exited this region. // @@ -660,7 +664,10 @@ class _TargetSelectionGestureRecognizer extends GestureRecognizer { // Collect the `_ActionSheetSlideTarget`s that are currently hit by the // pointer, check whether the current target have changed, and invoke their // methods if necessary. - void _updateDrag(Offset pointerPosition) { + // + // The `fromPointerDown` should be true if this update is triggered by a + // PointerDownEvent. + void _updateDrag(Offset pointerPosition, {required bool fromPointerDown}) { final HitTestResult result = hitTest(pointerPosition); // A slide target might nest other targets, therefore multiple targets might @@ -686,21 +693,21 @@ class _TargetSelectionGestureRecognizer extends GestureRecognizer { ..clear() ..addAll(foundTargets); for (final _ActionSheetSlideTarget target in _currentTargets) { - target.didEnter(); + target.didEnter(fromPointerDown: fromPointerDown); } } } void _onDown(DragDownDetails details) { - _updateDrag(details.globalPosition); + _updateDrag(details.globalPosition, fromPointerDown: true); } void _onUpdate(Offset globalPosition) { - _updateDrag(globalPosition); + _updateDrag(globalPosition, fromPointerDown: false); } void _onEnd(Offset globalPosition) { - _updateDrag(globalPosition); + _updateDrag(globalPosition, fromPointerDown: false); for (final _ActionSheetSlideTarget target in _currentTargets) { target.didConfirm(); } @@ -1121,7 +1128,7 @@ class _CupertinoActionSheetActionState extends State implements _ActionSheetSlideTarget { // |_ActionSheetSlideTarget| @override - void didEnter() {} + void didEnter({required bool fromPointerDown}) {} // |_ActionSheetSlideTarget| @override @@ -1243,11 +1250,27 @@ class _ActionSheetButtonBackground extends StatefulWidget { class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackground> implements _ActionSheetSlideTarget { bool isBeingPressed = false; + void _emitVibration(){ + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.android: + HapticFeedback.selectionClick(); + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + break; + } + } + // |_ActionSheetSlideTarget| @override - void didEnter() { + void didEnter({required bool fromPointerDown}) { setState(() { isBeingPressed = true; }); widget.onPressStateChange?.call(true); + if (!fromPointerDown) { + _emitVibration(); + } } // |_ActionSheetSlideTarget| diff --git a/packages/flutter/test/cupertino/action_sheet_test.dart b/packages/flutter/test/cupertino/action_sheet_test.dart index b60cf36007d..1903f349e4d 100644 --- a/packages/flutter/test/cupertino/action_sheet_test.dart +++ b/packages/flutter/test/cupertino/action_sheet_test.dart @@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -1900,6 +1901,50 @@ void main() { kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, ); }); + + testWidgets('Action sheets emits haptic vibration on sliding into a button', (WidgetTester tester) async { + int vibrationCount = 0; + + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'HapticFeedback.vibrate') { + expect(methodCall.arguments, 'HapticFeedbackType.selectionClick'); + vibrationCount += 1; + } + return null; + }); + + await tester.pumpWidget( + createAppWithButtonThatLaunchesActionSheet( + CupertinoActionSheet( + title: const Text('The title'), + actions: [ + CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}), + CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}), + CupertinoActionSheetAction(child: const Text('Three'), onPressed: () {}), + ], + ) + ), + ); + + await tester.tap(find.text('Go')); + await tester.pumpAndSettle(); + + final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('One'))); + await tester.pumpAndSettle(); + // Tapping down on a button should not emit vibration. + expect(vibrationCount, 0); + + await gesture.moveTo(tester.getCenter(find.text('Two'))); + await tester.pumpAndSettle(); + expect(vibrationCount, 1); + + await gesture.moveTo(tester.getCenter(find.text('Three'))); + await tester.pumpAndSettle(); + expect(vibrationCount, 2); + + await gesture.up(); + expect(vibrationCount, 2); + }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); } RenderBox findScrollableActionsSectionRenderBox(WidgetTester tester) {