Commit c623c7b25a82a33deaa820299b3e258bc97f79f8

Authored by 吴启风
1 parent 61e3478a

feat:语音跟读作答结果动效&语音

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&lt;StarRewardWidget&gt;
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&lt;StarRewardWidget&gt;
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 &#39;package:wow_english/models/course_process_entity.dart&#39;;
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 &#39;package:wow_english/route/route.dart&#39;;
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  
... ...