1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/rendering.dart';
7import 'package:flutter/semantics.dart';
8import 'package:flutter/services.dart';
9
10import 'framework.dart';
11import 'gesture_detector.dart';
12
13/// Provides platform-specific acoustic and/or haptic feedback for certain
14/// actions.
15///
16/// For example, to play the Android-typically click sound when a button is
17/// tapped, call [forTap]. For the Android-specific vibration when long pressing
18/// an element, call [forLongPress]. Alternatively, you can also wrap your
19/// [GestureDetector.onTap] or [GestureDetector.onLongPress] callback in
20/// [wrapForTap] or [wrapForLongPress] to achieve the same (see example code
21/// below).
22///
23/// All methods in this class are usually called from within a
24/// [StatelessWidget.build] method or from a [State]'s methods as you have to
25/// provide a [BuildContext].
26///
27/// {@tool snippet}
28///
29/// To trigger platform-specific feedback before executing the actual callback:
30///
31/// ```dart
32/// class WidgetWithWrappedHandler extends StatelessWidget {
33/// const WidgetWithWrappedHandler({super.key});
34///
35/// @override
36/// Widget build(BuildContext context) {
37/// return GestureDetector(
38/// onTap: Feedback.wrapForTap(_onTapHandler, context),
39/// onLongPress: Feedback.wrapForLongPress(_onLongPressHandler, context),
40/// child: const Text('X'),
41/// );
42/// }
43///
44/// void _onTapHandler() {
45/// // Respond to tap.
46/// }
47///
48/// void _onLongPressHandler() {
49/// // Respond to long press.
50/// }
51/// }
52/// ```
53/// {@end-tool}
54/// {@tool snippet}
55///
56/// Alternatively, you can also call [forTap] or [forLongPress] directly within
57/// your tap or long press handler:
58///
59/// ```dart
60/// class WidgetWithExplicitCall extends StatelessWidget {
61/// const WidgetWithExplicitCall({super.key});
62///
63/// @override
64/// Widget build(BuildContext context) {
65/// return GestureDetector(
66/// onTap: () {
67/// // Do some work (e.g. check if the tap is valid)
68/// Feedback.forTap(context);
69/// // Do more work (e.g. respond to the tap)
70/// },
71/// onLongPress: () {
72/// // Do some work (e.g. check if the long press is valid)
73/// Feedback.forLongPress(context);
74/// // Do more work (e.g. respond to the long press)
75/// },
76/// child: const Text('X'),
77/// );
78/// }
79/// }
80/// ```
81/// {@end-tool}
82abstract final class Feedback {
83 /// Provides platform-specific feedback for a tap.
84 ///
85 /// On Android the click system sound is played. On iOS this is a no-op.
86 ///
87 /// See also:
88 ///
89 /// * [wrapForTap] to trigger platform-specific feedback before executing a
90 /// [GestureTapCallback].
91 static Future<void> forTap(BuildContext context) async {
92 context.findRenderObject()!.sendSemanticsEvent(const TapSemanticEvent());
93 switch (defaultTargetPlatform) {
94 case TargetPlatform.android:
95 case TargetPlatform.fuchsia:
96 return SystemSound.play(SystemSoundType.click);
97 case TargetPlatform.iOS:
98 case TargetPlatform.linux:
99 case TargetPlatform.macOS:
100 case TargetPlatform.windows:
101 return Future<void>.value();
102 }
103 }
104
105 /// Wraps a [GestureTapCallback] to provide platform specific feedback for a
106 /// tap before the provided callback is executed.
107 ///
108 /// On Android the platform-typical click system sound is played. On iOS this
109 /// is a no-op as that platform usually doesn't provide feedback for a tap.
110 ///
111 /// See also:
112 ///
113 /// * [forTap] to just trigger the platform-specific feedback without wrapping
114 /// a [GestureTapCallback].
115 static GestureTapCallback? wrapForTap(GestureTapCallback? callback, BuildContext context) {
116 if (callback == null) {
117 return null;
118 }
119 return () {
120 forTap(context);
121 callback();
122 };
123 }
124
125 /// Provides platform-specific feedback for a long press.
126 ///
127 /// On Android the platform-typical vibration is triggered. On iOS a
128 /// heavy-impact haptic feedback is triggered alongside the click system
129 /// sound, which was observed to be the default behavior on a physical iPhone
130 /// 15 Pro running iOS version 17.5.
131 ///
132 /// See also:
133 ///
134 /// * [wrapForLongPress] to trigger platform-specific feedback before
135 /// executing a [GestureLongPressCallback].
136 static Future<void> forLongPress(BuildContext context) {
137 context.findRenderObject()!.sendSemanticsEvent(const LongPressSemanticsEvent());
138 switch (defaultTargetPlatform) {
139 case TargetPlatform.android:
140 case TargetPlatform.fuchsia:
141 return HapticFeedback.vibrate();
142 case TargetPlatform.iOS:
143 return Future.wait(<Future<void>>[
144 SystemSound.play(SystemSoundType.click),
145 HapticFeedback.heavyImpact()
146 ]);
147 case TargetPlatform.linux:
148 case TargetPlatform.macOS:
149 case TargetPlatform.windows:
150 return Future<void>.value();
151 }
152 }
153
154 /// Wraps a [GestureLongPressCallback] to provide platform specific feedback
155 /// for a long press before the provided callback is executed.
156 ///
157 /// On Android the platform-typical vibration is triggered. On iOS a
158 /// heavy-impact haptic feedback is triggered alongside the click system
159 /// sound, which was observed to be the default behavior on a physical iPhone
160 /// 15 Pro running iOS version 17.5.
161 ///
162 /// See also:
163 ///
164 /// * [forLongPress] to just trigger the platform-specific feedback without
165 /// wrapping a [GestureLongPressCallback].
166 static GestureLongPressCallback? wrapForLongPress(GestureLongPressCallback? callback, BuildContext context) {
167 if (callback == null) {
168 return null;
169 }
170 return () {
171 Feedback.forLongPress(context);
172 callback();
173 };
174 }
175}
176