mirror of https://github.com/flutter/pinball.git
feat: adding mobile controls (#377)
* feat: adding mobile controls * adding tests for pinball dpad button * feat: tests * lint * Apply suggestions from code review Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * suggestions Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>pull/389/head
parent
bba5316dfd
commit
9d184fedf9
@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
/// {@template mobile_controls}
|
||||
/// Widget with the controls used to enable the user initials input on mobile.
|
||||
/// {@endtemplate}
|
||||
class MobileControls extends StatelessWidget {
|
||||
/// {@macro mobile_controls}
|
||||
const MobileControls({
|
||||
Key? key,
|
||||
required this.game,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Game instance
|
||||
final PinballGame game;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
MobileDpad(
|
||||
onTapUp: () => game.triggerVirtualKeyUp(LogicalKeyboardKey.arrowUp),
|
||||
onTapDown: () => game.triggerVirtualKeyUp(
|
||||
LogicalKeyboardKey.arrowDown,
|
||||
),
|
||||
onTapLeft: () => game.triggerVirtualKeyUp(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
),
|
||||
onTapRight: () => game.triggerVirtualKeyUp(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
),
|
||||
),
|
||||
PinballButton(
|
||||
text: l10n.enter,
|
||||
onTap: () => game.triggerVirtualKeyUp(LogicalKeyboardKey.enter),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
/// {@template mobile_dpad}
|
||||
/// Widget rendering 4 directional input arrows.
|
||||
/// {@endtemplate}
|
||||
class MobileDpad extends StatelessWidget {
|
||||
/// {@template mobile_dpad}
|
||||
const MobileDpad({
|
||||
Key? key,
|
||||
required this.onTapUp,
|
||||
required this.onTapDown,
|
||||
required this.onTapLeft,
|
||||
required this.onTapRight,
|
||||
}) : super(key: key);
|
||||
|
||||
static const _size = 180.0;
|
||||
|
||||
/// Called when dpad up is pressed
|
||||
final VoidCallback onTapUp;
|
||||
|
||||
/// Called when dpad down is pressed
|
||||
final VoidCallback onTapDown;
|
||||
|
||||
/// Called when dpad left is pressed
|
||||
final VoidCallback onTapLeft;
|
||||
|
||||
/// Called when dpad right is pressed
|
||||
final VoidCallback onTapRight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: _size,
|
||||
height: _size,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
PinballDpadButton(
|
||||
direction: PinballDpadDirection.up,
|
||||
onTap: onTapUp,
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
PinballDpadButton(
|
||||
direction: PinballDpadDirection.left,
|
||||
onTap: onTapLeft,
|
||||
),
|
||||
const Spacer(),
|
||||
PinballDpadButton(
|
||||
direction: PinballDpadDirection.right,
|
||||
onTap: onTapRight,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
PinballDpadButton(
|
||||
direction: PinballDpadDirection.down,
|
||||
onTap: onTapDown,
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
export 'bonus_animation.dart';
|
||||
export 'game_hud.dart';
|
||||
export 'mobile_controls.dart';
|
||||
export 'mobile_dpad.dart';
|
||||
export 'play_button_overlay.dart';
|
||||
export 'round_count_display.dart';
|
||||
export 'score_view.dart';
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball_ui/gen/gen.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
/// Enum with all possibile directions of a [PinballDpadButton].
|
||||
enum PinballDpadDirection {
|
||||
/// Up
|
||||
up,
|
||||
|
||||
/// Down
|
||||
down,
|
||||
|
||||
/// Left
|
||||
left,
|
||||
|
||||
/// Right
|
||||
right,
|
||||
}
|
||||
|
||||
extension _PinballDpadDirectionX on PinballDpadDirection {
|
||||
String toAsset() {
|
||||
switch (this) {
|
||||
case PinballDpadDirection.up:
|
||||
return Assets.images.button.dpadUp.keyName;
|
||||
case PinballDpadDirection.down:
|
||||
return Assets.images.button.dpadDown.keyName;
|
||||
case PinballDpadDirection.left:
|
||||
return Assets.images.button.dpadLeft.keyName;
|
||||
case PinballDpadDirection.right:
|
||||
return Assets.images.button.dpadRight.keyName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template pinball_dpad_button}
|
||||
/// Widget that renders a Dpad button with a given direction.
|
||||
/// {@endtemplate}
|
||||
class PinballDpadButton extends StatelessWidget {
|
||||
/// {@macro pinball_dpad_button}
|
||||
const PinballDpadButton({
|
||||
Key? key,
|
||||
required this.direction,
|
||||
required this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Which [PinballDpadDirection] this button is.
|
||||
final PinballDpadDirection direction;
|
||||
|
||||
/// The function executed when the button is pressed.
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: PinballColors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Image.asset(
|
||||
direction.toAsset(),
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export 'animated_ellipsis_text.dart';
|
||||
export 'crt_background.dart';
|
||||
export 'pinball_button.dart';
|
||||
export 'pinball_dpad_button.dart';
|
||||
export 'pinball_loading_indicator.dart';
|
||||
|
@ -0,0 +1,122 @@
|
||||
// ignore_for_file: one_member_abstracts
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball_ui/gen/gen.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
extension _WidgetTesterX on WidgetTester {
|
||||
Future<void> pumpButton({
|
||||
required PinballDpadDirection direction,
|
||||
required VoidCallback onTap,
|
||||
}) async {
|
||||
await pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: PinballDpadButton(
|
||||
direction: direction,
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension _CommonFindersX on CommonFinders {
|
||||
Finder byImagePath(String path) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) {
|
||||
if (widget is Image) {
|
||||
final image = widget.image;
|
||||
|
||||
if (image is AssetImage) {
|
||||
return image.keyName == path;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _VoidCallbackStubBase {
|
||||
void onCall();
|
||||
}
|
||||
|
||||
class _VoidCallbackStub extends Mock implements _VoidCallbackStubBase {}
|
||||
|
||||
void main() {
|
||||
group('PinballDpadButton', () {
|
||||
testWidgets('can be tapped', (tester) async {
|
||||
final stub = _VoidCallbackStub();
|
||||
await tester.pumpButton(
|
||||
direction: PinballDpadDirection.up,
|
||||
onTap: stub.onCall,
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(Image));
|
||||
|
||||
verify(stub.onCall).called(1);
|
||||
});
|
||||
|
||||
group('up', () {
|
||||
testWidgets('renders the correct image', (tester) async {
|
||||
await tester.pumpButton(
|
||||
direction: PinballDpadDirection.up,
|
||||
onTap: () {},
|
||||
);
|
||||
|
||||
expect(
|
||||
find.byImagePath(Assets.images.button.dpadUp.keyName),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('down', () {
|
||||
testWidgets('renders the correct image', (tester) async {
|
||||
await tester.pumpButton(
|
||||
direction: PinballDpadDirection.down,
|
||||
onTap: () {},
|
||||
);
|
||||
|
||||
expect(
|
||||
find.byImagePath(Assets.images.button.dpadDown.keyName),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('left', () {
|
||||
testWidgets('renders the correct image', (tester) async {
|
||||
await tester.pumpButton(
|
||||
direction: PinballDpadDirection.left,
|
||||
onTap: () {},
|
||||
);
|
||||
|
||||
expect(
|
||||
find.byImagePath(Assets.images.button.dpadLeft.keyName),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('right', () {
|
||||
testWidgets('renders the correct image', (tester) async {
|
||||
await tester.pumpButton(
|
||||
direction: PinballDpadDirection.right,
|
||||
onTap: () {},
|
||||
);
|
||||
|
||||
expect(
|
||||
find.byImagePath(Assets.images.button.dpadRight.keyName),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
class _MockPinballGame extends Mock implements PinballGame {}
|
||||
|
||||
extension _WidgetTesterX on WidgetTester {
|
||||
Future<void> pumpMobileControls(PinballGame game) async {
|
||||
await pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
],
|
||||
home: Scaffold(
|
||||
body: MobileControls(game: game),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension _CommonFindersX on CommonFinders {
|
||||
Finder byPinballDpadDirection(PinballDpadDirection direction) {
|
||||
return byWidgetPredicate((widget) {
|
||||
return widget is PinballDpadButton && widget.direction == direction;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('MobileControls', () {
|
||||
testWidgets('renders', (tester) async {
|
||||
await tester.pumpMobileControls(_MockPinballGame());
|
||||
|
||||
expect(find.byType(PinballButton), findsOneWidget);
|
||||
expect(find.byType(MobileDpad), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('correctly triggers the arrow up', (tester) async {
|
||||
var pressed = false;
|
||||
final component = KeyboardInputController(
|
||||
keyUp: {
|
||||
LogicalKeyboardKey.arrowUp: () => pressed = true,
|
||||
},
|
||||
);
|
||||
final game = _MockPinballGame();
|
||||
when(game.descendants).thenReturn([component]);
|
||||
|
||||
await tester.pumpMobileControls(game);
|
||||
await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.up));
|
||||
await tester.pump();
|
||||
|
||||
expect(pressed, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('correctly triggers the arrow down', (tester) async {
|
||||
var pressed = false;
|
||||
final component = KeyboardInputController(
|
||||
keyUp: {
|
||||
LogicalKeyboardKey.arrowDown: () => pressed = true,
|
||||
},
|
||||
);
|
||||
final game = _MockPinballGame();
|
||||
when(game.descendants).thenReturn([component]);
|
||||
|
||||
await tester.pumpMobileControls(game);
|
||||
await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.down));
|
||||
await tester.pump();
|
||||
|
||||
expect(pressed, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('correctly triggers the arrow right', (tester) async {
|
||||
var pressed = false;
|
||||
final component = KeyboardInputController(
|
||||
keyUp: {
|
||||
LogicalKeyboardKey.arrowRight: () => pressed = true,
|
||||
},
|
||||
);
|
||||
final game = _MockPinballGame();
|
||||
when(game.descendants).thenReturn([component]);
|
||||
|
||||
await tester.pumpMobileControls(game);
|
||||
await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.right));
|
||||
await tester.pump();
|
||||
|
||||
expect(pressed, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('correctly triggers the arrow left', (tester) async {
|
||||
var pressed = false;
|
||||
final component = KeyboardInputController(
|
||||
keyUp: {
|
||||
LogicalKeyboardKey.arrowLeft: () => pressed = true,
|
||||
},
|
||||
);
|
||||
final game = _MockPinballGame();
|
||||
when(game.descendants).thenReturn([component]);
|
||||
|
||||
await tester.pumpMobileControls(game);
|
||||
await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.left));
|
||||
await tester.pump();
|
||||
|
||||
expect(pressed, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('correctly triggers the enter', (tester) async {
|
||||
var pressed = false;
|
||||
final component = KeyboardInputController(
|
||||
keyUp: {
|
||||
LogicalKeyboardKey.enter: () => pressed = true,
|
||||
},
|
||||
);
|
||||
final game = _MockPinballGame();
|
||||
when(game.descendants).thenReturn([component]);
|
||||
|
||||
await tester.pumpMobileControls(game);
|
||||
await tester.tap(find.byType(PinballButton));
|
||||
await tester.pump();
|
||||
|
||||
expect(pressed, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
// ignore_for_file: one_member_abstracts
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
extension _WidgetTesterX on WidgetTester {
|
||||
Future<void> pumpDpad({
|
||||
required VoidCallback onTapUp,
|
||||
required VoidCallback onTapDown,
|
||||
required VoidCallback onTapLeft,
|
||||
required VoidCallback onTapRight,
|
||||
}) async {
|
||||
await pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: MobileDpad(
|
||||
onTapUp: onTapUp,
|
||||
onTapDown: onTapDown,
|
||||
onTapLeft: onTapLeft,
|
||||
onTapRight: onTapRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension _CommonFindersX on CommonFinders {
|
||||
Finder byPinballDpadDirection(PinballDpadDirection direction) {
|
||||
return byWidgetPredicate((widget) {
|
||||
return widget is PinballDpadButton && widget.direction == direction;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _VoidCallbackStubBase {
|
||||
void onCall();
|
||||
}
|
||||
|
||||
class _VoidCallbackStub extends Mock implements _VoidCallbackStubBase {}
|
||||
|
||||
void main() {
|
||||
group('MobileDpad', () {
|
||||
testWidgets('renders correctly', (tester) async {
|
||||
await tester.pumpDpad(
|
||||
onTapUp: () {},
|
||||
onTapDown: () {},
|
||||
onTapLeft: () {},
|
||||
onTapRight: () {},
|
||||
);
|
||||
|
||||
expect(
|
||||
find.byType(PinballDpadButton),
|
||||
findsNWidgets(4),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('can tap up', (tester) async {
|
||||
final stub = _VoidCallbackStub();
|
||||
await tester.pumpDpad(
|
||||
onTapUp: stub.onCall,
|
||||
onTapDown: () {},
|
||||
onTapLeft: () {},
|
||||
onTapRight: () {},
|
||||
);
|
||||
|
||||
await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.up));
|
||||
verify(stub.onCall).called(1);
|
||||
});
|
||||
|
||||
testWidgets('can tap down', (tester) async {
|
||||
final stub = _VoidCallbackStub();
|
||||
await tester.pumpDpad(
|
||||
onTapUp: () {},
|
||||
onTapDown: stub.onCall,
|
||||
onTapLeft: () {},
|
||||
onTapRight: () {},
|
||||
);
|
||||
|
||||
await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.down));
|
||||
verify(stub.onCall).called(1);
|
||||
});
|
||||
|
||||
testWidgets('can tap left', (tester) async {
|
||||
final stub = _VoidCallbackStub();
|
||||
await tester.pumpDpad(
|
||||
onTapUp: () {},
|
||||
onTapDown: () {},
|
||||
onTapLeft: stub.onCall,
|
||||
onTapRight: () {},
|
||||
);
|
||||
|
||||
await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.left));
|
||||
verify(stub.onCall).called(1);
|
||||
});
|
||||
|
||||
testWidgets('can tap left', (tester) async {
|
||||
final stub = _VoidCallbackStub();
|
||||
await tester.pumpDpad(
|
||||
onTapUp: () {},
|
||||
onTapDown: () {},
|
||||
onTapLeft: () {},
|
||||
onTapRight: stub.onCall,
|
||||
);
|
||||
|
||||
await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.right));
|
||||
verify(stub.onCall).called(1);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in new issue