From 45c862fdec0ed25a4763590ebfe64a352007dbeb Mon Sep 17 00:00:00 2001 From: wuqifeng <540416539@qq.com> Date: Mon, 29 Jul 2024 02:09:54 +0800 Subject: [PATCH] feat:闯关交互优化-答错晃动、音效替换 --- assets/sounds/right.mp3 | Bin 0 -> 20552 bytes assets/sounds/wrong.mp3 | Bin 0 -> 7881 bytes lib/pages/practice/bloc/topic_picture_bloc.dart | 32 ++++++++++++++++++++------------ lib/pages/practice/topic_picture_page.dart | 317 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------- lib/pages/practice/widgets/shake_widget.dart | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 287 insertions(+), 157 deletions(-) create mode 100644 assets/sounds/right.mp3 create mode 100644 assets/sounds/wrong.mp3 create mode 100644 lib/pages/practice/widgets/shake_widget.dart diff --git a/assets/sounds/right.mp3 b/assets/sounds/right.mp3 new file mode 100644 index 0000000..1734ba9 Binary files /dev/null and b/assets/sounds/right.mp3 differ diff --git a/assets/sounds/wrong.mp3 b/assets/sounds/wrong.mp3 new file mode 100644 index 0000000..24be37f Binary files /dev/null and b/assets/sounds/wrong.mp3 differ diff --git a/lib/pages/practice/bloc/topic_picture_bloc.dart b/lib/pages/practice/bloc/topic_picture_bloc.dart index 113d87b..4acb23e 100644 --- a/lib/pages/practice/bloc/topic_picture_bloc.dart +++ b/lib/pages/practice/bloc/topic_picture_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -196,10 +198,8 @@ class TopicPictureBloc void _requestData( RequestDataEvent event, Emitter emitter) async { try { - await loading(() async { - _entity = await ListenDao.process(courseLessonId); - emitter(RequestDataState()); - }); + _entity = await ListenDao.process(courseLessonId); + emitter(RequestDataState()); } catch (e) { if (e is ApiException) { showToast(e.message ?? '请求失败,请检查网络连接'); @@ -237,19 +237,27 @@ class TopicPictureBloc return; } _selectItem = event.selectIndex; - CourseProcessTopics? topics = _entity?.topics?[_currentPage]; - CourseProcessTopicsTopicAnswerList? answerList = - topics?.topicAnswerList?[_selectItem]; - if (answerList?.correct == 0) { - _playResultSound(false); - // showToast('继续加油哦',duration: const Duration(seconds: 2)); - } else { + if (checkAnswerRight(_selectItem) == true) { _playResultSound(true); // showToast('恭喜你,答对啦!',duration: const Duration(seconds: 2)); + } else { + _playResultSound(false); + // showToast('继续加油哦',duration: const Duration(seconds: 2)); } emitter(SelectItemChangeState()); } + ///为空则数据异常,用于是否晃动时需要 + bool? checkAnswerRight(int selectIndex) { + CourseProcessTopics? topics = _entity?.topics?[_currentPage]; + if (topics == null || topics.topicAnswerList == null || selectIndex < 0 || selectIndex >= topics.topicAnswerList!.length) { + return null; + } + CourseProcessTopicsTopicAnswerList? answerList = + topics.topicAnswerList?[selectIndex]; + return answerList?.correct != 0; + } + ///初始化SDK _initVoiceSdk( XSVoiceInitEvent event, Emitter emitter) async { @@ -338,7 +346,7 @@ class TopicPictureBloc if (isCorrect) { await audioPlayer.play(AssetSource('correct_voice'.assetMp3)); } else { - await audioPlayer.play(AssetSource('incorrect_voice'.assetMp3)); + await audioPlayer.play(AssetSource('wrong'.assetMp3)); } } diff --git a/lib/pages/practice/topic_picture_page.dart b/lib/pages/practice/topic_picture_page.dart index af63342..6873c6a 100644 --- a/lib/pages/practice/topic_picture_page.dart +++ b/lib/pages/practice/topic_picture_page.dart @@ -7,6 +7,7 @@ import 'package:wow_english/common/extension/string_extension.dart'; import 'package:wow_english/common/widgets/ow_image_widget.dart'; import 'package:wow_english/models/course_process_entity.dart'; import 'package:wow_english/pages/practice/topic_type.dart'; +import 'package:wow_english/pages/practice/widgets/shake_widget.dart'; import 'package:wow_english/route/route.dart'; import 'package:wow_english/utils/toast_util.dart'; @@ -82,39 +83,45 @@ class _TopicPicturePage extends StatelessWidget { }, ), 35.verticalSpace, - Expanded( - child: PageView.builder( - itemCount: bloc.entity?.topics?.length, - scrollDirection: Axis.horizontal, - controller: bloc.pageController, - onPageChanged: (int index) { - bloc.add(CurrentPageIndexChangeEvent(index)); - }, - itemBuilder: (BuildContext context, int index) { - CourseProcessTopics? topics = - bloc.entity?.topics![index]; - if (topics?.type == - TopicType.audioImageSelect.value) { - //听音选图 - return _pageViewVoicePictureItemWidget(topics); - } else if (topics?.type == - TopicType.audioCharSelect.value) { - //听音选字 - return _pageViewVoiceWordItemWidget(topics); - } else if (topics?.type == - TopicType.questionCharSelect.value) { - //看题选字 - return _pageViewWordItemWidget(topics); - } else if (topics?.type == - TopicType.questionImageSelect.value) { - //看题选图 - return _pageViewItemWidget(topics); - } else { - //语音问答 - return _voiceAnswerItem(topics); - } - }), - ) + bloc.entity != null + ? Expanded( + child: PageView.builder( + itemCount: bloc.entity?.topics?.length, + scrollDirection: Axis.horizontal, + controller: bloc.pageController, + onPageChanged: (int index) { + bloc.add(CurrentPageIndexChangeEvent(index)); + }, + itemBuilder: (BuildContext context, int index) { + CourseProcessTopics? topics = + bloc.entity?.topics![index]; + if (topics?.type == + TopicType.audioImageSelect.value) { + //听音选图 + return _pageViewVoicePictureItemWidget( + topics); + } else if (topics?.type == + TopicType.audioCharSelect.value) { + //听音选字 + return _pageViewVoiceWordItemWidget(topics); + } else if (topics?.type == + TopicType.questionCharSelect.value) { + //看题选字 + return _pageViewWordItemWidget(topics); + } else if (topics?.type == + TopicType.questionImageSelect.value) { + //看题选图 + return _pageViewItemWidget(topics); + } else { + //语音问答 + return _voiceAnswerItem(topics); + } + }), + ) + : Container( + color: Colors.transparent, + child: const CircularProgressIndicator(), + ), ], ) ], @@ -157,29 +164,34 @@ class _TopicPicturePage extends StatelessWidget { buildWhen: (_, s) => s is SelectItemChangeState, builder: (context, state) { final bloc = BlocProvider.of(context); + final shouldShake = (bloc.selectItem == index && + bloc.checkAnswerRight(index) == false); return Container( padding: EdgeInsets.symmetric(horizontal: 10.w), child: GestureDetector( onTap: () => bloc.add(SelectItemEvent(index)), - child: Container( - padding: const EdgeInsets.all(4.5), - decoration: BoxDecoration( - color: bloc.selectItem == index - ? const Color(0xFF00B6F1) - : Colors.white, - borderRadius: BorderRadius.circular(15), - ), - height: 143.h, - width: 143.w, + child: ShakeWidget( + shouldShake: shouldShake, child: Container( + padding: const EdgeInsets.all(4.5), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(15), - border: Border.all( - width: 1.0, color: const Color(0xFF140C10)), - image: DecorationImage( - fit: BoxFit.fitWidth, - image: NetworkImage(answerLis?.picUrl ?? ''))), + color: bloc.selectItem == index + ? const Color(0xFF00B6F1) + : Colors.white, + borderRadius: BorderRadius.circular(15), + ), + height: 143.h, + width: 143.w, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + border: Border.all( + width: 1.0, color: const Color(0xFF140C10)), + image: DecorationImage( + fit: BoxFit.fitWidth, + image: NetworkImage(answerLis?.picUrl ?? ''))), + ), ), ), ), @@ -221,48 +233,53 @@ class _TopicPicturePage extends StatelessWidget { buildWhen: (_, s) => s is SelectItemChangeState, builder: (context, state) { final bloc = BlocProvider.of(context); + final shouldShake = (bloc.selectItem == index && + bloc.checkAnswerRight(index) == false); return Container( padding: EdgeInsets.symmetric(horizontal: 10.w), child: GestureDetector( onTap: () => bloc.add(SelectItemEvent(index)), - child: Container( - width: 143.w, - height: 143.h, - padding: EdgeInsets.only( - left: 13.w, right: 13.w, top: 13.h, bottom: 13.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(15), - border: - Border.all(width: 1.0, color: const Color(0xFF140C10)), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded( - child: Container( - alignment: Alignment.center, - child: Text(answerLis?.word ?? '', - style: TextStyle( - fontSize: 20.sp, - color: const Color(0xFF333333))), - ), - ), - Container( - height: 30.h, - width: double.infinity, - decoration: BoxDecoration( - color: bloc.selectItem == index - ? const Color(0xFF00B6F1) - : Colors.white, - borderRadius: BorderRadius.circular(15.r), - border: Border.all( - width: 1.5, color: const Color(0xFF140C10)), + child: ShakeWidget( + shouldShake: shouldShake, + child: Container( + width: 143.w, + height: 143.h, + padding: EdgeInsets.only( + left: 13.w, right: 13.w, top: 13.h, bottom: 13.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + border: Border.all( + width: 1.0, color: const Color(0xFF140C10)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded( + child: Container( + alignment: Alignment.center, + child: Text(answerLis?.word ?? '', + style: TextStyle( + fontSize: 20.sp, + color: const Color(0xFF333333))), + ), ), - alignment: Alignment.center, - child: Image.asset('choose'.assetPng), - ) - ], + Container( + height: 30.h, + width: double.infinity, + decoration: BoxDecoration( + color: bloc.selectItem == index + ? const Color(0xFF00B6F1) + : Colors.white, + borderRadius: BorderRadius.circular(15.r), + border: Border.all( + width: 1.5, color: const Color(0xFF140C10)), + ), + alignment: Alignment.center, + child: Image.asset('choose'.assetPng), + ) + ], + ), ), ), ), @@ -322,27 +339,32 @@ class _TopicPicturePage extends StatelessWidget { buildWhen: (_, s) => s is SelectItemChangeState, builder: (context, state) { final bloc = BlocProvider.of(context); - return Container( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: GestureDetector( - onTap: () => bloc.add(SelectItemEvent(index)), - child: Container( - padding: const EdgeInsets.all(4.5), - decoration: BoxDecoration( - color: bloc.selectItem == index - ? const Color(0xFF00B6F1) - : Colors.white, - borderRadius: BorderRadius.circular(15), - ), - height: 143.h, - width: 163.w, + final shouldShake = (bloc.selectItem == index && + bloc.checkAnswerRight(index) == false); + return ShakeWidget( + shouldShake: shouldShake, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: GestureDetector( + onTap: () => bloc.add(SelectItemEvent(index)), child: Container( + padding: const EdgeInsets.all(4.5), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(15), - image: DecorationImage( - fit: BoxFit.fill, - image: NetworkImage(answerList?.picUrl ?? ''))), + color: bloc.selectItem == index + ? const Color(0xFF00B6F1) + : Colors.white, + borderRadius: BorderRadius.circular(15), + ), + height: 143.h, + width: 163.w, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + image: DecorationImage( + fit: BoxFit.fill, + image: NetworkImage(answerList?.picUrl ?? ''))), + ), ), ), ), @@ -391,50 +413,55 @@ class _TopicPicturePage extends StatelessWidget { buildWhen: (_, s) => s is SelectItemChangeState, builder: (context, state) { final bloc = BlocProvider.of(context); + final shouldShake = (bloc.selectItem == index && + bloc.checkAnswerRight(index) == false); return GestureDetector( onTap: () => bloc.add(SelectItemEvent(index)), - child: Container( - width: 163.w, - height: 143.h, - padding: EdgeInsets.symmetric(horizontal: 10.w), + child: ShakeWidget( + shouldShake: shouldShake, child: Container( - width: 143.w, + width: 163.w, height: 143.h, - padding: EdgeInsets.only( - left: 13.w, right: 13.w, top: 13.h, bottom: 13.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(15), - border: - Border.all(width: 1.0, color: const Color(0xFF140C10)), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded( - child: Container( - alignment: Alignment.center, - child: Text(answerList.word ?? '', - style: TextStyle( - fontSize: 20.sp, - color: const Color(0xFF333333))), - ), - ), - Container( - height: 30.h, - width: double.infinity, - decoration: BoxDecoration( - color: bloc.selectItem == index - ? const Color(0xFF00B6F1) - : Colors.white, - borderRadius: BorderRadius.circular(15.r), - border: Border.all( - width: 1.5, color: const Color(0xFF140C10)), + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Container( + width: 143.w, + height: 143.h, + padding: EdgeInsets.only( + left: 13.w, right: 13.w, top: 13.h, bottom: 13.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + border: Border.all( + width: 1.0, color: const Color(0xFF140C10)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded( + child: Container( + alignment: Alignment.center, + child: Text(answerList.word ?? '', + style: TextStyle( + fontSize: 20.sp, + color: const Color(0xFF333333))), + ), ), - alignment: Alignment.center, - child: Image.asset('choose'.assetPng), - ) - ], + Container( + height: 30.h, + width: double.infinity, + decoration: BoxDecoration( + color: bloc.selectItem == index + ? const Color(0xFF00B6F1) + : Colors.white, + borderRadius: BorderRadius.circular(15.r), + border: Border.all( + width: 1.5, color: const Color(0xFF140C10)), + ), + alignment: Alignment.center, + child: Image.asset('choose'.assetPng), + ) + ], + ), ), ), ), diff --git a/lib/pages/practice/widgets/shake_widget.dart b/lib/pages/practice/widgets/shake_widget.dart new file mode 100644 index 0000000..7336fd1 --- /dev/null +++ b/lib/pages/practice/widgets/shake_widget.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/log_util.dart'; + +class ShakeWidget extends StatefulWidget { + final Widget child; + final int shakeCount; + final double shakeOffset; + final Duration duration; + final bool shouldShake; + + const ShakeWidget({ + Key? key, + required this.child, + this.shakeCount = 2, + this.shakeOffset = 10.0, + this.duration = const Duration(milliseconds: 500), + this.shouldShake = false, + }) : super(key: key); + + @override + _ShakeWidgetState createState() => _ShakeWidgetState(); +} + +class _ShakeWidgetState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: widget.duration, + ); + + _animation = TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 0.0, end: widget.shakeOffset) + .chain(CurveTween(curve: Curves.easeInOut)), + weight: 1, + ), + TweenSequenceItem( + tween: Tween(begin: widget.shakeOffset, end: -widget.shakeOffset) + .chain(CurveTween(curve: Curves.easeInOut)), + weight: 1, + ), + TweenSequenceItem( + tween: Tween(begin: -widget.shakeOffset, end: widget.shakeOffset) + .chain(CurveTween(curve: Curves.easeInOut)), + weight: 1, + ), + TweenSequenceItem( + tween: Tween(begin: widget.shakeOffset, end: -widget.shakeOffset) + .chain(CurveTween(curve: Curves.easeInOut)), + weight: 1, + ), + TweenSequenceItem( + tween: Tween(begin: -widget.shakeOffset, end: 0.0) + .chain(CurveTween(curve: Curves.easeInOut)), + weight: 1, + ), + ]).animate(_controller); + } + + @override + void didUpdateWidget(covariant ShakeWidget oldWidget) { + super.didUpdateWidget(oldWidget); + Log.d("WQF didUpdateWidget widget.shouldShake=${widget.shouldShake} oldWidget.shouldShake=${oldWidget.shouldShake}"); + // if (widget.shouldShake && !oldWidget.shouldShake) { + if (widget.shouldShake) { + _controller.forward(from: 0.0); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Transform.translate( + offset: Offset(_animation.value, 0), + child: widget.child, + ); + }, + child: widget.child, + ); + } +} \ No newline at end of file -- libgit2 0.22.2