diff --git a/lib/core/routers/hero_dialog_route.dart b/lib/core/routers/hero_dialog_route.dart index 7b53b6b..54a199f 100644 --- a/lib/core/routers/hero_dialog_route.dart +++ b/lib/core/routers/hero_dialog_route.dart @@ -16,7 +16,7 @@ class HeroDialogRoute extends PageRoute { bool get fullscreenDialog => false; @override - bool get barrierDismissible => true; + bool get barrierDismissible => false; @override Duration get transitionDuration => const Duration(seconds: 1); // Adjust as needed diff --git a/lib/core/widgets/animations/fade_anim_controller.dart b/lib/core/widgets/animations/fade_anim_controller.dart new file mode 100644 index 0000000..2965c32 --- /dev/null +++ b/lib/core/widgets/animations/fade_anim_controller.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class FadeAnimController extends StatefulWidget { + const FadeAnimController({ + super.key, + required this.child, + required this.controller, + }); + + final Widget child; + final AnimationController controller; + + @override + State createState() => _FadeAnimControllerState(); +} + +class _FadeAnimControllerState extends State + with SingleTickerProviderStateMixin { + late Animation _animation; + + @override + void initState() { + super.initState(); + _animation = Tween( + begin: 0, + end: 1, + ).animate(CurvedAnimation(parent: widget.controller, curve: Curves.linear)); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: widget.controller, + child: widget.child, + builder: (context, child) => + FadeTransition(opacity: _animation, child: child), + ); + } +} diff --git a/lib/core/widgets/answer_box/answer_box.dart b/lib/core/widgets/answer_box/answer_box.dart index b01d027..0e85ce9 100644 --- a/lib/core/widgets/answer_box/answer_box.dart +++ b/lib/core/widgets/answer_box/answer_box.dart @@ -53,8 +53,7 @@ class _AnswerBoxState extends State { } : null, child: Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, + alignment: Alignment.topCenter, children: [ AnswerPictureBox( selected: selected, @@ -68,7 +67,7 @@ class _AnswerBoxState extends State { Positioned( left: 0, right: 0, - bottom: -60, + bottom: 0, child: AnswerTextBox(text: widget.answer.title ?? ''), ), PositionedDirectional( diff --git a/lib/core/widgets/answer_box/answer_box_show.dart b/lib/core/widgets/answer_box/answer_box_show.dart index 726c799..b09f4c5 100644 --- a/lib/core/widgets/answer_box/answer_box_show.dart +++ b/lib/core/widgets/answer_box/answer_box_show.dart @@ -26,7 +26,7 @@ class AnswerBoxShow extends StatelessWidget { child: Material( type: MaterialType.transparency, child: Stack( - alignment: Alignment.center, + alignment: Alignment.topCenter, clipBehavior: Clip.none, children: [ AnswerPictureBox( @@ -38,7 +38,7 @@ class AnswerBoxShow extends StatelessWidget { Positioned( left: 0, right: 0, - bottom: -MySpaces.s40, + bottom: -10, child: AnswerTextBox(text: answer.title ?? ''), ), PositionedDirectional( diff --git a/lib/core/widgets/answer_box/styles/picture_box.dart b/lib/core/widgets/answer_box/styles/picture_box.dart index f1f2a56..164bb9e 100644 --- a/lib/core/widgets/answer_box/styles/picture_box.dart +++ b/lib/core/widgets/answer_box/styles/picture_box.dart @@ -33,29 +33,32 @@ class AnswerPictureBox extends StatelessWidget { child: Stack( alignment: Alignment.center, children: [ - AnimatedSwitcher( - duration: Duration(milliseconds: 150), - reverseDuration: Duration(milliseconds: 150), - switchInCurve: Curves.linear, - switchOutCurve: Curves.linear, - child: selected && (index != correctAnswer) ? - Image.file( - key: Key('1'), - File(image), - fit: BoxFit.cover, - color: MyColors.black, - colorBlendMode: BlendMode.color, - ) : - Image.file( - key: Key('2'), - File(image), - fit: BoxFit.cover, + AspectRatio( + aspectRatio: 1, + child: AnimatedSwitcher( + duration: Duration(milliseconds: 150), + reverseDuration: Duration(milliseconds: 150), + switchInCurve: Curves.linear, + switchOutCurve: Curves.linear, + child: selected && (index != correctAnswer) ? + Image.file( + key: Key('1'), + File(image), + fit: BoxFit.cover, + color: MyColors.black, + colorBlendMode: BlendMode.color, + ) : + Image.file( + key: Key('2'), + File(image), + fit: BoxFit.cover, + ), + transitionBuilder: (child, animation) => + FadeTransition( + opacity: animation, + child: child, + ), ), - transitionBuilder: (child, animation) => - FadeTransition( - opacity: animation, - child: child, - ), ), PositionedDirectional( top: 0, diff --git a/lib/features/question/domain/entity/question_entity.dart b/lib/features/question/domain/entity/question_entity.dart index 4effad3..98bb07a 100644 --- a/lib/features/question/domain/entity/question_entity.dart +++ b/lib/features/question/domain/entity/question_entity.dart @@ -63,6 +63,6 @@ class QuestionEntity extends HiveObject { }){ audio = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/question_audio/${audioInfo?.filename}'; correctAudio = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/correct_answer_audio/${correctAnswerAudioInfo?.filename}'; - image = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/question_image/${imageInfo?.filename}'; + image = '${StoragePath.documentDir.path}/question_image/${imageInfo?.filename}'; } } diff --git a/lib/features/question/presentation/bloc/question_bloc.dart b/lib/features/question/presentation/bloc/question_bloc.dart index 74d14c1..a78836a 100644 --- a/lib/features/question/presentation/bloc/question_bloc.dart +++ b/lib/features/question/presentation/bloc/question_bloc.dart @@ -46,7 +46,8 @@ class QuestionBloc extends Bloc { _mainAudioService.setVolume(volume: MyConstants.musicAudioVolume); } _backgroundAudioService.dispose(); - animationController.dispose(); + answerAnimationController.dispose(); + imageAnimationController.dispose(); return super.close(); } @@ -70,12 +71,14 @@ class QuestionBloc extends Bloc { }; late final Stream volumeStream; late final Stream playingStream; + bool showAnswerSequence = true; /// ------------Controllers------------ final AudioService _mainAudioService; final AudioService _backgroundAudioService = AudioService(volume: 0); final AudioService _effectAudioService; - late final AnimationController animationController; + late final AnimationController answerAnimationController; + late final AnimationController imageAnimationController; /// ------------Functions------------ void registerShowCase() { @@ -131,8 +134,8 @@ class QuestionBloc extends Bloc { } Future playDiamondAudio() async { - await _effectAudioService.setAudio(assetPath: MyAudios.diamondEnd); - await _effectAudioService.play(); + await _mainAudioService.setAudio(assetPath: MyAudios.diamondEnd); + await _mainAudioService.play(); } Future initAudios() async { @@ -174,23 +177,24 @@ class QuestionBloc extends Bloc { ]); } - // Future showQueueAnswer() async { - // final List answers = state.currentQuestion?.answers ?? []; - // if (answers.isNotEmpty) { - // answers.removeWhere((e) => e.imageId == null); - // } - // for (final answer in answers) { - // await Future.delayed(const Duration(milliseconds: 500), () async { - // if (MyContext.get.mounted) { - // await showAnswerDialog( - // context: MyContext.get, - // answerEntity: answer, - // autoClose: true, - // ); - // } - // }); - // } - // } + Future showQueueAnswer() async { + if(!showAnswerSequence) return; + final List answers = state.currentQuestion?.answers ?? []; + if (answers.isNotEmpty) { + answers.removeWhere((e) => e.imageId == null); + } + for (final answer in answers) { + await Future.delayed(const Duration(milliseconds: 500), () async { + if (MyContext.get.mounted) { + await showAnswerDialog( + context: MyContext.get, + answerEntity: answer, + autoClose: true, + ); + } + }); + } + } Future showAnswerDialog({ required BuildContext context, @@ -228,6 +232,10 @@ class QuestionBloc extends Bloc { ); } + void showingAnswerSequence({required bool show}){ + showAnswerSequence = show; + } + /// ------------Event Calls------------ FutureOr _getLevelEvent(GetLevelEvent event, Emitter emit) async { await _getLevelUseCase(QuestionParams(id: int.parse(event.id ?? '0'))).then( @@ -247,16 +255,18 @@ class QuestionBloc extends Bloc { getQuestionStatus: BaseComplete(''), levelEntity: level, currentQuestion: data.questions?.first, - showAnswers: true )); if(LocalStorage.readData(key: MyConstants.firstShowcase) != 'true'){ await Future.delayed(Duration(milliseconds: 500)); - animationController.forward().then((value) { + answerAnimationController.forward().then((value) { startShowcase(); }); } else { await playQuestionAudio(); - animationController.forward(); + imageAnimationController.reverse(); + answerAnimationController.forward().then((value) { + showQueueAnswer(); + }); } }, (error) { @@ -272,7 +282,7 @@ class QuestionBloc extends Bloc { emit(state.copyWith(correctAnswer: event.chooseCorrectAnswer)); if (event.chooseCorrectAnswer) { - animationController.reverse(); + answerAnimationController.reverse(); await showAnswerDialog( context: MyContext.get, correctAudio: state.currentQuestion?.correctAudio, @@ -301,8 +311,13 @@ class QuestionBloc extends Bloc { ); } } else { - await playQuestionAudio(); - animationController.forward(); + showingAnswerSequence(show: true); + imageAnimationController.forward(); + await playQuestionAudio(); + imageAnimationController.reverse(); + answerAnimationController.forward().then((value) { + showQueueAnswer(); + }); } }); } else { diff --git a/lib/features/question/presentation/ui/screens/answer_screen.dart b/lib/features/question/presentation/ui/screens/answer_screen.dart index 3360efd..46e2066 100644 --- a/lib/features/question/presentation/ui/screens/answer_screen.dart +++ b/lib/features/question/presentation/ui/screens/answer_screen.dart @@ -73,7 +73,6 @@ class _AnswerScreenState extends State { @override void dispose() { - audioService.pause(); super.dispose(); } @@ -106,8 +105,11 @@ class _AnswerScreenState extends State { Positioned( bottom: setPlatform(android: MySpaces.s30, iOS: MySpaces.s12), child: TextButton( - onPressed: () { - context.pop(); + onPressed: () async { + await audioService.pause(); + if (context.mounted) { + context.pop(); + } }, style: TextButton.styleFrom( foregroundColor: MyColors.white.withValues(alpha: 0.7), diff --git a/lib/features/question/presentation/ui/screens/question_screen.dart b/lib/features/question/presentation/ui/screens/question_screen.dart index 1d6b9da..b9012d4 100644 --- a/lib/features/question/presentation/ui/screens/question_screen.dart +++ b/lib/features/question/presentation/ui/screens/question_screen.dart @@ -5,15 +5,19 @@ import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.dart'; +import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; import 'package:hadi_hoda_flutter/core/utils/gap.dart'; +import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; import 'package:hadi_hoda_flutter/core/utils/screen_size.dart'; import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart'; import 'package:hadi_hoda_flutter/core/widgets/animations/fade_anim.dart'; +import 'package:hadi_hoda_flutter/core/widgets/animations/fade_anim_controller.dart'; import 'package:hadi_hoda_flutter/core/widgets/animations/globe_animation.dart'; import 'package:hadi_hoda_flutter/core/widgets/animations/slide_anim.dart'; import 'package:hadi_hoda_flutter/core/widgets/animations/slide_up_fade.dart'; import 'package:hadi_hoda_flutter/core/widgets/answer_box/answer_box.dart'; +import 'package:hadi_hoda_flutter/core/widgets/answer_box/styles/picture_box.dart'; import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; import 'package:hadi_hoda_flutter/core/widgets/showcase/my_showcase_widget.dart'; import 'package:hadi_hoda_flutter/features/question/domain/entity/answer_entity.dart'; @@ -30,16 +34,25 @@ class QuestionScreen extends StatefulWidget { State createState() => _QuestionScreenState(); } -class _QuestionScreenState extends State with SingleTickerProviderStateMixin { +class _QuestionScreenState extends State with TickerProviderStateMixin { @override void initState() { super.initState(); - context.read().animationController = AnimationController( + context.read().answerAnimationController = AnimationController( vsync: this, duration: Duration(milliseconds: 500), reverseDuration: Duration(milliseconds: 500), ); + + context.read().imageAnimationController = AnimationController( + vsync: this, + duration: Duration(milliseconds: 500), + reverseDuration: Duration(milliseconds: 500), + ); + if(LocalStorage.readData(key: MyConstants.firstShowcase) == 'true') { + context.read().imageAnimationController.forward(); + } } @override @@ -49,12 +62,39 @@ class _QuestionScreenState extends State with SingleTickerProvid _stepper(), _titles(context), MySpaces.s20.gapHeight, - _answers(context), + Expanded( + child: Stack( + children: [ + _questionImage(context), + _answers(context), + ], + ), + ), _bottom(context), ], ); } + Widget _questionImage(BuildContext context) { + return Column( + children: [ + Spacer(), + FadeAnimController( + controller: context.read().imageAnimationController, + child: BlocBuilder( + builder: (context, state) => AnswerPictureBox( + selected: false, + correctAnswer: 0, + index: 0, + image: state.currentQuestion?.image ?? '', + ), + ), + ), + Spacer(), + ], + ); + } + Widget _stepper() { return BlocBuilder( buildWhen: (previous, current) => @@ -97,54 +137,53 @@ class _QuestionScreenState extends State with SingleTickerProvid ); } - Expanded _answers(BuildContext context) { - return Expanded( - child: BlocBuilder( - buildWhen: (previous, current) => - previous.currentQuestion?.id != current.currentQuestion?.id, - builder: (context, state) => GridView.builder( - itemCount: state.currentQuestion?.answers?.length ?? 0, - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - padding: EdgeInsets.symmetric( - horizontal: setSize(context: context, tablet: 70) ?? 0, - ), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: MySpaces.s20, - mainAxisSpacing: 80, - ), - itemBuilder: (context, index) => - state.currentQuestion?.answers?[index].imageId == null - ? SizedBox.shrink() - : SlideAnim( - key: Key('${state.currentQuestion?.id}'), - controller: context.read().animationController, - index: index, - child: MyShowcaseWidget( - globalKey: context.read().showCaseKey['answer_key_$index']!, - description: context.translate.showcase_answer, - child: AnswerBox( - globalKey: context.read().showCaseKey['notif_key_$index']!, - index: state.currentQuestion?.answers?[index].order ?? 1, - answer: - state.currentQuestion?.answers?[index] ?? - AnswerEntity(), - correctAnswer: state.currentQuestion?.correctAnswer ?? 0, - onNotifTap: (AnswerEntity answer) { - context.read().showAnswerDialog( - context: context, - answerEntity: answer, - ); - }, - onTap: (isCorrect, correctAnswer) => - context.read().add( - ChooseAnswerEvent(isCorrect, correctAnswer, context), - ), - ), + Widget _answers(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.currentQuestion?.id != current.currentQuestion?.id, + builder: (context, state) => GridView.builder( + itemCount: state.currentQuestion?.answers?.length ?? 0, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsets.symmetric( + horizontal: setSize(context: context, tablet: 70) ?? 0, + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: MySpaces.s20, + mainAxisSpacing: 20, + mainAxisExtent: 250 + ), + itemBuilder: (context, index) => + state.currentQuestion?.answers?[index].imageId == null + ? SizedBox.shrink() + : SlideAnim( + key: Key('${state.currentQuestion?.id}'), + controller: context.read().answerAnimationController, + index: index, + child: MyShowcaseWidget( + globalKey: context.read().showCaseKey['answer_key_$index']!, + description: context.translate.showcase_answer, + child: AnswerBox( + globalKey: context.read().showCaseKey['notif_key_$index']!, + index: state.currentQuestion?.answers?[index].order ?? 1, + answer: + state.currentQuestion?.answers?[index] ?? + AnswerEntity(), + correctAnswer: state.currentQuestion?.correctAnswer ?? 0, + onNotifTap: (AnswerEntity answer) { + context.read().showAnswerDialog( + context: context, + answerEntity: answer, + ); + }, + onTap: (isCorrect, correctAnswer) => + context.read().add( + ChooseAnswerEvent(isCorrect, correctAnswer, context), + ), ), ), - ), + ), ), ); } @@ -173,6 +212,27 @@ class _QuestionScreenState extends State with SingleTickerProvid ), ), ), + Positioned( + left: 120, + right: 120, + child: FadeAnimController( + controller: context.read().imageAnimationController, + child: TextButton( + onPressed: () async { + context.read().imageAnimationController.reverse(); + context.read().answerAnimationController.forward(); + context.read().showingAnswerSequence(show: false); + }, + style: TextButton.styleFrom( + foregroundColor: MyColors.white.withValues(alpha: 0.7), + ), + child: Text( + context.translate.skip, + style: MYTextStyle.button2 + ), + ), + ), + ), PositionedDirectional( end: 0, child: MyShowcaseWidget( diff --git a/pubspec.yaml b/pubspec.yaml index bae9346..82c75e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: hadi_hoda_flutter description: "A new Flutter project." publish_to: 'none' -version: 0.1.7+1 +version: 0.1.8+1 environment: sdk: ^3.9.2