Skip to content

Commit 0b33898

Browse files
dkwingsmtpull[bot]
authored andcommitted
[CupertinoActionSheet] Add haptic feedback (flutter#151420)
This PR implements the behavior of native action sheets that produces haptic feedback when the user slides into a button.
1 parent 01f5885 commit 0b33898

File tree

2 files changed

+76
-8
lines changed

2 files changed

+76
-8
lines changed

packages/flutter/lib/src/cupertino/dialog.dart

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'dart:ui' show ImageFilter, lerpDouble;
1414
import 'package:flutter/foundation.dart';
1515
import 'package:flutter/gestures.dart';
1616
import 'package:flutter/rendering.dart';
17+
import 'package:flutter/services.dart';
1718
import 'package:flutter/widgets.dart';
1819

1920
import 'colors.dart';
@@ -595,7 +596,10 @@ abstract class _ActionSheetSlideTarget {
595596
// * The point has contacted the screen in this region. In this case, this
596597
// method is called as soon as the pointer down event occurs regardless of
597598
// whether the gesture wins the arena immediately.
598-
void didEnter();
599+
//
600+
// The `fromPointerDown` should be true if this callback is triggered by a
601+
// PointerDownEvent, i.e. the second case from the list above.
602+
void didEnter({required bool fromPointerDown});
599603

600604
// A pointer has exited this region.
601605
//
@@ -660,7 +664,10 @@ class _TargetSelectionGestureRecognizer extends GestureRecognizer {
660664
// Collect the `_ActionSheetSlideTarget`s that are currently hit by the
661665
// pointer, check whether the current target have changed, and invoke their
662666
// methods if necessary.
663-
void _updateDrag(Offset pointerPosition) {
667+
//
668+
// The `fromPointerDown` should be true if this update is triggered by a
669+
// PointerDownEvent.
670+
void _updateDrag(Offset pointerPosition, {required bool fromPointerDown}) {
664671
final HitTestResult result = hitTest(pointerPosition);
665672

666673
// A slide target might nest other targets, therefore multiple targets might
@@ -686,21 +693,21 @@ class _TargetSelectionGestureRecognizer extends GestureRecognizer {
686693
..clear()
687694
..addAll(foundTargets);
688695
for (final _ActionSheetSlideTarget target in _currentTargets) {
689-
target.didEnter();
696+
target.didEnter(fromPointerDown: fromPointerDown);
690697
}
691698
}
692699
}
693700

694701
void _onDown(DragDownDetails details) {
695-
_updateDrag(details.globalPosition);
702+
_updateDrag(details.globalPosition, fromPointerDown: true);
696703
}
697704

698705
void _onUpdate(Offset globalPosition) {
699-
_updateDrag(globalPosition);
706+
_updateDrag(globalPosition, fromPointerDown: false);
700707
}
701708

702709
void _onEnd(Offset globalPosition) {
703-
_updateDrag(globalPosition);
710+
_updateDrag(globalPosition, fromPointerDown: false);
704711
for (final _ActionSheetSlideTarget target in _currentTargets) {
705712
target.didConfirm();
706713
}
@@ -1121,7 +1128,7 @@ class _CupertinoActionSheetActionState extends State<CupertinoActionSheetAction>
11211128
implements _ActionSheetSlideTarget {
11221129
// |_ActionSheetSlideTarget|
11231130
@override
1124-
void didEnter() {}
1131+
void didEnter({required bool fromPointerDown}) {}
11251132

11261133
// |_ActionSheetSlideTarget|
11271134
@override
@@ -1243,11 +1250,27 @@ class _ActionSheetButtonBackground extends StatefulWidget {
12431250
class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackground> implements _ActionSheetSlideTarget {
12441251
bool isBeingPressed = false;
12451252

1253+
void _emitVibration(){
1254+
switch (defaultTargetPlatform) {
1255+
case TargetPlatform.iOS:
1256+
case TargetPlatform.android:
1257+
HapticFeedback.selectionClick();
1258+
case TargetPlatform.fuchsia:
1259+
case TargetPlatform.linux:
1260+
case TargetPlatform.macOS:
1261+
case TargetPlatform.windows:
1262+
break;
1263+
}
1264+
}
1265+
12461266
// |_ActionSheetSlideTarget|
12471267
@override
1248-
void didEnter() {
1268+
void didEnter({required bool fromPointerDown}) {
12491269
setState(() { isBeingPressed = true; });
12501270
widget.onPressStateChange?.call(true);
1271+
if (!fromPointerDown) {
1272+
_emitVibration();
1273+
}
12511274
}
12521275

12531276
// |_ActionSheetSlideTarget|

packages/flutter/test/cupertino/action_sheet_test.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart';
1212
import 'package:flutter/gestures.dart';
1313
import 'package:flutter/material.dart';
1414
import 'package:flutter/rendering.dart';
15+
import 'package:flutter/services.dart';
1516

1617
import 'package:flutter_test/flutter_test.dart';
1718

@@ -1900,6 +1901,50 @@ void main() {
19001901
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
19011902
);
19021903
});
1904+
1905+
testWidgets('Action sheets emits haptic vibration on sliding into a button', (WidgetTester tester) async {
1906+
int vibrationCount = 0;
1907+
1908+
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
1909+
if (methodCall.method == 'HapticFeedback.vibrate') {
1910+
expect(methodCall.arguments, 'HapticFeedbackType.selectionClick');
1911+
vibrationCount += 1;
1912+
}
1913+
return null;
1914+
});
1915+
1916+
await tester.pumpWidget(
1917+
createAppWithButtonThatLaunchesActionSheet(
1918+
CupertinoActionSheet(
1919+
title: const Text('The title'),
1920+
actions: <Widget>[
1921+
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
1922+
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
1923+
CupertinoActionSheetAction(child: const Text('Three'), onPressed: () {}),
1924+
],
1925+
)
1926+
),
1927+
);
1928+
1929+
await tester.tap(find.text('Go'));
1930+
await tester.pumpAndSettle();
1931+
1932+
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('One')));
1933+
await tester.pumpAndSettle();
1934+
// Tapping down on a button should not emit vibration.
1935+
expect(vibrationCount, 0);
1936+
1937+
await gesture.moveTo(tester.getCenter(find.text('Two')));
1938+
await tester.pumpAndSettle();
1939+
expect(vibrationCount, 1);
1940+
1941+
await gesture.moveTo(tester.getCenter(find.text('Three')));
1942+
await tester.pumpAndSettle();
1943+
expect(vibrationCount, 2);
1944+
1945+
await gesture.up();
1946+
expect(vibrationCount, 2);
1947+
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
19031948
}
19041949

19051950
RenderBox findScrollableActionsSectionRenderBox(WidgetTester tester) {

0 commit comments

Comments
 (0)