diff --git a/lib/common/utils/click_with_music_controller.dart b/lib/common/utils/click_with_music_controller.dart index 9f14323..f0f6d26 100644 --- a/lib/common/utils/click_with_music_controller.dart +++ b/lib/common/utils/click_with_music_controller.dart @@ -11,6 +11,8 @@ class ClickWithMusicController { static ClickWithMusicController? _instance; + static String TAG = 'ClickWithMusicController'; + ClickWithMusicController._privateConstructor(); static ClickWithMusicController get instance => _instance ??= ClickWithMusicController._privateConstructor(); @@ -21,11 +23,14 @@ class ClickWithMusicController { ///@param action 可以是同步函数也可以是异步函数 Future playMusicAndPerformAction(BuildContext? context, AudioPlayerUtilType audioType, FutureOr Function() action) async { + Log.d("$TAG playMusicAndPerformAction _isPlaying=$_isPlaying"); + ///todo 是否需要考虑打断覆盖能力 if (_isPlaying) return; _isPlaying = true; - Log.d("WQF playMusicAndPerformAction playAudio begin"); + Log.d("$TAG playMusicAndPerformAction playAudio begin"); + await AudioPlayerUtil.getInstance().pause(); // Play the music await AudioPlayerUtil.getInstance() .playAudio(audioType); @@ -33,15 +38,16 @@ class ClickWithMusicController { try { await Future.sync(action); } catch (e) { - Log.d('WQF playMusicAndPerformAction exception $e'); + Log.d('$TAG playMusicAndPerformAction exception $e'); } finally { - Log.d("WQF playMusicAndPerformAction playAudio end"); + Log.d("$TAG playMusicAndPerformAction playAudio end"); _isPlaying = false; } } - void reset() { + Future reset() async { _isPlaying = false; + await AudioPlayerUtil.getInstance().stop(); } // void dispose() { diff --git a/lib/common/widgets/cheer_reward_widget.dart b/lib/common/widgets/cheer_reward_widget.dart index d967917..155da27 100644 --- a/lib/common/widgets/cheer_reward_widget.dart +++ b/lib/common/widgets/cheer_reward_widget.dart @@ -54,13 +54,13 @@ class _CheerRewardWidgetState extends State } void _startAnimation() { - Log.d("$TAG _startAnimation"); + Log.d("$TAG ${identityHashCode(this)} _startAnimation"); setState(() { _isVisible = true; }); _futureComposition.then((composition) { - Log.d("$TAG _futureComposition.then duration=${composition.duration}"); + Log.d("$TAG ${identityHashCode(this)} _futureComposition.then duration=${composition.duration}"); _controller.duration = composition.duration; _controller.forward().whenCompleteOrCancel(() { if (mounted) { diff --git a/lib/common/widgets/recorder_widget.dart b/lib/common/widgets/recorder_widget.dart index 433580e..db50527 100644 --- a/lib/common/widgets/recorder_widget.dart +++ b/lib/common/widgets/recorder_widget.dart @@ -14,7 +14,7 @@ class RecorderWidget extends StatefulWidget { const RecorderWidget({ Key? key, - required this.isClickable, + this.isClickable = true, required this.isPlaying, required this.onTap, required this.width, diff --git a/lib/common/widgets/speaker_widget.dart b/lib/common/widgets/speaker_widget.dart index 39f2a27..68178ed 100644 --- a/lib/common/widgets/speaker_widget.dart +++ b/lib/common/widgets/speaker_widget.dart @@ -52,7 +52,10 @@ class _SpeakerWidgetState extends State Log.d( "$TAG ${identityHashCode(this)} initState widget=${widget.isPlaying} _isPlaying=$_isPlaying _controller=${identityHashCode(_controller)}"); if (widget.isPlaying) { - _startAnimation(); + ///fixme 增加200毫秒延迟,避免一进入就已经开始播了,效果有待观察 + Future.delayed(const Duration(milliseconds: 200), () { + _startAnimation(); + }); } } diff --git a/lib/models/voice_result_type.dart b/lib/models/voice_result_type.dart new file mode 100644 index 0000000..6edd37f --- /dev/null +++ b/lib/models/voice_result_type.dart @@ -0,0 +1,29 @@ +import 'package:wow_english/utils/audio_player_util.dart'; + +/// 语音评测结果聚合类 +class VoiceResultType { + /// lottie动画文件路径 + final String? lottieFilePath; + /// 音效 + final AudioPlayerUtilType audioType; + /// 得分范围最小值 + final int minScore; + /// 得分范围最大值 + final int maxScore; + + const VoiceResultType._(this.lottieFilePath, this.audioType, this.minScore, this.maxScore); + + static const VoiceResultType level1 = VoiceResultType._('assets/lotties/excellent.zip', AudioPlayerUtilType.excellent, 90, 100); + static const VoiceResultType level2 = VoiceResultType._('assets/lotties/great.zip', AudioPlayerUtilType.great, 70, 89); + static const VoiceResultType level3 = VoiceResultType._('assets/lotties/good.zip', AudioPlayerUtilType.good, 50, 69); + static const VoiceResultType level4 = VoiceResultType._(null, AudioPlayerUtilType.tryAgain, 0, 49); + + static List get values => [level1, level2, level3, level4]; + + static VoiceResultType fromScore(int score) { + return values.firstWhere( + (type) => score >= type.minScore && score <= type.maxScore, + orElse: () => level4, // 默认返回level4 + ); + } +} \ No newline at end of file diff --git a/lib/pages/practice/bloc/topic_picture_bloc.dart b/lib/pages/practice/bloc/topic_picture_bloc.dart index da136cb..4f0742a 100644 --- a/lib/pages/practice/bloc/topic_picture_bloc.dart +++ b/lib/pages/practice/bloc/topic_picture_bloc.dart @@ -20,8 +20,8 @@ import 'package:wow_english/utils/toast_util.dart'; import '../../../common/permission/permissionRequester.dart'; import '../../../common/utils/click_with_music_controller.dart'; import '../../../common/utils/show_star_reward_dialog.dart'; +import '../../../models/voice_result_type.dart'; import '../../../route/route.dart'; -import '../../../utils/log_util.dart'; part 'topic_picture_event.dart'; @@ -49,7 +49,7 @@ class TopicPictureBloc int _currentPage = 0; - int _selectItem = -1; + int _optionSelectItem = -1; CourseProcessEntity? _entity; @@ -61,19 +61,10 @@ class TopicPictureBloc ///正在播放音频 VoicePlayState _voicePlayState = VoicePlayState.unKnow; - // 是否是回答(选择)结果音效 - bool _isResultSoundPlaying = false; - - bool get isResultSoundPlaying => _isResultSoundPlaying; - - // 答对播放音效时禁止任何点击(选择)操作 - bool _forbiddenWhenCorrect = false; - - bool get forbiddenWhenCorrect => _forbiddenWhenCorrect; - int get currentPage => _currentPage + 1; - int get selectItem => _selectItem; + /// 选择题选中项 + int get optionSelectItem => _optionSelectItem; bool get isRecording => _isRecording; @@ -103,46 +94,24 @@ class TopicPictureBloc //音频播放器 audioPlayer = AudioPlayer(); audioPlayer.onPlayerStateChanged.listen((event) async { - debugPrint( - '播放状态变化 _voicePlayState=$_voicePlayState event=$event _isResultSoundPlaying=$_isResultSoundPlaying _forbiddenWhenCorrect=$_forbiddenWhenCorrect'); - if (_isResultSoundPlaying) { - if (event != PlayerState.playing) { - _isResultSoundPlaying = false; - if (_forbiddenWhenCorrect) { - _forbiddenWhenCorrect = false; - debugPrint('播放完成后解除禁止'); - if (event == PlayerState.completed) { - if (isLastPage()) { - showStepPage(); - } else { - // 答对后且播放完自动翻页 - pageController.nextPage( - duration: const Duration(milliseconds: 250), - curve: Curves.ease, - ); - } - } - } - } - } else { - if (event == PlayerState.completed) { - debugPrint('播放完成'); - _voicePlayState = VoicePlayState.completed; - } - if (event == PlayerState.stopped) { - debugPrint('播放结束'); - _voicePlayState = VoicePlayState.stop; - } + debugPrint('播放状态变化 _voicePlayState=$_voicePlayState event=$event'); + if (event == PlayerState.completed) { + debugPrint('播放完成'); + _voicePlayState = VoicePlayState.completed; + } + if (event == PlayerState.stopped) { + debugPrint('播放结束'); + _voicePlayState = VoicePlayState.stop; + } - if (event == PlayerState.playing) { - debugPrint('正在播放中'); - _voicePlayState = VoicePlayState.playing; - } - if (isClosed) { - return; - } - add(VoicePlayStateChangeEvent()); + if (event == PlayerState.playing) { + debugPrint('正在播放中'); + _voicePlayState = VoicePlayState.playing; } + if (isClosed) { + return; + } + add(VoicePlayStateChangeEvent()); }); methodChannel = @@ -191,8 +160,6 @@ class TopicPictureBloc pageController.dispose(); audioPlayer.release(); audioPlayer.dispose(); - _isResultSoundPlaying = false; - _forbiddenWhenCorrect = false; _voiceXsCancel(); return super.close(); } @@ -213,7 +180,7 @@ class TopicPictureBloc ///页面切换 void _pageControllerChange(CurrentPageIndexChangeEvent event, Emitter emitter) async { - await closePlayerResource(); + await pageResetIfNeed(); debugPrint('翻页 $_currentPage->${event.pageIndex}'); if (_currentPage == _entity?.topics?.length) { return; @@ -229,26 +196,22 @@ class TopicPictureBloc } } } - _selectItem = -1; emitter(CurrentPageIndexState()); } ///选择 void _selectItemLoad( SelectItemEvent event, Emitter emitter) async { - if (_forbiddenWhenCorrect) { - return; - } - _selectItem = event.selectIndex; - if (checkAnswerRight(_selectItem) == true) { - _playResultSound(true); + _optionSelectItem = event.selectIndex; + emitter(SelectItemChangeState()); + if (checkAnswerRight(_optionSelectItem) == true) { + /// 如果选择题答(选)对后题目没播完,则暂停播放题目。答错的话继续播放体验也不错 + await closePlayerResource(); showStarRewardDialog(context); - // showToast('恭喜你,答对啦!',duration: const Duration(seconds: 2)); + await _playResultSound(true); } else { - _playResultSound(false); - // showToast('继续加油哦',duration: const Duration(seconds: 2)); + await _playResultSound(false); } - emitter(SelectItemChangeState()); } ///为空则数据异常,用于是否晃动时需要 @@ -290,69 +253,37 @@ class TopicPictureBloc } ///终止评测 - void _voiceXsStop( + Future _voiceXsStop( XSVoiceStopEvent event, Emitter emitter) async { methodChannel.invokeMethod('stopVoice'); } ///取消评测(用于处理退出页面后录音未停止等异常情况的保护操作) - void _voiceXsCancel() { - methodChannel.invokeMethod('cancelVoice'); + Future _voiceXsCancel({bool force = false}) async { + if (_isRecording || force) { + methodChannel.invokeMethod('cancelVoice'); + } } ///先声评测结果 void _voiceXsResult( XSVoiceResultEvent event, Emitter emitter) async { + _isRecording = false; + emitter(XSVoiceTestState()); final Map args = event.message as Map; final result = args['result'] as Map; final overall = result['overall'].toString(); int score = int.parse(overall); - AudioPlayerUtilType audioPlayerUtilType; - String? lottieFile; - if (score > 90) { - audioPlayerUtilType = AudioPlayerUtilType.excellent; - lottieFile = 'assets/lotties/excellent.zip'; - } else if (score > 70) { - audioPlayerUtilType = AudioPlayerUtilType.great; - lottieFile = 'assets/lotties/great.zip'; - } else if (score > 50) { - audioPlayerUtilType = AudioPlayerUtilType.good; - lottieFile = 'assets/lotties/good.zip'; - } else { - audioPlayerUtilType = AudioPlayerUtilType.tryAgain; - } - if (lottieFile != null) { - showCheerRewardDialog(context, lottieFile: lottieFile); - } - Log.d("WQF audioPlayerUtilType=$audioPlayerUtilType lottieFile=$lottieFile"); - ClickWithMusicController.instance - .playMusicAndPerformAction(context, audioPlayerUtilType, () => { - ///todo 是否需要自动翻页 - }); - // showToast('测评成功,分数是$overall', duration: const Duration(seconds: 5)); - _isRecording = false; - emitter(XSVoiceTestState()); - if (isLastPage()) { - showStepPage(); - } - } - - /// 根据得分计算星星数 - int _evaluateScore(String scoreStr) { - try { - int score = int.parse(scoreStr); - if (score > 80) { - return 3; - } else if (score > 60) { - return 2; - } else { - return 1; - } - } catch (e) { - // 如果转换失败,可以返回一个默认值或抛出异常 - print('Error parsing score: $e'); - return 1; // 返回一个默认值表示错误 + final voiceResult = VoiceResultType.fromScore(score); + if (voiceResult.lottieFilePath != null) { + showCheerRewardDialog(context, lottieFile: voiceResult.lottieFilePath!); } + await ClickWithMusicController.instance.playMusicAndPerformAction( + context, + voiceResult.audioType, + () { + if (isLastPage()) {showStepPage();}; + }); } // 暂时没用上 @@ -364,38 +295,46 @@ class TopicPictureBloc // 题目音频播放 void _questionVoicePlay( VoicePlayEvent event, Emitter emitter) async { - if (_forbiddenWhenCorrect) { - return; - } - _forbiddenWhenCorrect = false; - await closePlayerResource(); + await pageResetIfNeed(); final topics = _entity?.topics?[_currentPage]; final urlStr = topics?.audioUrl ?? ''; await audioPlayer.play(UrlSource(urlStr), balance: 0.0, ctx: AudioContext()); } + /// 重置状态,音频播放、录音以及一些变量等。用于翻页,打断等场景 + Future pageResetIfNeed() async { + _optionSelectItem = -1; + _isRecording = false; + _voicePlayState = VoicePlayState.stop; + + await closePlayerResource(); + await _voiceXsCancel(); + } + Future closePlayerResource() async { - if (voicePlayState == VoicePlayState.playing || _isResultSoundPlaying) { + if (voicePlayState == VoicePlayState.playing) { await audioPlayer.stop(); } + await ClickWithMusicController.instance.reset(); } ///播放选择结果音效 - void _playResultSound(bool isCorrect) async { - // await audioPlayer.stop(); - if (audioPlayer.state == PlayerState.playing && - _isResultSoundPlaying == false) { - _voicePlayState = VoicePlayState.stop; - } - debugPrint("_playResultSound isCorrect=$isCorrect"); - _isResultSoundPlaying = true; - _forbiddenWhenCorrect = isCorrect; - if (isCorrect) { - await audioPlayer.play(AssetSource('right'.assetMp3)); - } else { - await audioPlayer.play(AssetSource('wrong'.assetMp3)); - } + Future _playResultSound(bool isCorrect) async { + await ClickWithMusicController.instance.playMusicAndPerformAction(context, + isCorrect ? AudioPlayerUtilType.right : AudioPlayerUtilType.wrong, () { + if (isCorrect) { + if (isLastPage()) { + showStepPage(); + } else { + // 答对后且播放完自动翻页 + pageController.nextPage( + duration: const Duration(milliseconds: 250), + curve: Curves.ease, + ); + } + } + }); } ///是否是最后一页 diff --git a/lib/pages/practice/topic_picture_page.dart b/lib/pages/practice/topic_picture_page.dart index c3eca11..7eaa996 100644 --- a/lib/pages/practice/topic_picture_page.dart +++ b/lib/pages/practice/topic_picture_page.dart @@ -173,7 +173,7 @@ class _TopicPicturePage extends StatelessWidget { buildWhen: (_, s) => s is SelectItemChangeState, builder: (context, state) { final bloc = BlocProvider.of(context); - final isAnswerOption = bloc.selectItem == index; + final isAnswerOption = bloc.optionSelectItem == index; final answerCorrect = isAnswerOption && bloc.checkAnswerRight(index) == true; return Container( @@ -243,7 +243,7 @@ class _TopicPicturePage extends StatelessWidget { buildWhen: (_, s) => s is SelectItemChangeState, builder: (context, state) { final bloc = BlocProvider.of(context); - final isAnswerOption = bloc.selectItem == index; + final isAnswerOption = bloc.optionSelectItem == index; final answerCorrect = isAnswerOption && bloc.checkAnswerRight(index) == true; return Container( @@ -349,7 +349,7 @@ class _TopicPicturePage extends StatelessWidget { buildWhen: (_, s) => s is SelectItemChangeState, builder: (context, state) { final bloc = BlocProvider.of(context); - final isAnswerOption = bloc.selectItem == index; + final isAnswerOption = bloc.optionSelectItem == index; final answerCorrect = isAnswerOption && bloc.checkAnswerRight(index) == true; return OptionWidget( @@ -423,7 +423,7 @@ class _TopicPicturePage extends StatelessWidget { buildWhen: (_, s) => s is SelectItemChangeState, builder: (context, state) { final bloc = BlocProvider.of(context); - final isAnswerOption = bloc.selectItem == index; + final isAnswerOption = bloc.optionSelectItem == index; final answerCorrect = isAnswerOption && bloc.checkAnswerRight(index) == true; return OptionWidget( @@ -522,8 +522,6 @@ class _TopicPicturePage extends StatelessWidget { 70.verticalSpace, RecorderWidget( isPlaying: bloc.isRecording, - isClickable: - bloc.voicePlayState != VoicePlayState.playing, width: 72.w, height: 72.w, onTap: () { diff --git a/lib/utils/audio_player_util.dart b/lib/utils/audio_player_util.dart index 0ead8d3..dad5c10 100644 --- a/lib/utils/audio_player_util.dart +++ b/lib/utils/audio_player_util.dart @@ -18,7 +18,9 @@ enum AudioPlayerUtilType { excellent('excellent'), great('great'), good('good'), - tryAgain('try_again'); + tryAgain('try_again'), + right('right'), + wrong('wrong'); const AudioPlayerUtilType(this.path);