[CupertinoActionSheet] Add haptic feedback (#151420)

This PR implements the behavior of native action sheets that produces haptic feedback when the user slides into a button.
This commit is contained in:
Tong Mu 2024-07-16 21:57:26 -07:00 committed by GitHub
parent 83bfab5376
commit f50feec5be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 8 deletions

View File

@ -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<CupertinoActionSheetAction>
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|

View File

@ -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: <Widget>[
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) {