仿微信发送语音按钮、录音以及动画
Frist
这边文章的来源,来自工作中开发涉及到的IM语音发送的功能需求,因为学习flutter学艺不精,起初不知道如何来构建这样的功能
主要用到的第三方库
-
permission_handler
: 获取设备的运行时权限,获取语音录制的权限 -
loading_indicator
: 一个开箱即用的加载动画库,用来在录音时播放对应的动画 -
flutter_sound
: 用来录制和播放声音。 -
just_audio
: 用来获取音频文件时长. -
path_provider
: 用于查找文件系统上的常用位置。 -
lottie
: 播放音频播放时的动画
先看效果
主要功能代码
- 录制语音的弹窗
通过OverlayEntry来构建弹窗
createOverlay() {
overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: canCancelNotifier,
builder: (_, canCancel, child) {
return Positioned(
child: Scaffold(
backgroundColor: Colors.white.withOpacity(0.2),
body: Column(
verticalDirection: VerticalDirection.up,
children: [
ClipPath(
clipper: TopClipper(),
child: Container(
color: canCancel ? Colors.grey : const Color(0xFFE1E1E1),
height: bezierBoxHeight,
),
),
const SizedBox(
height: 20,
),
Text(canCancel ? "松开 取消" : "松开 发送"),
const SizedBox(height: 20),
Container(
key: _cancelBtnKey,
height: 60,
width: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: canCancel ? Colors.grey : const Color(0xFFE1E1E1),
),
child: Icon(
Icons.close,
color: canCancel ? Colors.white : Colors.black,
),
),
const SizedBox(
height: 20,
),
const WechatSoundBubbleView()
],
),
),
);
},
);
},
);
}
- 按钮
使用GestrueDetector
中的相关长按事件,
onLongPressDown
: 长按按下时,创建并展示OverlayEntry 构建的弹窗onLongPressUp
: 松开长按时,隐藏并销毁OverlayEntryonLongPressStart
: 长按开始时,开始录制语音。onLongPressEnd
: 长按结束时,停止录音,并通过回调把录音文件的路径,传至上层组件onLongPressMoveUpdate
: 监听长按开始后的手指移动的位移偏移量,以计算手指是否在取消按钮的区域内。
代码:
GestureDetector(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
margin: const EdgeInsets.symmetric(horizontal: 32),
child: const Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("按住说话"),
],
),
),
onLongPressDown: (LongPressDownDetails details) {
debugPrint("long press down");
createOverlay();
initRecorder();
},
onLongPressUp: () {
cancelPosition = null;
cancelSize = null;
},
onLongPressStart: (LongPressStartDetails details) async {
showFloatingButtonOverlay(context);
await startRecorder();
},
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
final globalPosition = details.globalPosition;
getCancelBtnInfo();
if (cancelSize != null && cancelPosition != null && cancelSize != Size.zero && cancelPosition != Offset.zero) {
if (globalPosition.dx >= cancelPosition!.dx &&
globalPosition.dx <= cancelPosition!.dx + cancelSize!.width &&
globalPosition.dy >= cancelPosition!.dy &&
globalPosition.dy <= cancelPosition!.dy + cancelSize!.height) {
canCancelNotifier.value = true;
} else {
canCancelNotifier.value = false;
}
}
},
onLongPressEnd: (LongPressEndDetails details) {
overlayEntry.remove();
// 停止录音
stopRecording();
// disposeRecorder();
},
);
canCancelNotifier
用来通知更新视图,修改 “松开发送”、“松开取消”,以及按钮颜色等参数。
getCancelBtnInfo()
方法用来获取 取消按钮在屏幕中的位置:
getCancelBtnInfo() {
final RenderBox? renderBox = _cancelBtnKey.currentContext?.findRenderObject() as RenderBox?;
if (cancelPosition == null || cancelPosition == Offset.zero) {
final position = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
cancelPosition = position;
}
if (cancelSize == null || cancelSize == Size.zero) {
cancelSize = renderBox?.size;
}
}
- 开始录音和结束录音
使用的flutter_sound 第三方库来进行录音,
- 开始:
Future<void> startRecorder() async {
try {
final status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
print('microphone not has Permission ');
return;
}
await recorder.openRecorder();
soundFilePath =
"${(await getTemporaryDirectory()).path}/wechat_sound/sound_${DateTime.now().millisecondsSinceEpoch}${ext[Codec.pcm16WAV.index]}";
final file = File(soundFilePath);
if (!(await file.exists())) {
await file.create(recursive: true);
}
await recorder.openRecorder();
print('Recording started at path $soundFilePath');
await recorder.startRecorder(
toFile: soundFilePath,
codec: Codec.pcm16WAV,
numChannels: 1,
bitRate: 8000,
);
} catch (err) {
print(err);
}
}
- 结束:
Future<void> stopRecording() async {
try {
await recorder.stopRecorder();
if (!canCancelNotifier.value) {
widget.onRecordedCallback?.call(soundFilePath);
}
print('Recording stopped');
} catch (error) {
print('error stoped recording $error');
}
}
总结
本身功能不难,只是之前学习的时候解除OverlayEntry比较少,项目中用到的Dialog之类的组件都是用的现成的第三方库,无脑拿来就使,又遇上这个组件网上资源比较少,导致对这方面知识欠缺。这次相当于补齐了一部分关于Overlay相关的知识。
代码放在了github /lib/pages/wechat_sound
下,欢迎指正。