Skip to content

Commit 151fa40

Browse files
authored
feat(ui): add haptic feedback support for audio recorder (#2465)
1 parent ae5cc19 commit 151fa40

File tree

8 files changed

+654
-9
lines changed

8 files changed

+654
-9
lines changed

packages/stream_chat_flutter/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
✅ Added
44

5+
- Added haptic and system sound feedback for voice recording interactions. Feedback can be
6+
customized by extending `AudioRecorderFeedback` or using `AudioRecorderFeedbackWrapper` with
7+
custom callbacks. [[#2463]](https://github.com/GetStream/stream-chat-flutter/issues/2463)
58
- Added support for deleting options while creating a poll in
69
`StreamPollCreator`. [[#2453]](https://github.com/GetStream/stream-chat-flutter/issues/2453)
710

packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import 'dart:async';
2+
import 'dart:ui';
23

34
import 'package:file_selector/file_selector.dart';
45
import 'package:flutter/foundation.dart';
5-
import 'package:flutter/gestures.dart';
66
import 'package:path_provider/path_provider.dart';
77
import 'package:record/record.dart';
88
import 'package:stream_chat_flutter/src/audio/audio_sampling.dart' as sampling;
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import 'package:flutter/widgets.dart';
2+
3+
/// A feedback handler for audio recorder interactions.
4+
///
5+
/// Provides default feedback behavior (haptic feedback, system sounds, etc.)
6+
/// and can be used directly, or extended to customize feedback behavior.
7+
///
8+
/// {@tool snippet}
9+
/// Basic usage with default feedback:
10+
/// ```dart
11+
/// const AudioRecorderFeedback()
12+
/// ```
13+
///
14+
/// Disable feedback:
15+
/// ```dart
16+
/// const AudioRecorderFeedback.disabled()
17+
/// ```
18+
///
19+
/// Custom feedback (haptic or system sounds):
20+
/// ```dart
21+
/// class CustomFeedback extends AudioRecorderFeedback {
22+
/// @override
23+
/// Future<void> onRecordStart(BuildContext context) async {
24+
/// // Haptic feedback
25+
/// await HapticFeedback.heavyImpact();
26+
/// // Or system sound
27+
/// // await SystemSound.play(SystemSoundType.click);
28+
/// }
29+
/// }
30+
///
31+
/// CustomFeedback()
32+
/// ```
33+
/// {@end-tool}
34+
class AudioRecorderFeedback {
35+
/// Creates a new [AudioRecorderFeedback] instance.
36+
///
37+
/// The [enableFeedback] parameter controls whether feedback is enabled.
38+
/// Defaults to `true`.
39+
const AudioRecorderFeedback({this.enableFeedback = true});
40+
41+
/// Creates a new [AudioRecorderFeedback] instance with feedback disabled.
42+
///
43+
/// Use this constructor to disable feedback entirely.
44+
const AudioRecorderFeedback.disabled() : enableFeedback = false;
45+
46+
/// Whether feedback is enabled.
47+
///
48+
/// If `false`, no feedback will be triggered regardless of which methods are
49+
/// called.
50+
final bool enableFeedback;
51+
52+
/// Provides platform-specific feedback when recording starts.
53+
///
54+
/// This is called when the user initiates a new recording session and the
55+
/// recording actually begins.
56+
Future<void> onRecordStart(BuildContext context) async {
57+
if (!enableFeedback) return;
58+
return Feedback.forLongPress(context);
59+
}
60+
61+
/// Provides platform-specific feedback when recording is paused.
62+
///
63+
/// This is called when the user pauses the ongoing recording.
64+
Future<void> onRecordPause(BuildContext context) async {
65+
if (!enableFeedback) return;
66+
return Feedback.forTap(context);
67+
}
68+
69+
/// Provides platform-specific feedback when recording is finished.
70+
///
71+
/// This is called when the user finishes the recording.
72+
Future<void> onRecordFinish(BuildContext context) async {
73+
if (!enableFeedback) return;
74+
return Feedback.forTap(context);
75+
}
76+
77+
/// Provides platform-specific feedback when the recording is locked.
78+
///
79+
/// This is called when the user locks the recording to continue without
80+
/// holding the record button.
81+
Future<void> onRecordLock(BuildContext context) async {
82+
if (!enableFeedback) return;
83+
return Feedback.forLongPress(context);
84+
}
85+
86+
/// Provides platform-specific feedback when recording is canceled.
87+
///
88+
/// This is called when the user cancels an ongoing recording.
89+
Future<void> onRecordCancel(BuildContext context) async {
90+
if (!enableFeedback) return;
91+
return Feedback.forTap(context);
92+
}
93+
94+
/// Provides platform-specific feedback when recording is canceled before
95+
/// starting.
96+
///
97+
/// This can occur if the user holds the record button but releases it before
98+
/// the recording actually starts.
99+
Future<void> onRecordStartCancel(BuildContext context) async {
100+
if (!enableFeedback) return;
101+
return Feedback.forTap(context);
102+
}
103+
104+
/// Provides platform-specific feedback when recording stops.
105+
///
106+
/// This is called when the recording is finalized and the audio track
107+
/// is now ready to be used.
108+
Future<void> onRecordStop(BuildContext context) async {
109+
if (!enableFeedback) return;
110+
return Feedback.forTap(context);
111+
}
112+
}
113+
114+
// A callback function for providing feedback during audio recorder interaction.
115+
typedef _FeedbackCallback = Future<void> Function(BuildContext context);
116+
117+
/// A wrapper around [AudioRecorderFeedback] that allows providing custom
118+
/// callbacks for each feedback event without extending the class.
119+
///
120+
/// This is useful when you want to customize feedback behavior using callbacks
121+
/// rather than extending [AudioRecorderFeedback] and overriding methods.
122+
///
123+
/// {@tool snippet}
124+
/// Basic usage with custom callbacks:
125+
/// ```dart
126+
/// AudioRecorderFeedbackWrapper(
127+
/// onStart: (context) async {
128+
/// await HapticFeedback.heavyImpact();
129+
/// },
130+
/// onFinish: (context) async {
131+
/// await HapticFeedback.mediumImpact();
132+
/// },
133+
/// )
134+
/// ```
135+
///
136+
/// Disable feedback:
137+
/// ```dart
138+
/// AudioRecorderFeedbackWrapper(
139+
/// enableFeedback: false,
140+
/// )
141+
/// ```
142+
/// {@end-tool}
143+
class AudioRecorderFeedbackWrapper extends AudioRecorderFeedback {
144+
/// Creates a new [AudioRecorderFeedbackWrapper] instance.
145+
///
146+
/// The [enableFeedback] parameter controls whether feedback is enabled.
147+
/// Defaults to `true`.
148+
///
149+
/// All callback parameters are optional. If a callback is not provided, the
150+
/// default behavior from [AudioRecorderFeedback] will be used.
151+
///
152+
/// - [onStart]: Called when recording starts.
153+
/// - [onPause]: Called when recording is paused.
154+
/// - [onFinish]: Called when recording is finished.
155+
/// - [onLock]: Called when recording is locked.
156+
/// - [onCancel]: Called when recording is canceled.
157+
/// - [onStartCancel]: Called when recording start is canceled.
158+
/// - [onStop]: Called when recording stops.
159+
const AudioRecorderFeedbackWrapper({
160+
super.enableFeedback = true,
161+
_FeedbackCallback? onStart,
162+
_FeedbackCallback? onPause,
163+
_FeedbackCallback? onFinish,
164+
_FeedbackCallback? onLock,
165+
_FeedbackCallback? onCancel,
166+
_FeedbackCallback? onStartCancel,
167+
_FeedbackCallback? onStop,
168+
}) : _onStop = onStop,
169+
_onStartCancel = onStartCancel,
170+
_onCancel = onCancel,
171+
_onLock = onLock,
172+
_onFinish = onFinish,
173+
_onPause = onPause,
174+
_onStart = onStart;
175+
176+
// Callback for when recording starts.
177+
final _FeedbackCallback? _onStart;
178+
// Callback for when recording is paused.
179+
final _FeedbackCallback? _onPause;
180+
// Callback for when recording is finished.
181+
final _FeedbackCallback? _onFinish;
182+
// Callback for when recording is locked.
183+
final _FeedbackCallback? _onLock;
184+
// Callback for when recording is canceled.
185+
final _FeedbackCallback? _onCancel;
186+
// Callback for when recording start is canceled.
187+
final _FeedbackCallback? _onStartCancel;
188+
// Callback for when recording stops.
189+
final _FeedbackCallback? _onStop;
190+
191+
@override
192+
Future<void> onRecordStart(BuildContext context) async {
193+
if (_onStart case final callback?) {
194+
if (!enableFeedback) return;
195+
return callback.call(context);
196+
}
197+
198+
return super.onRecordStart(context);
199+
}
200+
201+
@override
202+
Future<void> onRecordPause(BuildContext context) async {
203+
if (_onPause case final callback?) {
204+
if (!enableFeedback) return;
205+
return callback.call(context);
206+
}
207+
208+
return super.onRecordPause(context);
209+
}
210+
211+
@override
212+
Future<void> onRecordFinish(BuildContext context) async {
213+
if (_onFinish case final callback?) {
214+
if (!enableFeedback) return;
215+
return callback.call(context);
216+
}
217+
218+
return super.onRecordFinish(context);
219+
}
220+
221+
@override
222+
Future<void> onRecordLock(BuildContext context) async {
223+
if (_onLock case final callback?) {
224+
if (!enableFeedback) return;
225+
return callback.call(context);
226+
}
227+
228+
return super.onRecordLock(context);
229+
}
230+
231+
@override
232+
Future<void> onRecordCancel(BuildContext context) async {
233+
if (_onCancel case final callback?) {
234+
if (!enableFeedback) return;
235+
return callback.call(context);
236+
}
237+
238+
return super.onRecordCancel(context);
239+
}
240+
241+
@override
242+
Future<void> onRecordStartCancel(BuildContext context) async {
243+
if (_onStartCancel case final callback?) {
244+
if (!enableFeedback) return;
245+
return callback.call(context);
246+
}
247+
248+
return super.onRecordStartCancel(context);
249+
}
250+
251+
@override
252+
Future<void> onRecordStop(BuildContext context) async {
253+
if (_onStop case final callback?) {
254+
if (!enableFeedback) return;
255+
return callback.call(context);
256+
}
257+
258+
return super.onRecordStop(context);
259+
}
260+
}

packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_state.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import 'package:flutter/gestures.dart';
1+
import 'dart:ui';
2+
23
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
34

45
/// The state of the audio recorder.

0 commit comments

Comments
 (0)