From 26acb6346035d42500a399deb88e2ef7a6fca863 Mon Sep 17 00:00:00 2001 From: Jorge Coca Date: Mon, 2 May 2022 08:38:09 -0500 Subject: [PATCH] feat: added animations to character selection (#284) --- lib/game/view/pinball_game_page.dart | 5 +- .../view/character_selection_page.dart | 15 +-- .../view/selected_character.dart | 102 ++++++++++++++++++ lib/select_character/view/view.dart | 1 + pubspec.lock | 7 -- pubspec.yaml | 1 - .../google_word_bonus_behavior_test.dart | 2 +- .../behaviors/multipliers_behavior_test.dart | 2 +- .../widgets/play_button_overlay_test.dart | 11 +- test/helpers/helpers.dart | 1 - test/helpers/navigator.dart | 37 ------- test/helpers/pump_app.dart | 7 +- .../view/character_selection_page_test.dart | 55 +++++++++- 13 files changed, 171 insertions(+), 75 deletions(-) create mode 100644 lib/select_character/view/selected_character.dart delete mode 100644 test/helpers/navigator.dart diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index be11a15c..9ac25cfe 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -44,15 +44,14 @@ class PinballGamePage extends StatelessWidget { ...game.preLoadAssets(), pinballAudio.load(), ...BonusAnimation.loadAssets(), + ...SelectedCharacter.loadAssets(), ]; return MultiBlocProvider( providers: [ BlocProvider(create: (_) => StartGameBloc(game: game)), BlocProvider(create: (_) => GameBloc()), - BlocProvider( - create: (_) => AssetsManagerCubit(loadables)..load(), - ), + BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()), ], child: PinballGameView(game: game), ); diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 1df01ad7..5fb0f594 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/select_character/cubit/character_theme_cubit.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_ui/pinball_ui.dart'; @@ -118,19 +117,7 @@ class _CharacterPreview extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - state.characterTheme.name, - style: Theme.of(context).textTheme.headline2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ), - const SizedBox(height: 10), - Expanded(child: state.characterTheme.icon.image()), - ], - ); + return SelectedCharacter(currentCharacter: state.characterTheme); }, ); } diff --git a/lib/select_character/view/selected_character.dart b/lib/select_character/view/selected_character.dart new file mode 100644 index 00000000..68b5ad8a --- /dev/null +++ b/lib/select_character/view/selected_character.dart @@ -0,0 +1,102 @@ +import 'package:flame/components.dart'; +import 'package:flame/flame.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +/// {@template selected_character} +/// Shows an animated version of the character currently selected. +/// {@endtemplate} +class SelectedCharacter extends StatefulWidget { + /// {@macro selected_character} + const SelectedCharacter({ + Key? key, + required this.currentCharacter, + }) : super(key: key); + + /// The character that is selected at the moment. + final CharacterTheme currentCharacter; + + @override + State createState() => _SelectedCharacterState(); + + /// Returns a list of assets to be loaded. + static List loadAssets() { + return [ + Flame.images.load(const DashTheme().animation.keyName), + Flame.images.load(const AndroidTheme().animation.keyName), + Flame.images.load(const DinoTheme().animation.keyName), + Flame.images.load(const SparkyTheme().animation.keyName), + ]; + } +} + +class _SelectedCharacterState extends State + with TickerProviderStateMixin { + SpriteAnimationController? _controller; + + @override + void initState() { + super.initState(); + _setupCharacterAnimation(); + } + + @override + void didUpdateWidget(covariant SelectedCharacter oldWidget) { + super.didUpdateWidget(oldWidget); + _setupCharacterAnimation(); + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + widget.currentCharacter.name, + style: Theme.of(context).textTheme.headline2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: SpriteAnimationWidget( + controller: _controller!, + anchor: Anchor.center, + ), + ); + }, + ), + ), + ], + ); + } + + void _setupCharacterAnimation() { + final spriteSheet = SpriteSheet.fromColumnsAndRows( + image: Flame.images.fromCache(widget.currentCharacter.animation.keyName), + columns: 12, + rows: 6, + ); + final animation = spriteSheet.createAnimation( + row: 0, + stepTime: 1 / 24, + to: spriteSheet.rows * spriteSheet.columns, + ); + if (_controller != null) _controller?.dispose(); + _controller = SpriteAnimationController(vsync: this, animation: animation) + ..forward() + ..repeat(); + } +} diff --git a/lib/select_character/view/view.dart b/lib/select_character/view/view.dart index 1af489b5..41f82053 100644 --- a/lib/select_character/view/view.dart +++ b/lib/select_character/view/view.dart @@ -1 +1,2 @@ export 'character_selection_page.dart'; +export 'selected_character.dart'; diff --git a/pubspec.lock b/pubspec.lock index db5233c3..ffbd3899 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -401,13 +401,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" - mockingjay: - dependency: "direct dev" - description: - name: mockingjay - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" mocktail: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index fa08f453..b98c84a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,6 @@ dev_dependencies: flame_test: ^1.3.0 flutter_test: sdk: flutter - mockingjay: ^0.3.0 mocktail: ^0.3.0 very_good_analysis: ^2.4.0 diff --git a/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart b/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart index 97efc207..305a0c1f 100644 --- a/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart +++ b/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart @@ -3,7 +3,7 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; diff --git a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart index a4f3502c..c4f2bd33 100644 --- a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart +++ b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index 2229f4b5..ee9778bc 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -1,8 +1,10 @@ import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/flame.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_theme/pinball_theme.dart'; import '../../../helpers/helpers.dart'; @@ -12,7 +14,12 @@ void main() { late GameFlowController gameFlowController; late CharacterThemeCubit characterThemeCubit; - setUp(() { + setUp(() async { + Flame.images.prefix = ''; + await Flame.images.load(const DashTheme().animation.keyName); + await Flame.images.load(const AndroidTheme().animation.keyName); + await Flame.images.load(const DinoTheme().animation.keyName); + await Flame.images.load(const SparkyTheme().animation.keyName); game = MockPinballGame(); gameFlowController = MockGameFlowController(); characterThemeCubit = MockCharacterThemeCubit(); @@ -49,7 +56,7 @@ void main() { characterThemeCubit: characterThemeCubit, ); await tester.tap(find.text('Play')); - await tester.pumpAndSettle(); + await tester.pump(); expect(find.byType(CharacterSelectionDialog), findsOneWidget); }); }); diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 58b4b126..fb27f72a 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -9,7 +9,6 @@ export 'fakes.dart'; export 'forge2d.dart'; export 'key_testers.dart'; export 'mocks.dart'; -export 'navigator.dart'; export 'pump_app.dart'; export 'test_games.dart'; export 'text_span.dart'; diff --git a/test/helpers/navigator.dart b/test/helpers/navigator.dart deleted file mode 100644 index 5a8ea52e..00000000 --- a/test/helpers/navigator.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'helpers.dart'; - -Future expectNavigatesToRoute( - WidgetTester tester, - Route route, { - bool hasFlameGameInside = false, -}) async { - // ignore: avoid_dynamic_calls - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context).push(route); - }, - child: const Text('Tap me'), - ); - }, - ), - ), - ); - - await tester.tap(find.text('Tap me')); - if (hasFlameGameInside) { - // We can't use pumpAndSettle here because the page renders a Flame game - // which is an infinity animation, so it will timeout - await tester.pump(); // Runs the button action - await tester.pump(); // Runs the navigation - } else { - await tester.pumpAndSettle(); - } - - expect(find.byType(Type), findsOneWidget); -} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index be67d4d0..672f9b5e 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -11,7 +11,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:mockingjay/mockingjay.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; @@ -51,7 +51,6 @@ MockAssetsManagerCubit _buildDefaultAssetsManagerCubit() { extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { - MockNavigator? navigator, GameBloc? gameBloc, StartGameBloc? startGameBloc, AssetsManagerCubit? assetsManagerCubit, @@ -92,9 +91,7 @@ extension PumpApp on WidgetTester { GlobalMaterialLocalizations.delegate, ], supportedLocales: AppLocalizations.supportedLocales, - home: navigator != null - ? MockNavigatorProvider(navigator: navigator, child: widget) - : widget, + home: widget, ), ), ), diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index b9c95f7f..c5cfb494 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -1,4 +1,5 @@ import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/flame.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -10,9 +11,15 @@ import 'package:pinball_ui/pinball_ui.dart'; import '../../helpers/helpers.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); late CharacterThemeCubit characterThemeCubit; - setUp(() { + setUp(() async { + Flame.images.prefix = ''; + await Flame.images.load(const DashTheme().animation.keyName); + await Flame.images.load(const AndroidTheme().animation.keyName); + await Flame.images.load(const DinoTheme().animation.keyName); + await Flame.images.load(const SparkyTheme().animation.keyName); characterThemeCubit = MockCharacterThemeCubit(); whenListen( characterThemeCubit, @@ -38,7 +45,7 @@ void main() { characterThemeCubit: characterThemeCubit, ); await tester.tap(find.text('test')); - await tester.pumpAndSettle(); + await tester.pump(); expect(find.byType(CharacterSelectionDialog), findsOneWidget); }); }); @@ -50,7 +57,7 @@ void main() { characterThemeCubit: characterThemeCubit, ); await tester.tap(find.byKey(const Key('sparky_character_selection'))); - await tester.pumpAndSettle(); + await tester.pump(); verify( () => characterThemeCubit.characterSelected(const SparkyTheme()), ).called(1); @@ -68,5 +75,47 @@ void main() { expect(find.byType(CharacterSelectionDialog), findsNothing); expect(find.byType(HowToPlayDialog), findsOneWidget); }); + + testWidgets('updating the selected character updates the preview', + (tester) async { + await tester.pumpApp(_TestCharacterPreview()); + expect(find.text('Dash'), findsOneWidget); + await tester.tap(find.text('test')); + await tester.pump(); + expect(find.text('Android'), findsOneWidget); + }); }); } + +class _TestCharacterPreview extends StatefulWidget { + @override + State createState() => _TestCharacterPreviewState(); +} + +class _TestCharacterPreviewState extends State<_TestCharacterPreview> { + late CharacterTheme currentCharacter; + + @override + void initState() { + super.initState(); + currentCharacter = const DashTheme(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded(child: SelectedCharacter(currentCharacter: currentCharacter)), + TextButton( + onPressed: () { + setState(() { + currentCharacter = const AndroidTheme(); + }); + }, + child: const Text('test'), + ) + ], + ); + } +}