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 | |
5 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/rendering.dart'; |
7 | import 'package:flutter/semantics.dart'; |
8 | import 'package:flutter/services.dart'; |
9 | |
10 | import 'framework.dart'; |
11 | import '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} |
82 | abstract 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 | |