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 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
2 | 2 | ||
3 | +import '../widgets/cheer_reward_widget.dart'; | ||
3 | import '../widgets/star_reward_widget.dart'; | 4 | import '../widgets/star_reward_widget.dart'; |
4 | 5 | ||
5 | void showStarRewardDialog(BuildContext context, { | 6 | void showStarRewardDialog(BuildContext context, { |
@@ -26,4 +27,30 @@ 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 | \ No newline at end of file | 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,7 +68,7 @@ class _StarRewardWidgetState extends State<StarRewardWidget> | ||
68 | _futureComposition.then((composition) { | 68 | _futureComposition.then((composition) { |
69 | Log.d("$TAG _futureComposition.then duration=${composition.duration}"); | 69 | Log.d("$TAG _futureComposition.then duration=${composition.duration}"); |
70 | _controller.duration = composition.duration; | 70 | _controller.duration = composition.duration; |
71 | - _controller.forward().whenComplete(() { | 71 | + _controller.forward().whenCompleteOrCancel(() { |
72 | setState(() { | 72 | setState(() { |
73 | _isVisible = false; | 73 | _isVisible = false; |
74 | }); | 74 | }); |
@@ -93,7 +93,7 @@ class _StarRewardWidgetState extends State<StarRewardWidget> | @@ -93,7 +93,7 @@ class _StarRewardWidgetState extends State<StarRewardWidget> | ||
93 | assetPath = 'assets/lotties/star3_reward.zip'; | 93 | assetPath = 'assets/lotties/star3_reward.zip'; |
94 | break; | 94 | break; |
95 | } | 95 | } |
96 | - return await AssetLottie('assets/lotties/reward.zip').load(); | 96 | + return await AssetLottie(assetPath).load(); |
97 | } | 97 | } |
98 | 98 | ||
99 | @override | 99 | @override |
lib/pages/practice/bloc/topic_picture_bloc.dart
@@ -14,11 +14,14 @@ import 'package:wow_english/models/course_process_entity.dart'; | @@ -14,11 +14,14 @@ import 'package:wow_english/models/course_process_entity.dart'; | ||
14 | import 'package:wow_english/pages/section/subsection/base_section/bloc.dart'; | 14 | import 'package:wow_english/pages/section/subsection/base_section/bloc.dart'; |
15 | import 'package:wow_english/pages/section/subsection/base_section/event.dart'; | 15 | import 'package:wow_english/pages/section/subsection/base_section/event.dart'; |
16 | import 'package:wow_english/pages/section/subsection/base_section/state.dart'; | 16 | import 'package:wow_english/pages/section/subsection/base_section/state.dart'; |
17 | +import 'package:wow_english/utils/audio_player_util.dart'; | ||
17 | import 'package:wow_english/utils/toast_util.dart'; | 18 | import 'package:wow_english/utils/toast_util.dart'; |
18 | 19 | ||
19 | import '../../../common/permission/permissionRequester.dart'; | 20 | import '../../../common/permission/permissionRequester.dart'; |
21 | +import '../../../common/utils/click_with_music_controller.dart'; | ||
20 | import '../../../common/utils/show_star_reward_dialog.dart'; | 22 | import '../../../common/utils/show_star_reward_dialog.dart'; |
21 | import '../../../route/route.dart'; | 23 | import '../../../route/route.dart'; |
24 | +import '../../../utils/log_util.dart'; | ||
22 | 25 | ||
23 | part 'topic_picture_event.dart'; | 26 | part 'topic_picture_event.dart'; |
24 | 27 | ||
@@ -251,7 +254,10 @@ class TopicPictureBloc | @@ -251,7 +254,10 @@ class TopicPictureBloc | ||
251 | ///为空则数据异常,用于是否晃动时需要 | 254 | ///为空则数据异常,用于是否晃动时需要 |
252 | bool? checkAnswerRight(int selectIndex) { | 255 | bool? checkAnswerRight(int selectIndex) { |
253 | CourseProcessTopics? topics = _entity?.topics?[_currentPage]; | 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 | return null; | 261 | return null; |
256 | } | 262 | } |
257 | CourseProcessTopicsTopicAnswerList? answerList = | 263 | CourseProcessTopicsTopicAnswerList? answerList = |
@@ -300,7 +306,29 @@ class TopicPictureBloc | @@ -300,7 +306,29 @@ class TopicPictureBloc | ||
300 | final Map args = event.message as Map; | 306 | final Map args = event.message as Map; |
301 | final result = args['result'] as Map; | 307 | final result = args['result'] as Map; |
302 | final overall = result['overall'].toString(); | 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 | // showToast('测评成功,分数是$overall', duration: const Duration(seconds: 5)); | 332 | // showToast('测评成功,分数是$overall', duration: const Duration(seconds: 5)); |
305 | _isRecording = false; | 333 | _isRecording = false; |
306 | emitter(XSVoiceTestState()); | 334 | emitter(XSVoiceTestState()); |
@@ -364,7 +392,7 @@ class TopicPictureBloc | @@ -364,7 +392,7 @@ class TopicPictureBloc | ||
364 | _isResultSoundPlaying = true; | 392 | _isResultSoundPlaying = true; |
365 | _forbiddenWhenCorrect = isCorrect; | 393 | _forbiddenWhenCorrect = isCorrect; |
366 | if (isCorrect) { | 394 | if (isCorrect) { |
367 | - await audioPlayer.play(AssetSource('correct_voice'.assetMp3)); | 395 | + await audioPlayer.play(AssetSource('right'.assetMp3)); |
368 | } else { | 396 | } else { |
369 | await audioPlayer.play(AssetSource('wrong'.assetMp3)); | 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,7 +8,7 @@ import 'package:wow_english/route/route.dart'; | ||
8 | 8 | ||
9 | import '../../common/core/app_consts.dart'; | 9 | import '../../common/core/app_consts.dart'; |
10 | import '../../common/core/user_util.dart'; | 10 | import '../../common/core/user_util.dart'; |
11 | -import '../../common/widgets/throttledGesture_gesture_detector.dart'; | 11 | +import '../../common/widgets/recorder_widget.dart'; |
12 | import '../../models/course_process_entity.dart'; | 12 | import '../../models/course_process_entity.dart'; |
13 | import '../../utils/log_util.dart'; | 13 | import '../../utils/log_util.dart'; |
14 | import 'bloc/reading_bloc.dart'; | 14 | import 'bloc/reading_bloc.dart'; |
@@ -186,25 +186,22 @@ class _ReadingPage extends StatelessWidget { | @@ -186,25 +186,22 @@ class _ReadingPage extends StatelessWidget { | ||
186 | SizedBox( | 186 | SizedBox( |
187 | width: 10.w, | 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 | SizedBox( | 205 | SizedBox( |
209 | width: 10.w, | 206 | width: 10.w, |
210 | ), | 207 | ), |
lib/utils/audio_player_util.dart
@@ -14,7 +14,11 @@ enum AudioPlayerUtilType { | @@ -14,7 +14,11 @@ enum AudioPlayerUtilType { | ||
14 | quizTime('quiz_time'), | 14 | quizTime('quiz_time'), |
15 | countWithMe('count_with_me_instrumental'), | 15 | countWithMe('count_with_me_instrumental'), |
16 | inMyTummy('in_my_tummy_instrumental'), | 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 | const AudioPlayerUtilType(this.path); | 23 | const AudioPlayerUtilType(this.path); |
20 | 24 |