Commit 45c862fdec0ed25a4763590ebfe64a352007dbeb

Authored by 吴启风
1 parent 8a556f76

feat:闯关交互优化-答错晃动、音效替换

assets/sounds/right.mp3 0 → 100644
No preview for this file type
assets/sounds/wrong.mp3 0 → 100644
No preview for this file type
lib/pages/practice/bloc/topic_picture_bloc.dart
  1 +import 'dart:async';
  2 +
1 3 import 'package:audioplayers/audioplayers.dart';
2 4 import 'package:flutter/cupertino.dart';
3 5 import 'package:flutter/foundation.dart';
... ... @@ -196,10 +198,8 @@ class TopicPictureBloc
196 198 void _requestData(
197 199 RequestDataEvent event, Emitter<TopicPictureState> emitter) async {
198 200 try {
199   - await loading(() async {
200   - _entity = await ListenDao.process(courseLessonId);
201   - emitter(RequestDataState());
202   - });
  201 + _entity = await ListenDao.process(courseLessonId);
  202 + emitter(RequestDataState());
203 203 } catch (e) {
204 204 if (e is ApiException) {
205 205 showToast(e.message ?? '请求失败,请检查网络连接');
... ... @@ -237,19 +237,27 @@ class TopicPictureBloc
237 237 return;
238 238 }
239 239 _selectItem = event.selectIndex;
240   - CourseProcessTopics? topics = _entity?.topics?[_currentPage];
241   - CourseProcessTopicsTopicAnswerList? answerList =
242   - topics?.topicAnswerList?[_selectItem];
243   - if (answerList?.correct == 0) {
244   - _playResultSound(false);
245   - // showToast('继续加油哦',duration: const Duration(seconds: 2));
246   - } else {
  240 + if (checkAnswerRight(_selectItem) == true) {
247 241 _playResultSound(true);
248 242 // showToast('恭喜你,答对啦!',duration: const Duration(seconds: 2));
  243 + } else {
  244 + _playResultSound(false);
  245 + // showToast('继续加油哦',duration: const Duration(seconds: 2));
249 246 }
250 247 emitter(SelectItemChangeState());
251 248 }
252 249  
  250 + ///为空则数据异常,用于是否晃动时需要
  251 + bool? checkAnswerRight(int selectIndex) {
  252 + CourseProcessTopics? topics = _entity?.topics?[_currentPage];
  253 + if (topics == null || topics.topicAnswerList == null || selectIndex < 0 || selectIndex >= topics.topicAnswerList!.length) {
  254 + return null;
  255 + }
  256 + CourseProcessTopicsTopicAnswerList? answerList =
  257 + topics.topicAnswerList?[selectIndex];
  258 + return answerList?.correct != 0;
  259 + }
  260 +
253 261 ///初始化SDK
254 262 _initVoiceSdk(
255 263 XSVoiceInitEvent event, Emitter<TopicPictureState> emitter) async {
... ... @@ -338,7 +346,7 @@ class TopicPictureBloc
338 346 if (isCorrect) {
339 347 await audioPlayer.play(AssetSource('correct_voice'.assetMp3));
340 348 } else {
341   - await audioPlayer.play(AssetSource('incorrect_voice'.assetMp3));
  349 + await audioPlayer.play(AssetSource('wrong'.assetMp3));
342 350 }
343 351 }
344 352  
... ...
lib/pages/practice/topic_picture_page.dart
... ... @@ -7,6 +7,7 @@ import &#39;package:wow_english/common/extension/string_extension.dart&#39;;
7 7 import 'package:wow_english/common/widgets/ow_image_widget.dart';
8 8 import 'package:wow_english/models/course_process_entity.dart';
9 9 import 'package:wow_english/pages/practice/topic_type.dart';
  10 +import 'package:wow_english/pages/practice/widgets/shake_widget.dart';
10 11 import 'package:wow_english/route/route.dart';
11 12 import 'package:wow_english/utils/toast_util.dart';
12 13  
... ... @@ -82,39 +83,45 @@ class _TopicPicturePage extends StatelessWidget {
82 83 },
83 84 ),
84 85 35.verticalSpace,
85   - Expanded(
86   - child: PageView.builder(
87   - itemCount: bloc.entity?.topics?.length,
88   - scrollDirection: Axis.horizontal,
89   - controller: bloc.pageController,
90   - onPageChanged: (int index) {
91   - bloc.add(CurrentPageIndexChangeEvent(index));
92   - },
93   - itemBuilder: (BuildContext context, int index) {
94   - CourseProcessTopics? topics =
95   - bloc.entity?.topics![index];
96   - if (topics?.type ==
97   - TopicType.audioImageSelect.value) {
98   - //听音选图
99   - return _pageViewVoicePictureItemWidget(topics);
100   - } else if (topics?.type ==
101   - TopicType.audioCharSelect.value) {
102   - //听音选字
103   - return _pageViewVoiceWordItemWidget(topics);
104   - } else if (topics?.type ==
105   - TopicType.questionCharSelect.value) {
106   - //看题选字
107   - return _pageViewWordItemWidget(topics);
108   - } else if (topics?.type ==
109   - TopicType.questionImageSelect.value) {
110   - //看题选图
111   - return _pageViewItemWidget(topics);
112   - } else {
113   - //语音问答
114   - return _voiceAnswerItem(topics);
115   - }
116   - }),
117   - )
  86 + bloc.entity != null
  87 + ? Expanded(
  88 + child: PageView.builder(
  89 + itemCount: bloc.entity?.topics?.length,
  90 + scrollDirection: Axis.horizontal,
  91 + controller: bloc.pageController,
  92 + onPageChanged: (int index) {
  93 + bloc.add(CurrentPageIndexChangeEvent(index));
  94 + },
  95 + itemBuilder: (BuildContext context, int index) {
  96 + CourseProcessTopics? topics =
  97 + bloc.entity?.topics![index];
  98 + if (topics?.type ==
  99 + TopicType.audioImageSelect.value) {
  100 + //听音选图
  101 + return _pageViewVoicePictureItemWidget(
  102 + topics);
  103 + } else if (topics?.type ==
  104 + TopicType.audioCharSelect.value) {
  105 + //听音选字
  106 + return _pageViewVoiceWordItemWidget(topics);
  107 + } else if (topics?.type ==
  108 + TopicType.questionCharSelect.value) {
  109 + //看题选字
  110 + return _pageViewWordItemWidget(topics);
  111 + } else if (topics?.type ==
  112 + TopicType.questionImageSelect.value) {
  113 + //看题选图
  114 + return _pageViewItemWidget(topics);
  115 + } else {
  116 + //语音问答
  117 + return _voiceAnswerItem(topics);
  118 + }
  119 + }),
  120 + )
  121 + : Container(
  122 + color: Colors.transparent,
  123 + child: const CircularProgressIndicator(),
  124 + ),
118 125 ],
119 126 )
120 127 ],
... ... @@ -157,29 +164,34 @@ class _TopicPicturePage extends StatelessWidget {
157 164 buildWhen: (_, s) => s is SelectItemChangeState,
158 165 builder: (context, state) {
159 166 final bloc = BlocProvider.of<TopicPictureBloc>(context);
  167 + final shouldShake = (bloc.selectItem == index &&
  168 + bloc.checkAnswerRight(index) == false);
160 169 return Container(
161 170 padding: EdgeInsets.symmetric(horizontal: 10.w),
162 171 child: GestureDetector(
163 172 onTap: () => bloc.add(SelectItemEvent(index)),
164   - child: Container(
165   - padding: const EdgeInsets.all(4.5),
166   - decoration: BoxDecoration(
167   - color: bloc.selectItem == index
168   - ? const Color(0xFF00B6F1)
169   - : Colors.white,
170   - borderRadius: BorderRadius.circular(15),
171   - ),
172   - height: 143.h,
173   - width: 143.w,
  173 + child: ShakeWidget(
  174 + shouldShake: shouldShake,
174 175 child: Container(
  176 + padding: const EdgeInsets.all(4.5),
175 177 decoration: BoxDecoration(
176   - color: Colors.white,
177   - borderRadius: BorderRadius.circular(15),
178   - border: Border.all(
179   - width: 1.0, color: const Color(0xFF140C10)),
180   - image: DecorationImage(
181   - fit: BoxFit.fitWidth,
182   - image: NetworkImage(answerLis?.picUrl ?? ''))),
  178 + color: bloc.selectItem == index
  179 + ? const Color(0xFF00B6F1)
  180 + : Colors.white,
  181 + borderRadius: BorderRadius.circular(15),
  182 + ),
  183 + height: 143.h,
  184 + width: 143.w,
  185 + child: Container(
  186 + decoration: BoxDecoration(
  187 + color: Colors.white,
  188 + borderRadius: BorderRadius.circular(15),
  189 + border: Border.all(
  190 + width: 1.0, color: const Color(0xFF140C10)),
  191 + image: DecorationImage(
  192 + fit: BoxFit.fitWidth,
  193 + image: NetworkImage(answerLis?.picUrl ?? ''))),
  194 + ),
183 195 ),
184 196 ),
185 197 ),
... ... @@ -221,48 +233,53 @@ class _TopicPicturePage extends StatelessWidget {
221 233 buildWhen: (_, s) => s is SelectItemChangeState,
222 234 builder: (context, state) {
223 235 final bloc = BlocProvider.of<TopicPictureBloc>(context);
  236 + final shouldShake = (bloc.selectItem == index &&
  237 + bloc.checkAnswerRight(index) == false);
224 238 return Container(
225 239 padding: EdgeInsets.symmetric(horizontal: 10.w),
226 240 child: GestureDetector(
227 241 onTap: () => bloc.add(SelectItemEvent(index)),
228   - child: Container(
229   - width: 143.w,
230   - height: 143.h,
231   - padding: EdgeInsets.only(
232   - left: 13.w, right: 13.w, top: 13.h, bottom: 13.h),
233   - decoration: BoxDecoration(
234   - color: Colors.white,
235   - borderRadius: BorderRadius.circular(15),
236   - border:
237   - Border.all(width: 1.0, color: const Color(0xFF140C10)),
238   - ),
239   - child: Column(
240   - mainAxisAlignment: MainAxisAlignment.end,
241   - children: [
242   - Expanded(
243   - child: Container(
244   - alignment: Alignment.center,
245   - child: Text(answerLis?.word ?? '',
246   - style: TextStyle(
247   - fontSize: 20.sp,
248   - color: const Color(0xFF333333))),
249   - ),
250   - ),
251   - Container(
252   - height: 30.h,
253   - width: double.infinity,
254   - decoration: BoxDecoration(
255   - color: bloc.selectItem == index
256   - ? const Color(0xFF00B6F1)
257   - : Colors.white,
258   - borderRadius: BorderRadius.circular(15.r),
259   - border: Border.all(
260   - width: 1.5, color: const Color(0xFF140C10)),
  242 + child: ShakeWidget(
  243 + shouldShake: shouldShake,
  244 + child: Container(
  245 + width: 143.w,
  246 + height: 143.h,
  247 + padding: EdgeInsets.only(
  248 + left: 13.w, right: 13.w, top: 13.h, bottom: 13.h),
  249 + decoration: BoxDecoration(
  250 + color: Colors.white,
  251 + borderRadius: BorderRadius.circular(15),
  252 + border: Border.all(
  253 + width: 1.0, color: const Color(0xFF140C10)),
  254 + ),
  255 + child: Column(
  256 + mainAxisAlignment: MainAxisAlignment.end,
  257 + children: [
  258 + Expanded(
  259 + child: Container(
  260 + alignment: Alignment.center,
  261 + child: Text(answerLis?.word ?? '',
  262 + style: TextStyle(
  263 + fontSize: 20.sp,
  264 + color: const Color(0xFF333333))),
  265 + ),
261 266 ),
262   - alignment: Alignment.center,
263   - child: Image.asset('choose'.assetPng),
264   - )
265   - ],
  267 + Container(
  268 + height: 30.h,
  269 + width: double.infinity,
  270 + decoration: BoxDecoration(
  271 + color: bloc.selectItem == index
  272 + ? const Color(0xFF00B6F1)
  273 + : Colors.white,
  274 + borderRadius: BorderRadius.circular(15.r),
  275 + border: Border.all(
  276 + width: 1.5, color: const Color(0xFF140C10)),
  277 + ),
  278 + alignment: Alignment.center,
  279 + child: Image.asset('choose'.assetPng),
  280 + )
  281 + ],
  282 + ),
266 283 ),
267 284 ),
268 285 ),
... ... @@ -322,27 +339,32 @@ class _TopicPicturePage extends StatelessWidget {
322 339 buildWhen: (_, s) => s is SelectItemChangeState,
323 340 builder: (context, state) {
324 341 final bloc = BlocProvider.of<TopicPictureBloc>(context);
325   - return Container(
326   - padding: EdgeInsets.symmetric(horizontal: 20.w),
327   - child: GestureDetector(
328   - onTap: () => bloc.add(SelectItemEvent(index)),
329   - child: Container(
330   - padding: const EdgeInsets.all(4.5),
331   - decoration: BoxDecoration(
332   - color: bloc.selectItem == index
333   - ? const Color(0xFF00B6F1)
334   - : Colors.white,
335   - borderRadius: BorderRadius.circular(15),
336   - ),
337   - height: 143.h,
338   - width: 163.w,
  342 + final shouldShake = (bloc.selectItem == index &&
  343 + bloc.checkAnswerRight(index) == false);
  344 + return ShakeWidget(
  345 + shouldShake: shouldShake,
  346 + child: Container(
  347 + padding: EdgeInsets.symmetric(horizontal: 20.w),
  348 + child: GestureDetector(
  349 + onTap: () => bloc.add(SelectItemEvent(index)),
339 350 child: Container(
  351 + padding: const EdgeInsets.all(4.5),
340 352 decoration: BoxDecoration(
341   - color: Colors.white,
342   - borderRadius: BorderRadius.circular(15),
343   - image: DecorationImage(
344   - fit: BoxFit.fill,
345   - image: NetworkImage(answerList?.picUrl ?? ''))),
  353 + color: bloc.selectItem == index
  354 + ? const Color(0xFF00B6F1)
  355 + : Colors.white,
  356 + borderRadius: BorderRadius.circular(15),
  357 + ),
  358 + height: 143.h,
  359 + width: 163.w,
  360 + child: Container(
  361 + decoration: BoxDecoration(
  362 + color: Colors.white,
  363 + borderRadius: BorderRadius.circular(15),
  364 + image: DecorationImage(
  365 + fit: BoxFit.fill,
  366 + image: NetworkImage(answerList?.picUrl ?? ''))),
  367 + ),
346 368 ),
347 369 ),
348 370 ),
... ... @@ -391,50 +413,55 @@ class _TopicPicturePage extends StatelessWidget {
391 413 buildWhen: (_, s) => s is SelectItemChangeState,
392 414 builder: (context, state) {
393 415 final bloc = BlocProvider.of<TopicPictureBloc>(context);
  416 + final shouldShake = (bloc.selectItem == index &&
  417 + bloc.checkAnswerRight(index) == false);
394 418 return GestureDetector(
395 419 onTap: () => bloc.add(SelectItemEvent(index)),
396   - child: Container(
397   - width: 163.w,
398   - height: 143.h,
399   - padding: EdgeInsets.symmetric(horizontal: 10.w),
  420 + child: ShakeWidget(
  421 + shouldShake: shouldShake,
400 422 child: Container(
401   - width: 143.w,
  423 + width: 163.w,
402 424 height: 143.h,
403   - padding: EdgeInsets.only(
404   - left: 13.w, right: 13.w, top: 13.h, bottom: 13.h),
405   - decoration: BoxDecoration(
406   - color: Colors.white,
407   - borderRadius: BorderRadius.circular(15),
408   - border:
409   - Border.all(width: 1.0, color: const Color(0xFF140C10)),
410   - ),
411   - child: Column(
412   - mainAxisAlignment: MainAxisAlignment.end,
413   - children: [
414   - Expanded(
415   - child: Container(
416   - alignment: Alignment.center,
417   - child: Text(answerList.word ?? '',
418   - style: TextStyle(
419   - fontSize: 20.sp,
420   - color: const Color(0xFF333333))),
421   - ),
422   - ),
423   - Container(
424   - height: 30.h,
425   - width: double.infinity,
426   - decoration: BoxDecoration(
427   - color: bloc.selectItem == index
428   - ? const Color(0xFF00B6F1)
429   - : Colors.white,
430   - borderRadius: BorderRadius.circular(15.r),
431   - border: Border.all(
432   - width: 1.5, color: const Color(0xFF140C10)),
  425 + padding: EdgeInsets.symmetric(horizontal: 10.w),
  426 + child: Container(
  427 + width: 143.w,
  428 + height: 143.h,
  429 + padding: EdgeInsets.only(
  430 + left: 13.w, right: 13.w, top: 13.h, bottom: 13.h),
  431 + decoration: BoxDecoration(
  432 + color: Colors.white,
  433 + borderRadius: BorderRadius.circular(15),
  434 + border: Border.all(
  435 + width: 1.0, color: const Color(0xFF140C10)),
  436 + ),
  437 + child: Column(
  438 + mainAxisAlignment: MainAxisAlignment.end,
  439 + children: [
  440 + Expanded(
  441 + child: Container(
  442 + alignment: Alignment.center,
  443 + child: Text(answerList.word ?? '',
  444 + style: TextStyle(
  445 + fontSize: 20.sp,
  446 + color: const Color(0xFF333333))),
  447 + ),
433 448 ),
434   - alignment: Alignment.center,
435   - child: Image.asset('choose'.assetPng),
436   - )
437   - ],
  449 + Container(
  450 + height: 30.h,
  451 + width: double.infinity,
  452 + decoration: BoxDecoration(
  453 + color: bloc.selectItem == index
  454 + ? const Color(0xFF00B6F1)
  455 + : Colors.white,
  456 + borderRadius: BorderRadius.circular(15.r),
  457 + border: Border.all(
  458 + width: 1.5, color: const Color(0xFF140C10)),
  459 + ),
  460 + alignment: Alignment.center,
  461 + child: Image.asset('choose'.assetPng),
  462 + )
  463 + ],
  464 + ),
438 465 ),
439 466 ),
440 467 ),
... ...
lib/pages/practice/widgets/shake_widget.dart 0 → 100644
  1 +import 'package:flutter/material.dart';
  2 +
  3 +import '../../../utils/log_util.dart';
  4 +
  5 +class ShakeWidget extends StatefulWidget {
  6 + final Widget child;
  7 + final int shakeCount;
  8 + final double shakeOffset;
  9 + final Duration duration;
  10 + final bool shouldShake;
  11 +
  12 + const ShakeWidget({
  13 + Key? key,
  14 + required this.child,
  15 + this.shakeCount = 2,
  16 + this.shakeOffset = 10.0,
  17 + this.duration = const Duration(milliseconds: 500),
  18 + this.shouldShake = false,
  19 + }) : super(key: key);
  20 +
  21 + @override
  22 + _ShakeWidgetState createState() => _ShakeWidgetState();
  23 +}
  24 +
  25 +class _ShakeWidgetState extends State<ShakeWidget> with SingleTickerProviderStateMixin {
  26 + late AnimationController _controller;
  27 + late Animation<double> _animation;
  28 +
  29 + @override
  30 + void initState() {
  31 + super.initState();
  32 + _controller = AnimationController(
  33 + vsync: this,
  34 + duration: widget.duration,
  35 + );
  36 +
  37 + _animation = TweenSequence([
  38 + TweenSequenceItem(
  39 + tween: Tween(begin: 0.0, end: widget.shakeOffset)
  40 + .chain(CurveTween(curve: Curves.easeInOut)),
  41 + weight: 1,
  42 + ),
  43 + TweenSequenceItem(
  44 + tween: Tween(begin: widget.shakeOffset, end: -widget.shakeOffset)
  45 + .chain(CurveTween(curve: Curves.easeInOut)),
  46 + weight: 1,
  47 + ),
  48 + TweenSequenceItem(
  49 + tween: Tween(begin: -widget.shakeOffset, end: widget.shakeOffset)
  50 + .chain(CurveTween(curve: Curves.easeInOut)),
  51 + weight: 1,
  52 + ),
  53 + TweenSequenceItem(
  54 + tween: Tween(begin: widget.shakeOffset, end: -widget.shakeOffset)
  55 + .chain(CurveTween(curve: Curves.easeInOut)),
  56 + weight: 1,
  57 + ),
  58 + TweenSequenceItem(
  59 + tween: Tween(begin: -widget.shakeOffset, end: 0.0)
  60 + .chain(CurveTween(curve: Curves.easeInOut)),
  61 + weight: 1,
  62 + ),
  63 + ]).animate(_controller);
  64 + }
  65 +
  66 + @override
  67 + void didUpdateWidget(covariant ShakeWidget oldWidget) {
  68 + super.didUpdateWidget(oldWidget);
  69 + Log.d("WQF didUpdateWidget widget.shouldShake=${widget.shouldShake} oldWidget.shouldShake=${oldWidget.shouldShake}");
  70 + // if (widget.shouldShake && !oldWidget.shouldShake) {
  71 + if (widget.shouldShake) {
  72 + _controller.forward(from: 0.0);
  73 + }
  74 + }
  75 +
  76 + @override
  77 + void dispose() {
  78 + _controller.dispose();
  79 + super.dispose();
  80 + }
  81 +
  82 + @override
  83 + Widget build(BuildContext context) {
  84 + return AnimatedBuilder(
  85 + animation: _animation,
  86 + builder: (context, child) {
  87 + return Transform.translate(
  88 + offset: Offset(_animation.value, 0),
  89 + child: widget.child,
  90 + );
  91 + },
  92 + child: widget.child,
  93 + );
  94 + }
  95 +}
0 96 \ No newline at end of file
... ...