Commit 819ae43b6372c182f6ab91a6313e226000746817
1 parent
081fbff7
feat:体验优化-练习题目取消阻塞,支持任何条件下的点击,增加体验流畅感
Showing
8 changed files
with
125 additions
and
148 deletions
lib/common/utils/click_with_music_controller.dart
| @@ -11,6 +11,8 @@ class ClickWithMusicController { | @@ -11,6 +11,8 @@ class ClickWithMusicController { | ||
| 11 | 11 | ||
| 12 | static ClickWithMusicController? _instance; | 12 | static ClickWithMusicController? _instance; |
| 13 | 13 | ||
| 14 | + static String TAG = 'ClickWithMusicController'; | ||
| 15 | + | ||
| 14 | ClickWithMusicController._privateConstructor(); | 16 | ClickWithMusicController._privateConstructor(); |
| 15 | 17 | ||
| 16 | static ClickWithMusicController get instance => _instance ??= ClickWithMusicController._privateConstructor(); | 18 | static ClickWithMusicController get instance => _instance ??= ClickWithMusicController._privateConstructor(); |
| @@ -21,11 +23,14 @@ class ClickWithMusicController { | @@ -21,11 +23,14 @@ class ClickWithMusicController { | ||
| 21 | ///@param action 可以是同步函数也可以是异步函数 | 23 | ///@param action 可以是同步函数也可以是异步函数 |
| 22 | Future<void> playMusicAndPerformAction(BuildContext? context, | 24 | Future<void> playMusicAndPerformAction(BuildContext? context, |
| 23 | AudioPlayerUtilType audioType, FutureOr<void> Function() action) async { | 25 | AudioPlayerUtilType audioType, FutureOr<void> Function() action) async { |
| 26 | + Log.d("$TAG playMusicAndPerformAction _isPlaying=$_isPlaying"); | ||
| 27 | + ///todo 是否需要考虑打断覆盖能力 | ||
| 24 | if (_isPlaying) return; | 28 | if (_isPlaying) return; |
| 25 | 29 | ||
| 26 | _isPlaying = true; | 30 | _isPlaying = true; |
| 27 | - Log.d("WQF playMusicAndPerformAction playAudio begin"); | 31 | + Log.d("$TAG playMusicAndPerformAction playAudio begin"); |
| 28 | 32 | ||
| 33 | + await AudioPlayerUtil.getInstance().pause(); | ||
| 29 | // Play the music | 34 | // Play the music |
| 30 | await AudioPlayerUtil.getInstance() | 35 | await AudioPlayerUtil.getInstance() |
| 31 | .playAudio(audioType); | 36 | .playAudio(audioType); |
| @@ -33,15 +38,16 @@ class ClickWithMusicController { | @@ -33,15 +38,16 @@ class ClickWithMusicController { | ||
| 33 | try { | 38 | try { |
| 34 | await Future.sync(action); | 39 | await Future.sync(action); |
| 35 | } catch (e) { | 40 | } catch (e) { |
| 36 | - Log.d('WQF playMusicAndPerformAction exception $e'); | 41 | + Log.d('$TAG playMusicAndPerformAction exception $e'); |
| 37 | } finally { | 42 | } finally { |
| 38 | - Log.d("WQF playMusicAndPerformAction playAudio end"); | 43 | + Log.d("$TAG playMusicAndPerformAction playAudio end"); |
| 39 | _isPlaying = false; | 44 | _isPlaying = false; |
| 40 | } | 45 | } |
| 41 | } | 46 | } |
| 42 | 47 | ||
| 43 | - void reset() { | 48 | + Future<void> reset() async { |
| 44 | _isPlaying = false; | 49 | _isPlaying = false; |
| 50 | + await AudioPlayerUtil.getInstance().stop(); | ||
| 45 | } | 51 | } |
| 46 | 52 | ||
| 47 | // void dispose() { | 53 | // void dispose() { |
lib/common/widgets/cheer_reward_widget.dart
| @@ -54,13 +54,13 @@ class _CheerRewardWidgetState extends State<CheerRewardWidget> | @@ -54,13 +54,13 @@ class _CheerRewardWidgetState extends State<CheerRewardWidget> | ||
| 54 | } | 54 | } |
| 55 | 55 | ||
| 56 | void _startAnimation() { | 56 | void _startAnimation() { |
| 57 | - Log.d("$TAG _startAnimation"); | 57 | + Log.d("$TAG ${identityHashCode(this)} _startAnimation"); |
| 58 | setState(() { | 58 | setState(() { |
| 59 | _isVisible = true; | 59 | _isVisible = true; |
| 60 | }); | 60 | }); |
| 61 | 61 | ||
| 62 | _futureComposition.then((composition) { | 62 | _futureComposition.then((composition) { |
| 63 | - Log.d("$TAG _futureComposition.then duration=${composition.duration}"); | 63 | + Log.d("$TAG ${identityHashCode(this)} _futureComposition.then duration=${composition.duration}"); |
| 64 | _controller.duration = composition.duration; | 64 | _controller.duration = composition.duration; |
| 65 | _controller.forward().whenCompleteOrCancel(() { | 65 | _controller.forward().whenCompleteOrCancel(() { |
| 66 | if (mounted) { | 66 | if (mounted) { |
lib/common/widgets/recorder_widget.dart
| @@ -14,7 +14,7 @@ class RecorderWidget extends StatefulWidget { | @@ -14,7 +14,7 @@ class RecorderWidget extends StatefulWidget { | ||
| 14 | 14 | ||
| 15 | const RecorderWidget({ | 15 | const RecorderWidget({ |
| 16 | Key? key, | 16 | Key? key, |
| 17 | - required this.isClickable, | 17 | + this.isClickable = true, |
| 18 | required this.isPlaying, | 18 | required this.isPlaying, |
| 19 | required this.onTap, | 19 | required this.onTap, |
| 20 | required this.width, | 20 | required this.width, |
lib/common/widgets/speaker_widget.dart
| @@ -52,7 +52,10 @@ class _SpeakerWidgetState extends State<SpeakerWidget> | @@ -52,7 +52,10 @@ class _SpeakerWidgetState extends State<SpeakerWidget> | ||
| 52 | Log.d( | 52 | Log.d( |
| 53 | "$TAG ${identityHashCode(this)} initState widget=${widget.isPlaying} _isPlaying=$_isPlaying _controller=${identityHashCode(_controller)}"); | 53 | "$TAG ${identityHashCode(this)} initState widget=${widget.isPlaying} _isPlaying=$_isPlaying _controller=${identityHashCode(_controller)}"); |
| 54 | if (widget.isPlaying) { | 54 | if (widget.isPlaying) { |
| 55 | - _startAnimation(); | 55 | + ///fixme 增加200毫秒延迟,避免一进入就已经开始播了,效果有待观察 |
| 56 | + Future.delayed(const Duration(milliseconds: 200), () { | ||
| 57 | + _startAnimation(); | ||
| 58 | + }); | ||
| 56 | } | 59 | } |
| 57 | } | 60 | } |
| 58 | 61 |
lib/models/voice_result_type.dart
0 → 100644
| 1 | +import 'package:wow_english/utils/audio_player_util.dart'; | ||
| 2 | + | ||
| 3 | +/// 语音评测结果聚合类 | ||
| 4 | +class VoiceResultType { | ||
| 5 | + /// lottie动画文件路径 | ||
| 6 | + final String? lottieFilePath; | ||
| 7 | + /// 音效 | ||
| 8 | + final AudioPlayerUtilType audioType; | ||
| 9 | + /// 得分范围最小值 | ||
| 10 | + final int minScore; | ||
| 11 | + /// 得分范围最大值 | ||
| 12 | + final int maxScore; | ||
| 13 | + | ||
| 14 | + const VoiceResultType._(this.lottieFilePath, this.audioType, this.minScore, this.maxScore); | ||
| 15 | + | ||
| 16 | + static const VoiceResultType level1 = VoiceResultType._('assets/lotties/excellent.zip', AudioPlayerUtilType.excellent, 90, 100); | ||
| 17 | + static const VoiceResultType level2 = VoiceResultType._('assets/lotties/great.zip', AudioPlayerUtilType.great, 70, 89); | ||
| 18 | + static const VoiceResultType level3 = VoiceResultType._('assets/lotties/good.zip', AudioPlayerUtilType.good, 50, 69); | ||
| 19 | + static const VoiceResultType level4 = VoiceResultType._(null, AudioPlayerUtilType.tryAgain, 0, 49); | ||
| 20 | + | ||
| 21 | + static List<VoiceResultType> get values => [level1, level2, level3, level4]; | ||
| 22 | + | ||
| 23 | + static VoiceResultType fromScore(int score) { | ||
| 24 | + return values.firstWhere( | ||
| 25 | + (type) => score >= type.minScore && score <= type.maxScore, | ||
| 26 | + orElse: () => level4, // 默认返回level4 | ||
| 27 | + ); | ||
| 28 | + } | ||
| 29 | +} | ||
| 0 | \ No newline at end of file | 30 | \ No newline at end of file |
lib/pages/practice/bloc/topic_picture_bloc.dart
| @@ -20,8 +20,8 @@ import 'package:wow_english/utils/toast_util.dart'; | @@ -20,8 +20,8 @@ import 'package:wow_english/utils/toast_util.dart'; | ||
| 20 | import '../../../common/permission/permissionRequester.dart'; | 20 | import '../../../common/permission/permissionRequester.dart'; |
| 21 | import '../../../common/utils/click_with_music_controller.dart'; | 21 | import '../../../common/utils/click_with_music_controller.dart'; |
| 22 | import '../../../common/utils/show_star_reward_dialog.dart'; | 22 | import '../../../common/utils/show_star_reward_dialog.dart'; |
| 23 | +import '../../../models/voice_result_type.dart'; | ||
| 23 | import '../../../route/route.dart'; | 24 | import '../../../route/route.dart'; |
| 24 | -import '../../../utils/log_util.dart'; | ||
| 25 | 25 | ||
| 26 | part 'topic_picture_event.dart'; | 26 | part 'topic_picture_event.dart'; |
| 27 | 27 | ||
| @@ -49,7 +49,7 @@ class TopicPictureBloc | @@ -49,7 +49,7 @@ class TopicPictureBloc | ||
| 49 | 49 | ||
| 50 | int _currentPage = 0; | 50 | int _currentPage = 0; |
| 51 | 51 | ||
| 52 | - int _selectItem = -1; | 52 | + int _optionSelectItem = -1; |
| 53 | 53 | ||
| 54 | CourseProcessEntity? _entity; | 54 | CourseProcessEntity? _entity; |
| 55 | 55 | ||
| @@ -61,19 +61,10 @@ class TopicPictureBloc | @@ -61,19 +61,10 @@ class TopicPictureBloc | ||
| 61 | ///正在播放音频 | 61 | ///正在播放音频 |
| 62 | VoicePlayState _voicePlayState = VoicePlayState.unKnow; | 62 | VoicePlayState _voicePlayState = VoicePlayState.unKnow; |
| 63 | 63 | ||
| 64 | - // 是否是回答(选择)结果音效 | ||
| 65 | - bool _isResultSoundPlaying = false; | ||
| 66 | - | ||
| 67 | - bool get isResultSoundPlaying => _isResultSoundPlaying; | ||
| 68 | - | ||
| 69 | - // 答对播放音效时禁止任何点击(选择)操作 | ||
| 70 | - bool _forbiddenWhenCorrect = false; | ||
| 71 | - | ||
| 72 | - bool get forbiddenWhenCorrect => _forbiddenWhenCorrect; | ||
| 73 | - | ||
| 74 | int get currentPage => _currentPage + 1; | 64 | int get currentPage => _currentPage + 1; |
| 75 | 65 | ||
| 76 | - int get selectItem => _selectItem; | 66 | + /// 选择题选中项 |
| 67 | + int get optionSelectItem => _optionSelectItem; | ||
| 77 | 68 | ||
| 78 | bool get isRecording => _isRecording; | 69 | bool get isRecording => _isRecording; |
| 79 | 70 | ||
| @@ -103,46 +94,24 @@ class TopicPictureBloc | @@ -103,46 +94,24 @@ class TopicPictureBloc | ||
| 103 | //音频播放器 | 94 | //音频播放器 |
| 104 | audioPlayer = AudioPlayer(); | 95 | audioPlayer = AudioPlayer(); |
| 105 | audioPlayer.onPlayerStateChanged.listen((event) async { | 96 | audioPlayer.onPlayerStateChanged.listen((event) async { |
| 106 | - debugPrint( | ||
| 107 | - '播放状态变化 _voicePlayState=$_voicePlayState event=$event _isResultSoundPlaying=$_isResultSoundPlaying _forbiddenWhenCorrect=$_forbiddenWhenCorrect'); | ||
| 108 | - if (_isResultSoundPlaying) { | ||
| 109 | - if (event != PlayerState.playing) { | ||
| 110 | - _isResultSoundPlaying = false; | ||
| 111 | - if (_forbiddenWhenCorrect) { | ||
| 112 | - _forbiddenWhenCorrect = false; | ||
| 113 | - debugPrint('播放完成后解除禁止'); | ||
| 114 | - if (event == PlayerState.completed) { | ||
| 115 | - if (isLastPage()) { | ||
| 116 | - showStepPage(); | ||
| 117 | - } else { | ||
| 118 | - // 答对后且播放完自动翻页 | ||
| 119 | - pageController.nextPage( | ||
| 120 | - duration: const Duration(milliseconds: 250), | ||
| 121 | - curve: Curves.ease, | ||
| 122 | - ); | ||
| 123 | - } | ||
| 124 | - } | ||
| 125 | - } | ||
| 126 | - } | ||
| 127 | - } else { | ||
| 128 | - if (event == PlayerState.completed) { | ||
| 129 | - debugPrint('播放完成'); | ||
| 130 | - _voicePlayState = VoicePlayState.completed; | ||
| 131 | - } | ||
| 132 | - if (event == PlayerState.stopped) { | ||
| 133 | - debugPrint('播放结束'); | ||
| 134 | - _voicePlayState = VoicePlayState.stop; | ||
| 135 | - } | 97 | + debugPrint('播放状态变化 _voicePlayState=$_voicePlayState event=$event'); |
| 98 | + if (event == PlayerState.completed) { | ||
| 99 | + debugPrint('播放完成'); | ||
| 100 | + _voicePlayState = VoicePlayState.completed; | ||
| 101 | + } | ||
| 102 | + if (event == PlayerState.stopped) { | ||
| 103 | + debugPrint('播放结束'); | ||
| 104 | + _voicePlayState = VoicePlayState.stop; | ||
| 105 | + } | ||
| 136 | 106 | ||
| 137 | - if (event == PlayerState.playing) { | ||
| 138 | - debugPrint('正在播放中'); | ||
| 139 | - _voicePlayState = VoicePlayState.playing; | ||
| 140 | - } | ||
| 141 | - if (isClosed) { | ||
| 142 | - return; | ||
| 143 | - } | ||
| 144 | - add(VoicePlayStateChangeEvent()); | 107 | + if (event == PlayerState.playing) { |
| 108 | + debugPrint('正在播放中'); | ||
| 109 | + _voicePlayState = VoicePlayState.playing; | ||
| 145 | } | 110 | } |
| 111 | + if (isClosed) { | ||
| 112 | + return; | ||
| 113 | + } | ||
| 114 | + add(VoicePlayStateChangeEvent()); | ||
| 146 | }); | 115 | }); |
| 147 | 116 | ||
| 148 | methodChannel = | 117 | methodChannel = |
| @@ -191,8 +160,6 @@ class TopicPictureBloc | @@ -191,8 +160,6 @@ class TopicPictureBloc | ||
| 191 | pageController.dispose(); | 160 | pageController.dispose(); |
| 192 | audioPlayer.release(); | 161 | audioPlayer.release(); |
| 193 | audioPlayer.dispose(); | 162 | audioPlayer.dispose(); |
| 194 | - _isResultSoundPlaying = false; | ||
| 195 | - _forbiddenWhenCorrect = false; | ||
| 196 | _voiceXsCancel(); | 163 | _voiceXsCancel(); |
| 197 | return super.close(); | 164 | return super.close(); |
| 198 | } | 165 | } |
| @@ -213,7 +180,7 @@ class TopicPictureBloc | @@ -213,7 +180,7 @@ class TopicPictureBloc | ||
| 213 | ///页面切换 | 180 | ///页面切换 |
| 214 | void _pageControllerChange(CurrentPageIndexChangeEvent event, | 181 | void _pageControllerChange(CurrentPageIndexChangeEvent event, |
| 215 | Emitter<TopicPictureState> emitter) async { | 182 | Emitter<TopicPictureState> emitter) async { |
| 216 | - await closePlayerResource(); | 183 | + await pageResetIfNeed(); |
| 217 | debugPrint('翻页 $_currentPage->${event.pageIndex}'); | 184 | debugPrint('翻页 $_currentPage->${event.pageIndex}'); |
| 218 | if (_currentPage == _entity?.topics?.length) { | 185 | if (_currentPage == _entity?.topics?.length) { |
| 219 | return; | 186 | return; |
| @@ -229,26 +196,22 @@ class TopicPictureBloc | @@ -229,26 +196,22 @@ class TopicPictureBloc | ||
| 229 | } | 196 | } |
| 230 | } | 197 | } |
| 231 | } | 198 | } |
| 232 | - _selectItem = -1; | ||
| 233 | emitter(CurrentPageIndexState()); | 199 | emitter(CurrentPageIndexState()); |
| 234 | } | 200 | } |
| 235 | 201 | ||
| 236 | ///选择 | 202 | ///选择 |
| 237 | void _selectItemLoad( | 203 | void _selectItemLoad( |
| 238 | SelectItemEvent event, Emitter<TopicPictureState> emitter) async { | 204 | SelectItemEvent event, Emitter<TopicPictureState> emitter) async { |
| 239 | - if (_forbiddenWhenCorrect) { | ||
| 240 | - return; | ||
| 241 | - } | ||
| 242 | - _selectItem = event.selectIndex; | ||
| 243 | - if (checkAnswerRight(_selectItem) == true) { | ||
| 244 | - _playResultSound(true); | 205 | + _optionSelectItem = event.selectIndex; |
| 206 | + emitter(SelectItemChangeState()); | ||
| 207 | + if (checkAnswerRight(_optionSelectItem) == true) { | ||
| 208 | + /// 如果选择题答(选)对后题目没播完,则暂停播放题目。答错的话继续播放体验也不错 | ||
| 209 | + await closePlayerResource(); | ||
| 245 | showStarRewardDialog(context); | 210 | showStarRewardDialog(context); |
| 246 | - // showToast('恭喜你,答对啦!',duration: const Duration(seconds: 2)); | 211 | + await _playResultSound(true); |
| 247 | } else { | 212 | } else { |
| 248 | - _playResultSound(false); | ||
| 249 | - // showToast('继续加油哦',duration: const Duration(seconds: 2)); | 213 | + await _playResultSound(false); |
| 250 | } | 214 | } |
| 251 | - emitter(SelectItemChangeState()); | ||
| 252 | } | 215 | } |
| 253 | 216 | ||
| 254 | ///为空则数据异常,用于是否晃动时需要 | 217 | ///为空则数据异常,用于是否晃动时需要 |
| @@ -290,69 +253,37 @@ class TopicPictureBloc | @@ -290,69 +253,37 @@ class TopicPictureBloc | ||
| 290 | } | 253 | } |
| 291 | 254 | ||
| 292 | ///终止评测 | 255 | ///终止评测 |
| 293 | - void _voiceXsStop( | 256 | + Future<void> _voiceXsStop( |
| 294 | XSVoiceStopEvent event, Emitter<TopicPictureState> emitter) async { | 257 | XSVoiceStopEvent event, Emitter<TopicPictureState> emitter) async { |
| 295 | methodChannel.invokeMethod('stopVoice'); | 258 | methodChannel.invokeMethod('stopVoice'); |
| 296 | } | 259 | } |
| 297 | 260 | ||
| 298 | ///取消评测(用于处理退出页面后录音未停止等异常情况的保护操作) | 261 | ///取消评测(用于处理退出页面后录音未停止等异常情况的保护操作) |
| 299 | - void _voiceXsCancel() { | ||
| 300 | - methodChannel.invokeMethod('cancelVoice'); | 262 | + Future<void> _voiceXsCancel({bool force = false}) async { |
| 263 | + if (_isRecording || force) { | ||
| 264 | + methodChannel.invokeMethod('cancelVoice'); | ||
| 265 | + } | ||
| 301 | } | 266 | } |
| 302 | 267 | ||
| 303 | ///先声评测结果 | 268 | ///先声评测结果 |
| 304 | void _voiceXsResult( | 269 | void _voiceXsResult( |
| 305 | XSVoiceResultEvent event, Emitter<TopicPictureState> emitter) async { | 270 | XSVoiceResultEvent event, Emitter<TopicPictureState> emitter) async { |
| 271 | + _isRecording = false; | ||
| 272 | + emitter(XSVoiceTestState()); | ||
| 306 | final Map args = event.message as Map; | 273 | final Map args = event.message as Map; |
| 307 | final result = args['result'] as Map; | 274 | final result = args['result'] as Map; |
| 308 | final overall = result['overall'].toString(); | 275 | final overall = result['overall'].toString(); |
| 309 | int score = int.parse(overall); | 276 | int score = int.parse(overall); |
| 310 | - AudioPlayerUtilType audioPlayerUtilType; | ||
| 311 | - String? lottieFile; | ||
| 312 | - if (score > 90) { | ||
| 313 | - audioPlayerUtilType = AudioPlayerUtilType.excellent; | ||
| 314 | - lottieFile = 'assets/lotties/excellent.zip'; | ||
| 315 | - } else if (score > 70) { | ||
| 316 | - audioPlayerUtilType = AudioPlayerUtilType.great; | ||
| 317 | - lottieFile = 'assets/lotties/great.zip'; | ||
| 318 | - } else if (score > 50) { | ||
| 319 | - audioPlayerUtilType = AudioPlayerUtilType.good; | ||
| 320 | - lottieFile = 'assets/lotties/good.zip'; | ||
| 321 | - } else { | ||
| 322 | - audioPlayerUtilType = AudioPlayerUtilType.tryAgain; | ||
| 323 | - } | ||
| 324 | - if (lottieFile != null) { | ||
| 325 | - showCheerRewardDialog(context, lottieFile: lottieFile); | ||
| 326 | - } | ||
| 327 | - Log.d("WQF audioPlayerUtilType=$audioPlayerUtilType lottieFile=$lottieFile"); | ||
| 328 | - ClickWithMusicController.instance | ||
| 329 | - .playMusicAndPerformAction(context, audioPlayerUtilType, () => { | ||
| 330 | - ///todo 是否需要自动翻页 | ||
| 331 | - }); | ||
| 332 | - // showToast('测评成功,分数是$overall', duration: const Duration(seconds: 5)); | ||
| 333 | - _isRecording = false; | ||
| 334 | - emitter(XSVoiceTestState()); | ||
| 335 | - if (isLastPage()) { | ||
| 336 | - showStepPage(); | ||
| 337 | - } | ||
| 338 | - } | ||
| 339 | - | ||
| 340 | - /// 根据得分计算星星数 | ||
| 341 | - int _evaluateScore(String scoreStr) { | ||
| 342 | - try { | ||
| 343 | - int score = int.parse(scoreStr); | ||
| 344 | - if (score > 80) { | ||
| 345 | - return 3; | ||
| 346 | - } else if (score > 60) { | ||
| 347 | - return 2; | ||
| 348 | - } else { | ||
| 349 | - return 1; | ||
| 350 | - } | ||
| 351 | - } catch (e) { | ||
| 352 | - // 如果转换失败,可以返回一个默认值或抛出异常 | ||
| 353 | - print('Error parsing score: $e'); | ||
| 354 | - return 1; // 返回一个默认值表示错误 | 277 | + final voiceResult = VoiceResultType.fromScore(score); |
| 278 | + if (voiceResult.lottieFilePath != null) { | ||
| 279 | + showCheerRewardDialog(context, lottieFile: voiceResult.lottieFilePath!); | ||
| 355 | } | 280 | } |
| 281 | + await ClickWithMusicController.instance.playMusicAndPerformAction( | ||
| 282 | + context, | ||
| 283 | + voiceResult.audioType, | ||
| 284 | + () { | ||
| 285 | + if (isLastPage()) {showStepPage();}; | ||
| 286 | + }); | ||
| 356 | } | 287 | } |
| 357 | 288 | ||
| 358 | // 暂时没用上 | 289 | // 暂时没用上 |
| @@ -364,38 +295,46 @@ class TopicPictureBloc | @@ -364,38 +295,46 @@ class TopicPictureBloc | ||
| 364 | // 题目音频播放 | 295 | // 题目音频播放 |
| 365 | void _questionVoicePlay( | 296 | void _questionVoicePlay( |
| 366 | VoicePlayEvent event, Emitter<TopicPictureState> emitter) async { | 297 | VoicePlayEvent event, Emitter<TopicPictureState> emitter) async { |
| 367 | - if (_forbiddenWhenCorrect) { | ||
| 368 | - return; | ||
| 369 | - } | ||
| 370 | - _forbiddenWhenCorrect = false; | ||
| 371 | - await closePlayerResource(); | 298 | + await pageResetIfNeed(); |
| 372 | final topics = _entity?.topics?[_currentPage]; | 299 | final topics = _entity?.topics?[_currentPage]; |
| 373 | final urlStr = topics?.audioUrl ?? ''; | 300 | final urlStr = topics?.audioUrl ?? ''; |
| 374 | await audioPlayer.play(UrlSource(urlStr), | 301 | await audioPlayer.play(UrlSource(urlStr), |
| 375 | balance: 0.0, ctx: AudioContext()); | 302 | balance: 0.0, ctx: AudioContext()); |
| 376 | } | 303 | } |
| 377 | 304 | ||
| 305 | + /// 重置状态,音频播放、录音以及一些变量等。用于翻页,打断等场景 | ||
| 306 | + Future<void> pageResetIfNeed() async { | ||
| 307 | + _optionSelectItem = -1; | ||
| 308 | + _isRecording = false; | ||
| 309 | + _voicePlayState = VoicePlayState.stop; | ||
| 310 | + | ||
| 311 | + await closePlayerResource(); | ||
| 312 | + await _voiceXsCancel(); | ||
| 313 | + } | ||
| 314 | + | ||
| 378 | Future<void> closePlayerResource() async { | 315 | Future<void> closePlayerResource() async { |
| 379 | - if (voicePlayState == VoicePlayState.playing || _isResultSoundPlaying) { | 316 | + if (voicePlayState == VoicePlayState.playing) { |
| 380 | await audioPlayer.stop(); | 317 | await audioPlayer.stop(); |
| 381 | } | 318 | } |
| 319 | + await ClickWithMusicController.instance.reset(); | ||
| 382 | } | 320 | } |
| 383 | 321 | ||
| 384 | ///播放选择结果音效 | 322 | ///播放选择结果音效 |
| 385 | - void _playResultSound(bool isCorrect) async { | ||
| 386 | - // await audioPlayer.stop(); | ||
| 387 | - if (audioPlayer.state == PlayerState.playing && | ||
| 388 | - _isResultSoundPlaying == false) { | ||
| 389 | - _voicePlayState = VoicePlayState.stop; | ||
| 390 | - } | ||
| 391 | - debugPrint("_playResultSound isCorrect=$isCorrect"); | ||
| 392 | - _isResultSoundPlaying = true; | ||
| 393 | - _forbiddenWhenCorrect = isCorrect; | ||
| 394 | - if (isCorrect) { | ||
| 395 | - await audioPlayer.play(AssetSource('right'.assetMp3)); | ||
| 396 | - } else { | ||
| 397 | - await audioPlayer.play(AssetSource('wrong'.assetMp3)); | ||
| 398 | - } | 323 | + Future<void> _playResultSound(bool isCorrect) async { |
| 324 | + await ClickWithMusicController.instance.playMusicAndPerformAction(context, | ||
| 325 | + isCorrect ? AudioPlayerUtilType.right : AudioPlayerUtilType.wrong, () { | ||
| 326 | + if (isCorrect) { | ||
| 327 | + if (isLastPage()) { | ||
| 328 | + showStepPage(); | ||
| 329 | + } else { | ||
| 330 | + // 答对后且播放完自动翻页 | ||
| 331 | + pageController.nextPage( | ||
| 332 | + duration: const Duration(milliseconds: 250), | ||
| 333 | + curve: Curves.ease, | ||
| 334 | + ); | ||
| 335 | + } | ||
| 336 | + } | ||
| 337 | + }); | ||
| 399 | } | 338 | } |
| 400 | 339 | ||
| 401 | ///是否是最后一页 | 340 | ///是否是最后一页 |
lib/pages/practice/topic_picture_page.dart
| @@ -173,7 +173,7 @@ class _TopicPicturePage extends StatelessWidget { | @@ -173,7 +173,7 @@ class _TopicPicturePage extends StatelessWidget { | ||
| 173 | buildWhen: (_, s) => s is SelectItemChangeState, | 173 | buildWhen: (_, s) => s is SelectItemChangeState, |
| 174 | builder: (context, state) { | 174 | builder: (context, state) { |
| 175 | final bloc = BlocProvider.of<TopicPictureBloc>(context); | 175 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
| 176 | - final isAnswerOption = bloc.selectItem == index; | 176 | + final isAnswerOption = bloc.optionSelectItem == index; |
| 177 | final answerCorrect = | 177 | final answerCorrect = |
| 178 | isAnswerOption && bloc.checkAnswerRight(index) == true; | 178 | isAnswerOption && bloc.checkAnswerRight(index) == true; |
| 179 | return Container( | 179 | return Container( |
| @@ -243,7 +243,7 @@ class _TopicPicturePage extends StatelessWidget { | @@ -243,7 +243,7 @@ class _TopicPicturePage extends StatelessWidget { | ||
| 243 | buildWhen: (_, s) => s is SelectItemChangeState, | 243 | buildWhen: (_, s) => s is SelectItemChangeState, |
| 244 | builder: (context, state) { | 244 | builder: (context, state) { |
| 245 | final bloc = BlocProvider.of<TopicPictureBloc>(context); | 245 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
| 246 | - final isAnswerOption = bloc.selectItem == index; | 246 | + final isAnswerOption = bloc.optionSelectItem == index; |
| 247 | final answerCorrect = | 247 | final answerCorrect = |
| 248 | isAnswerOption && bloc.checkAnswerRight(index) == true; | 248 | isAnswerOption && bloc.checkAnswerRight(index) == true; |
| 249 | return Container( | 249 | return Container( |
| @@ -349,7 +349,7 @@ class _TopicPicturePage extends StatelessWidget { | @@ -349,7 +349,7 @@ class _TopicPicturePage extends StatelessWidget { | ||
| 349 | buildWhen: (_, s) => s is SelectItemChangeState, | 349 | buildWhen: (_, s) => s is SelectItemChangeState, |
| 350 | builder: (context, state) { | 350 | builder: (context, state) { |
| 351 | final bloc = BlocProvider.of<TopicPictureBloc>(context); | 351 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
| 352 | - final isAnswerOption = bloc.selectItem == index; | 352 | + final isAnswerOption = bloc.optionSelectItem == index; |
| 353 | final answerCorrect = | 353 | final answerCorrect = |
| 354 | isAnswerOption && bloc.checkAnswerRight(index) == true; | 354 | isAnswerOption && bloc.checkAnswerRight(index) == true; |
| 355 | return OptionWidget( | 355 | return OptionWidget( |
| @@ -423,7 +423,7 @@ class _TopicPicturePage extends StatelessWidget { | @@ -423,7 +423,7 @@ class _TopicPicturePage extends StatelessWidget { | ||
| 423 | buildWhen: (_, s) => s is SelectItemChangeState, | 423 | buildWhen: (_, s) => s is SelectItemChangeState, |
| 424 | builder: (context, state) { | 424 | builder: (context, state) { |
| 425 | final bloc = BlocProvider.of<TopicPictureBloc>(context); | 425 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
| 426 | - final isAnswerOption = bloc.selectItem == index; | 426 | + final isAnswerOption = bloc.optionSelectItem == index; |
| 427 | final answerCorrect = | 427 | final answerCorrect = |
| 428 | isAnswerOption && bloc.checkAnswerRight(index) == true; | 428 | isAnswerOption && bloc.checkAnswerRight(index) == true; |
| 429 | return OptionWidget( | 429 | return OptionWidget( |
| @@ -522,8 +522,6 @@ class _TopicPicturePage extends StatelessWidget { | @@ -522,8 +522,6 @@ class _TopicPicturePage extends StatelessWidget { | ||
| 522 | 70.verticalSpace, | 522 | 70.verticalSpace, |
| 523 | RecorderWidget( | 523 | RecorderWidget( |
| 524 | isPlaying: bloc.isRecording, | 524 | isPlaying: bloc.isRecording, |
| 525 | - isClickable: | ||
| 526 | - bloc.voicePlayState != VoicePlayState.playing, | ||
| 527 | width: 72.w, | 525 | width: 72.w, |
| 528 | height: 72.w, | 526 | height: 72.w, |
| 529 | onTap: () { | 527 | onTap: () { |
lib/utils/audio_player_util.dart
| @@ -18,7 +18,9 @@ enum AudioPlayerUtilType { | @@ -18,7 +18,9 @@ enum AudioPlayerUtilType { | ||
| 18 | excellent('excellent'), | 18 | excellent('excellent'), |
| 19 | great('great'), | 19 | great('great'), |
| 20 | good('good'), | 20 | good('good'), |
| 21 | - tryAgain('try_again'); | 21 | + tryAgain('try_again'), |
| 22 | + right('right'), | ||
| 23 | + wrong('wrong'); | ||
| 22 | 24 | ||
| 23 | const AudioPlayerUtilType(this.path); | 25 | const AudioPlayerUtilType(this.path); |
| 24 | 26 |