Loading... ## 写在前面 ### 成品展示    ### 相关依赖 * get * path_provider * uuid * record * audioplayers * permission_handler ## 录音组件 <div class="tab-container post_tab box-shadow-wrap-lg"> <ul class="nav no-padder b-b scroll-hide" role="tablist"> <li class='nav-item active' role="presentation"><a class='nav-link active' style="" data-toggle="tab" aria-controls='tabs-cae8a234d651d633646e1b85c4b158e1270' role="tab" data-target='#tabs-cae8a234d651d633646e1b85c4b158e1270'>View</a></li><li class='nav-item ' role="presentation"><a class='nav-link ' style="" data-toggle="tab" aria-controls='tabs-47ed7648e717a64da283a159f510ac66921' role="tab" data-target='#tabs-47ed7648e717a64da283a159f510ac66921'>State</a></li><li class='nav-item ' role="presentation"><a class='nav-link ' style="" data-toggle="tab" aria-controls='tabs-77231c85055f541e3ccada91d66b1dcd732' role="tab" data-target='#tabs-77231c85055f541e3ccada91d66b1dcd732'>Logic</a></li> </ul> <div class="tab-content no-border"> <div role="tabpanel" id='tabs-cae8a234d651d633646e1b85c4b158e1270' class="tab-pane fade active in"> ```dart 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, ), ); }), ], ), ); }, ); } } ``` </div><div role="tabpanel" id='tabs-47ed7648e717a64da283a159f510ac66921' class="tab-pane fade "> ```dart 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 } } ``` </div><div role="tabpanel" id='tabs-77231c85055f541e3ccada91d66b1dcd732' class="tab-pane fade "> ```dart 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(); } } ``` </div> </div> </div> ## 频谱组件 <div class="tab-container post_tab box-shadow-wrap-lg"> <ul class="nav no-padder b-b scroll-hide" role="tablist"> <li class='nav-item active' role="presentation"><a class='nav-link active' style="" data-toggle="tab" aria-controls='tabs-d969834b2c5f6e49e413e89dfb3b314f460' role="tab" data-target='#tabs-d969834b2c5f6e49e413e89dfb3b314f460'>View</a></li> </ul> <div class="tab-content no-border"> <div role="tabpanel" id='tabs-d969834b2c5f6e49e413e89dfb3b314f460' class="tab-pane fade active in"> ```dart 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), ); } } ``` </div> </div> </div> ## 播放组件 <div class="tab-container post_tab box-shadow-wrap-lg"> <ul class="nav no-padder b-b scroll-hide" role="tablist"> <li class='nav-item active' role="presentation"><a class='nav-link active' style="" data-toggle="tab" aria-controls='tabs-75543ba9e204ed6a9746a7e97fcbf860130' role="tab" data-target='#tabs-75543ba9e204ed6a9746a7e97fcbf860130'>View</a></li><li class='nav-item ' role="presentation"><a class='nav-link ' style="" data-toggle="tab" aria-controls='tabs-daca0a6b8327e91dc84f27848ed8377b511' role="tab" data-target='#tabs-daca0a6b8327e91dc84f27848ed8377b511'>State</a></li><li class='nav-item ' role="presentation"><a class='nav-link ' style="" data-toggle="tab" aria-controls='tabs-49d325ca2bb8531ad2bbf86a3406a116582' role="tab" data-target='#tabs-49d325ca2bb8531ad2bbf86a3406a116582'>Logic</a></li> </ul> <div class="tab-content no-border"> <div role="tabpanel" id='tabs-75543ba9e204ed6a9746a7e97fcbf860130' class="tab-pane fade active in"> ```dart 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), ) ] ], ), ); }, ); } } ``` </div><div role="tabpanel" id='tabs-daca0a6b8327e91dc84f27848ed8377b511' class="tab-pane fade "> ```dart 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 } } ``` </div><div role="tabpanel" id='tabs-49d325ca2bb8531ad2bbf86a3406a116582' class="tab-pane fade "> ```dart 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()); } } ``` </div> </div> </div> © 允许规范转载 打赏 赞赏作者 微信 赞 1