写在前面
成品展示
相关依赖
- get
- path_provider
- uuid
- record
- audioplayers
- permission_handler
录音组件
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mood_diary/components/wave_form/wave_form_view.dart';
import 'record_sheet_logic.dart';
class RecordSheetComponent extends StatelessWidget {
const RecordSheetComponent({super.key});
@override
Widget build(BuildContext context) {
final logic = Get.put(RecordSheetLogic());
final state = Bind.find<RecordSheetLogic>().state;
final colorScheme = Theme.of(context).colorScheme;
return GetBuilder<RecordSheetLogic>(
init: logic,
assignId: true,
builder: (logic) {
return SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 48.0,
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Divider(
thickness: 8.0,
height: 8.0,
color: colorScheme.outline,
),
)),
],
),
),
Obx(() {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 100),
child: state.isStarted.value
? SizedBox(
height: 100,
key: const ValueKey('wave'),
child: Obx(() {
return WaveFormComponent(
amplitudes: state.amplitudes.value,
height: 100,
);
}),
)
: SizedBox(
height: 100,
key: const ValueKey('start'),
child: Center(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: colorScheme.outline, width: 4.0), shape: BoxShape.circle),
child: IconButton(
onPressed: () {
logic.startRecorder();
},
padding: EdgeInsets.zero,
icon: const Icon(
Icons.circle,
size: 48,
color: Colors.redAccent,
)),
)),
),
);
}),
Obx(() {
return AnimatedContainer(
duration: const Duration(milliseconds: 100),
height: state.height.value,
child: OverflowBox(
minHeight: 0,
maxHeight: state.height.value,
alignment: Alignment.center,
child: state.isStarted.value
? Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
key: const ValueKey('playButton'),
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
return Text(state.durationTime.value.toString().split('.')[0]);
}),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
logic.cancelRecorder();
},
child: const Text('取消')),
FilledButton(
onPressed: () {
state.isRecording.value ? logic.pauseRecorder() : logic.resumeRecorder();
},
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: logic.animationController,
)),
TextButton(
onPressed: () {
logic.stopRecorder();
},
child: const Text('保存')),
],
)
],
)
: null,
),
);
}),
],
),
);
},
);
}
}
import 'package:get/get.dart';
class RecordSheetState {
late RxList<double> amplitudes;
late Rx<Duration> durationTime;
late RxBool isRecording;
late RxBool isStarted;
late String fileName;
late RxDouble height;
late bool isStop;
RecordSheetState() {
amplitudes = <double>[].obs;
isRecording = false.obs;
isStarted = false.obs;
fileName = '';
height = .0.obs;
isStop = false;
durationTime = const Duration().obs;
///Initialize variables
}
}
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mood_diary/pages/edit/edit_logic.dart';
import 'package:mood_diary/utils/utils.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:record/record.dart';
import 'package:uuid/uuid.dart';
import 'record_sheet_state.dart';
class RecordSheetLogic extends GetxController with GetSingleTickerProviderStateMixin {
final RecordSheetState state = RecordSheetState();
final AudioRecorder audioRecorder = AudioRecorder();
late AnimationController animationController =
AnimationController(vsync: this, duration: const Duration(milliseconds: 100), lowerBound: 0, upperBound: 1.0);
final size = MediaQuery.sizeOf(Get.context!);
final editLogic = Bind.find<EditLogic>();
late double baseline = .0;
@override
void onReady() {
// TODO: implement onReady
super.onReady();
listenAmplitude();
}
@override
void onClose() async {
// TODO: implement onClose
if (state.isStop == false) {
await audioRecorder.cancel();
}
audioRecorder.dispose();
animationController.dispose();
super.onClose();
}
Future<void> startRecorder() async {
if (await Utils().permissionUtil.checkPermission(Permission.microphone)) {
await animationController.forward();
state.isRecording.value = true;
state.isStarted.value = true;
state.height.value = 140.0;
state.fileName = 'audio-${const Uuid().v7()}.m4a';
//暂时保存在缓存目录中
await audioRecorder.start(const RecordConfig(encoder: AudioEncoder.aacLc),
path: Utils().fileUtil.getCachePath(state.fileName));
}
}
void listenAmplitude() {
final amplitudeStream = audioRecorder.onAmplitudeChanged(const Duration(milliseconds: 100));
amplitudeStream.listen((amplitude) {
if (amplitude.current.isFinite && amplitude.current != amplitude.max) {
maxLengthAdd(amplitude.current);
}
timeIncrease();
});
}
double normalizeAmplitude(double amplitude) {
baseline = min(baseline, amplitude);
return (amplitude + baseline.abs()) / baseline.abs();
}
void maxLengthAdd(value) {
if (state.amplitudes.length > size.width ~/ 6.0) {
state.amplitudes.removeAt(0);
}
state.amplitudes.add(normalizeAmplitude(value));
}
void timeIncrease() {
state.durationTime.value += const Duration(milliseconds: 100);
}
Future<void> stopRecorder() async {
state.isStop = true;
await audioRecorder.stop();
animationController.reset();
editLogic.setAudioName(state.fileName);
Get.backLegacy();
}
Future<void> pauseRecorder() async {
state.isRecording.value = false;
await audioRecorder.pause();
await animationController.reverse();
}
Future<void> resumeRecorder() async {
await animationController.forward();
state.isRecording.value = true;
await audioRecorder.resume();
}
Future<void> cancelRecorder() async {
state.amplitudes.value = <double>[];
state.durationTime.value = const Duration();
animationController.reset();
state.isStarted.value = false;
state.isRecording.value = false;
state.height.value = 0;
await audioRecorder.cancel();
}
}
频谱组件
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mood_diary/components/record_sheet/record_sheet_logic.dart';
class WaveFormPainter extends CustomPainter {
final double barWidth;
final double spaceWidth;
final Color color;
final List<double> amplitudes;
final recordLogic = Bind.find<RecordSheetLogic>();
WaveFormPainter(
this.amplitudes, {
required this.barWidth,
required this.spaceWidth,
this.color = Colors.white,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeCap = StrokeCap.round
..strokeWidth = barWidth
..style = PaintingStyle.fill;
final barHeightFactor = size.height - barWidth;
for (int i = 0; i < amplitudes.length; i++) {
final x = i * (barWidth + spaceWidth) + barWidth / 2;
final y = barHeightFactor * (1 - amplitudes[i]);
canvas.drawLine(Offset(x, barHeightFactor), Offset(x, y), paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class WaveFormComponent extends StatelessWidget {
const WaveFormComponent({
super.key,
required this.amplitudes,
this.barWidth = 4.0,
this.spaceWidth = 2.0,
required this.height,
});
final List<double> amplitudes;
final double barWidth;
final double spaceWidth;
final double height;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return CustomPaint(
painter: WaveFormPainter(amplitudes, color: colorScheme.primary, barWidth: barWidth, spaceWidth: spaceWidth),
size: Size(amplitudes.length * (barWidth + spaceWidth), height),
);
}
}
播放组件
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'audio_player_logic.dart';
class AudioPlayerComponent extends StatelessWidget {
const AudioPlayerComponent({super.key, required this.path, required this.index, this.isEdit = false});
final String path;
final String index;
final bool isEdit;
@override
Widget build(BuildContext context) {
final logic = Get.put(AudioPlayerLogic(), tag: index);
final state = Bind.find<AudioPlayerLogic>(tag: index).state;
final colorScheme = Theme.of(context).colorScheme;
return GetBuilder<AudioPlayerLogic>(
init: logic,
tag: index,
assignId: true,
initState: (_) async {
await logic.initAudioPlayer(path);
},
builder: (logic) {
return Container(
decoration: BoxDecoration(color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(8.0)),
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
IconButton.filled(
onPressed: () {
logic.audioPlayer.state == PlayerState.playing ? logic.pause() : logic.play(path);
},
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: logic.animationController,
)),
Expanded(
child: Column(
children: [
Obx(() {
return Slider(
value: state.totalDuration.value != Duration.zero
? ((state.currentDuration.value.inMilliseconds / state.totalDuration.value.inMilliseconds)
.clamp(0, 1))
: 0,
onChangeEnd: (value) {
logic.to(value);
},
onChanged: (double value) {
logic.changeValue(value);
},
inactiveColor: colorScheme.outline,
);
}),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(() {
return Text(
state.totalDuration.value.toString().split('.')[0].padLeft(8, '0'),
style: TextStyle(color: colorScheme.onSecondaryContainer),
);
}),
Obx(() {
return Text(
state.currentDuration.value.toString().split('.')[0].padLeft(8, '0'),
style: TextStyle(color: colorScheme.onSecondaryContainer),
);
}),
],
),
),
],
),
),
if (isEdit) ...[
IconButton(
onPressed: () {
logic.editLogic.deleteAudio(int.parse(index));
},
icon: const Icon(Icons.cancel),
style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
)
]
],
),
);
},
);
}
}
import 'package:get/get.dart';
class AudioPlayerState {
late String audioPath;
late Rx<Duration> totalDuration;
late Rx<Duration> currentDuration;
late RxBool handleChange;
AudioPlayerState() {
audioPath = '';
handleChange = false.obs;
totalDuration = Duration.zero.obs;
currentDuration = Duration.zero.obs;
///Initialize variables
}
}
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/animation.dart';
import 'package:get/get.dart';
import 'package:mood_diary/pages/edit/edit_logic.dart';
import 'audio_player_state.dart';
class AudioPlayerLogic extends GetxController with GetSingleTickerProviderStateMixin {
final AudioPlayerState state = AudioPlayerState();
final AudioPlayer audioPlayer = AudioPlayer();
late final AnimationController animationController =
AnimationController(vsync: this, duration: const Duration(milliseconds: 100));
final EditLogic editLogic = Bind.find<EditLogic>();
@override
void onInit() {
// TODO: implement onInit
audioPlayer.eventStream.listen((event) {
//读取时间完成
if (event.eventType == AudioEventType.duration) {
state.totalDuration.value = event.duration!;
}
//播放完成
if (event.eventType == AudioEventType.complete) {
animationController.reset();
state.currentDuration.value = Duration.zero;
audioPlayer.stop();
}
});
audioPlayer.onPositionChanged.listen((duration) {
if (state.handleChange.value == false) {
state.currentDuration.value = duration;
}
});
super.onInit();
}
@override
void onReady() {
// TODO: implement onReady
super.onReady();
}
@override
void onClose() {
// TODO: implement onClose
animationController.dispose();
audioPlayer.dispose();
super.onClose();
}
//初始化获取时长信息
Future<void> initAudioPlayer(String value) async {
await audioPlayer.setSourceDeviceFile(value);
}
Future<void> play(String path) async {
await animationController.forward();
await audioPlayer.play(DeviceFileSource(path));
}
Future<void> pause() async {
await animationController.reverse();
await audioPlayer.pause();
}
Future<void> to(value) async {
await audioPlayer.seek(state.currentDuration.value);
state.handleChange.value = false;
}
Future<void> changeValue(value) async {
state.handleChange.value = true;
state.currentDuration.value = Duration(milliseconds: (state.totalDuration.value.inMilliseconds * value).toInt());
}
}