Commit c623c7b25a82a33deaa820299b3e258bc97f79f8
1 parent
61e3478a
feat:语音跟读作答结果动效&语音
Showing
13 changed files
with
205 additions
and
26 deletions
assets/lotties/excellent.zip
0 → 100644
No preview for this file type
assets/lotties/good.zip
0 → 100644
No preview for this file type
assets/lotties/great.zip
0 → 100644
No preview for this file type
assets/sounds/excellent.mp3
0 → 100644
No preview for this file type
assets/sounds/good.mp3
0 → 100644
No preview for this file type
assets/sounds/great.mp3
0 → 100644
No preview for this file type
assets/sounds/try_again.mp3
0 → 100644
No preview for this file type
lib/common/utils/show_star_reward_dialog.dart
1 | 1 | import 'package:flutter/material.dart'; |
2 | 2 | |
3 | +import '../widgets/cheer_reward_widget.dart'; | |
3 | 4 | import '../widgets/star_reward_widget.dart'; |
4 | 5 | |
5 | 6 | void showStarRewardDialog(BuildContext context, { |
... | ... | @@ -26,4 +27,30 @@ void showStarRewardDialog(BuildContext context, { |
26 | 27 | ); |
27 | 28 | }, |
28 | 29 | ); |
30 | +} | |
31 | + | |
32 | +void showCheerRewardDialog(BuildContext context, { | |
33 | + required String lottieFile, | |
34 | + double width = 200, | |
35 | + double height = 200, | |
36 | +}) { | |
37 | + showDialog( | |
38 | + context: context, | |
39 | + barrierDismissible: false, // 点击对话框外部不关闭对话框 | |
40 | + builder: (BuildContext context) { | |
41 | + return Dialog( | |
42 | + backgroundColor: Colors.transparent, // 设置对话框背景为透明 | |
43 | + insetPadding: const EdgeInsets.all(0), // 去除对话框的内边距 | |
44 | + child: CheerRewardWidget( | |
45 | + lottieFile: lottieFile, | |
46 | + width: width, | |
47 | + height: height, | |
48 | + isPlaying: true, | |
49 | + onAnimationEnd: () { | |
50 | + Navigator.of(context).pop(); // 关闭对话框 | |
51 | + }, | |
52 | + ), | |
53 | + ); | |
54 | + }, | |
55 | + ); | |
29 | 56 | } |
30 | 57 | \ No newline at end of file | ... | ... |
lib/common/widgets/cheer_reward_widget.dart
0 → 100644
1 | +import 'package:flutter/material.dart'; | |
2 | +import 'package:lottie/lottie.dart'; | |
3 | + | |
4 | +import '../../utils/log_util.dart'; | |
5 | + | |
6 | +class CheerRewardWidget extends StatefulWidget { | |
7 | + final String lottieFile; | |
8 | + final double width; | |
9 | + final double height; | |
10 | + final bool isPlaying; | |
11 | + final VoidCallback onAnimationEnd; | |
12 | + | |
13 | + const CheerRewardWidget({ | |
14 | + Key? key, | |
15 | + required this.lottieFile, | |
16 | + required this.width, | |
17 | + required this.height, | |
18 | + required this.isPlaying, | |
19 | + required this.onAnimationEnd, | |
20 | + }) : super(key: key); | |
21 | + | |
22 | + @override | |
23 | + _CheerRewardWidgetState createState() => _CheerRewardWidgetState(); | |
24 | +} | |
25 | + | |
26 | +class _CheerRewardWidgetState extends State<CheerRewardWidget> | |
27 | + with SingleTickerProviderStateMixin { | |
28 | + late final AnimationController _controller; | |
29 | + late final Future<LottieComposition> _futureComposition; | |
30 | + bool _isVisible = false; | |
31 | + static const String TAG = "CheerRewardWidget"; | |
32 | + | |
33 | + @override | |
34 | + void initState() { | |
35 | + super.initState(); | |
36 | + _controller = AnimationController(vsync: this); | |
37 | + _loadComposition(); | |
38 | + } | |
39 | + | |
40 | + @override | |
41 | + void didUpdateWidget(CheerRewardWidget oldWidget) { | |
42 | + super.didUpdateWidget(oldWidget); | |
43 | + if (widget.isPlaying && !_controller.isAnimating) { | |
44 | + _startAnimation(); | |
45 | + } | |
46 | + } | |
47 | + | |
48 | + void _loadComposition() { | |
49 | + // final composition = await AssetLottie('assets/lotties/recorder_input.zip').load(); | |
50 | + // setState(() { | |
51 | + // _composition = composition; | |
52 | + // _controller.duration = _composition.duration; | |
53 | + // }); | |
54 | + | |
55 | + _futureComposition = _loadLottieComposition(); | |
56 | + | |
57 | + if (widget.isPlaying) { | |
58 | + _startAnimation(); | |
59 | + } | |
60 | + } | |
61 | + | |
62 | + void _startAnimation() { | |
63 | + Log.d("$TAG _startAnimation"); | |
64 | + setState(() { | |
65 | + _isVisible = true; | |
66 | + }); | |
67 | + | |
68 | + _futureComposition.then((composition) { | |
69 | + Log.d("$TAG _futureComposition.then duration=${composition.duration}"); | |
70 | + _controller.duration = composition.duration; | |
71 | + _controller.forward().whenCompleteOrCancel(() { | |
72 | + setState(() { | |
73 | + _isVisible = false; | |
74 | + }); | |
75 | + widget.onAnimationEnd(); // 调用外部回调函数 | |
76 | + }); | |
77 | + }); | |
78 | + } | |
79 | + | |
80 | + Future<LottieComposition> _loadLottieComposition() async { | |
81 | + return await AssetLottie(widget.lottieFile).load(); | |
82 | + } | |
83 | + | |
84 | + @override | |
85 | + void dispose() { | |
86 | + _controller.dispose(); | |
87 | + super.dispose(); | |
88 | + } | |
89 | + | |
90 | + @override | |
91 | + Widget build(BuildContext context) { | |
92 | + return Center( | |
93 | + child: SizedBox( | |
94 | + width: widget.width, | |
95 | + height: widget.height, | |
96 | + child: FutureBuilder<LottieComposition>( | |
97 | + future: _futureComposition, | |
98 | + builder: (context, snapshot) { | |
99 | + if (snapshot.hasData) { | |
100 | + final composition = snapshot.data!; | |
101 | + return Lottie( | |
102 | + composition: composition, | |
103 | + controller: _controller, | |
104 | + renderCache: RenderCache.raster, | |
105 | + width: widget.width, | |
106 | + height: widget.height, | |
107 | + ); | |
108 | + } else { | |
109 | + return const SizedBox.shrink(); | |
110 | + } | |
111 | + }) | |
112 | + | |
113 | + // child: Lottie( | |
114 | + // composition: _composition, | |
115 | + // controller: _controller, | |
116 | + // renderCache: RenderCache.raster, | |
117 | + // width: widget.width, | |
118 | + // height: widget.height, | |
119 | + // ), | |
120 | + ), | |
121 | + ); | |
122 | + } | |
123 | +} | ... | ... |
lib/common/widgets/star_reward_widget.dart
... | ... | @@ -68,7 +68,7 @@ class _StarRewardWidgetState extends State<StarRewardWidget> |
68 | 68 | _futureComposition.then((composition) { |
69 | 69 | Log.d("$TAG _futureComposition.then duration=${composition.duration}"); |
70 | 70 | _controller.duration = composition.duration; |
71 | - _controller.forward().whenComplete(() { | |
71 | + _controller.forward().whenCompleteOrCancel(() { | |
72 | 72 | setState(() { |
73 | 73 | _isVisible = false; |
74 | 74 | }); |
... | ... | @@ -93,7 +93,7 @@ class _StarRewardWidgetState extends State<StarRewardWidget> |
93 | 93 | assetPath = 'assets/lotties/star3_reward.zip'; |
94 | 94 | break; |
95 | 95 | } |
96 | - return await AssetLottie('assets/lotties/reward.zip').load(); | |
96 | + return await AssetLottie(assetPath).load(); | |
97 | 97 | } |
98 | 98 | |
99 | 99 | @override | ... | ... |
lib/pages/practice/bloc/topic_picture_bloc.dart
... | ... | @@ -14,11 +14,14 @@ import 'package:wow_english/models/course_process_entity.dart'; |
14 | 14 | import 'package:wow_english/pages/section/subsection/base_section/bloc.dart'; |
15 | 15 | import 'package:wow_english/pages/section/subsection/base_section/event.dart'; |
16 | 16 | import 'package:wow_english/pages/section/subsection/base_section/state.dart'; |
17 | +import 'package:wow_english/utils/audio_player_util.dart'; | |
17 | 18 | import 'package:wow_english/utils/toast_util.dart'; |
18 | 19 | |
19 | 20 | import '../../../common/permission/permissionRequester.dart'; |
21 | +import '../../../common/utils/click_with_music_controller.dart'; | |
20 | 22 | import '../../../common/utils/show_star_reward_dialog.dart'; |
21 | 23 | import '../../../route/route.dart'; |
24 | +import '../../../utils/log_util.dart'; | |
22 | 25 | |
23 | 26 | part 'topic_picture_event.dart'; |
24 | 27 | |
... | ... | @@ -251,7 +254,10 @@ class TopicPictureBloc |
251 | 254 | ///为空则数据异常,用于是否晃动时需要 |
252 | 255 | bool? checkAnswerRight(int selectIndex) { |
253 | 256 | CourseProcessTopics? topics = _entity?.topics?[_currentPage]; |
254 | - if (topics == null || topics.topicAnswerList == null || selectIndex < 0 || selectIndex >= topics.topicAnswerList!.length) { | |
257 | + if (topics == null || | |
258 | + topics.topicAnswerList == null || | |
259 | + selectIndex < 0 || | |
260 | + selectIndex >= topics.topicAnswerList!.length) { | |
255 | 261 | return null; |
256 | 262 | } |
257 | 263 | CourseProcessTopicsTopicAnswerList? answerList = |
... | ... | @@ -300,7 +306,29 @@ class TopicPictureBloc |
300 | 306 | final Map args = event.message as Map; |
301 | 307 | final result = args['result'] as Map; |
302 | 308 | final overall = result['overall'].toString(); |
303 | - showStarRewardDialog(context, starCount: _evaluateScore(overall)); | |
309 | + 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 | + }); | |
304 | 332 | // showToast('测评成功,分数是$overall', duration: const Duration(seconds: 5)); |
305 | 333 | _isRecording = false; |
306 | 334 | emitter(XSVoiceTestState()); |
... | ... | @@ -364,7 +392,7 @@ class TopicPictureBloc |
364 | 392 | _isResultSoundPlaying = true; |
365 | 393 | _forbiddenWhenCorrect = isCorrect; |
366 | 394 | if (isCorrect) { |
367 | - await audioPlayer.play(AssetSource('correct_voice'.assetMp3)); | |
395 | + await audioPlayer.play(AssetSource('right'.assetMp3)); | |
368 | 396 | } else { |
369 | 397 | await audioPlayer.play(AssetSource('wrong'.assetMp3)); |
370 | 398 | } | ... | ... |
lib/pages/reading/reading_page.dart
... | ... | @@ -8,7 +8,7 @@ import 'package:wow_english/route/route.dart'; |
8 | 8 | |
9 | 9 | import '../../common/core/app_consts.dart'; |
10 | 10 | import '../../common/core/user_util.dart'; |
11 | -import '../../common/widgets/throttledGesture_gesture_detector.dart'; | |
11 | +import '../../common/widgets/recorder_widget.dart'; | |
12 | 12 | import '../../models/course_process_entity.dart'; |
13 | 13 | import '../../utils/log_util.dart'; |
14 | 14 | import 'bloc/reading_bloc.dart'; |
... | ... | @@ -186,25 +186,22 @@ class _ReadingPage extends StatelessWidget { |
186 | 186 | SizedBox( |
187 | 187 | width: 10.w, |
188 | 188 | ), |
189 | - ThrottledGestureDetector( | |
190 | - throttleTime: 1000, | |
191 | - onTap: () { | |
192 | - if (bloc.isRecording) { | |
193 | - bloc.add(XSVoiceStopEvent()); | |
194 | - } else { | |
195 | - bloc.add(XSVoiceStartEvent( | |
196 | - bloc.readingContent(), | |
197 | - '0', | |
198 | - UserUtil.getUser()?.id.toString())); | |
199 | - } | |
200 | - }, | |
201 | - child: Image.asset( | |
202 | - bloc.isRecording | |
203 | - ? 'micro_phone'.assetGif | |
204 | - : 'micro_phone'.assetPng, | |
205 | - height: 47.h, | |
206 | - width: 47.w, | |
207 | - )), | |
189 | + RecorderWidget( | |
190 | + isPlaying: bloc.isRecording, | |
191 | + isClickable: bloc.voicePlayState != VoicePlayState.playing, | |
192 | + width: 60.w, | |
193 | + height: 60.w, | |
194 | + onTap: () { | |
195 | + if (bloc.isRecording) { | |
196 | + bloc.add(XSVoiceStopEvent()); | |
197 | + } else { | |
198 | + bloc.add(XSVoiceStartEvent( | |
199 | + bloc.readingContent(), | |
200 | + '0', | |
201 | + UserUtil.getUser()?.id.toString())); | |
202 | + } | |
203 | + }, | |
204 | + ), | |
208 | 205 | SizedBox( |
209 | 206 | width: 10.w, |
210 | 207 | ), | ... | ... |
lib/utils/audio_player_util.dart
... | ... | @@ -14,7 +14,11 @@ enum AudioPlayerUtilType { |
14 | 14 | quizTime('quiz_time'), |
15 | 15 | countWithMe('count_with_me_instrumental'), |
16 | 16 | inMyTummy('in_my_tummy_instrumental'), |
17 | - touch('touch_instrumental'); | |
17 | + touch('touch_instrumental'), | |
18 | + excellent('excellent'), | |
19 | + great('great'), | |
20 | + good('good'), | |
21 | + tryAgain('try_again'); | |
18 | 22 | |
19 | 23 | const AudioPlayerUtilType(this.path); |
20 | 24 | ... | ... |