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 |