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 |