mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 19:12:31 +08:00
feat: Add advanced button component (#2742)
New button with support for multiple states: <img width="278" alt="image" src="https://github.com/flame-engine/flame/assets/18004353/041c1105-8991-4976-b1a2-0553c149ec4e">
This commit is contained in:
@ -142,3 +142,33 @@ else which isn't a pure sprite.
|
||||
|
||||
Flame has a separate plugin to support external game controllers (gamepads), check
|
||||
[here](https://github.com/flame-engine/flame_gamepad) for more information.
|
||||
|
||||
|
||||
## AdvancedButtonComponent
|
||||
|
||||
The `AdvancedButtonComponent` have separate states for each of the different pointer phases.
|
||||
The skin can be customized for each state and each skin is represented by a `PositionComponent`.
|
||||
|
||||
These are the fields that can be used to customize the looks of the `AdvancedButtonComponent`:
|
||||
|
||||
- `defaultSkin`: Component that will be displayed by default on the button.
|
||||
- `downSkin`: Component displayed when the button is clicked or tapped.
|
||||
- `hoverSkin`: Component displayed when the button is hovered. (desktop and web).
|
||||
- `defaultLabel`: Component shown on top of skins. Automatically aligned to center.
|
||||
- `disabledSkin`: Component displayed when button is disabled.
|
||||
- `disabledLabel`: Component shown on top of skins when button is disabled.
|
||||
|
||||
|
||||
## ToggleButtonComponent
|
||||
|
||||
The [ToggleButtonComponent] is an [AdvancedButtonComponent] that can switch between selected
|
||||
and not selected.
|
||||
|
||||
In addition to the already existing skins, the [ToggleButtonComponent] contains the following skins:
|
||||
|
||||
- `defaultSelectedSkin`: The component to display when the button is selected.
|
||||
- `downAndSelectedSkin`: The component that is displayed when the selectable button is selected and
|
||||
pressed.
|
||||
- `hoverAndSelectedSkin`: Hover on selectable and selected button (desktop and web).
|
||||
- `disabledAndSelectedSkin`: For when the button is selected and in the disabled state.
|
||||
- `defaultSelectedLabel`: Component shown on top of the skins when button is selected.
|
||||
|
||||
125
examples/lib/stories/input/advanced_button_example.dart
Normal file
125
examples/lib/stories/input/advanced_button_example.dart
Normal file
@ -0,0 +1,125 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/palette.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
class AdvancedButtonExample extends FlameGame {
|
||||
static const String description =
|
||||
'''This example shows how you can use a button with different states''';
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
final defaultButton = DefaultButton();
|
||||
defaultButton.position = Vector2(50, 50);
|
||||
defaultButton.size = Vector2(250, 50);
|
||||
add(defaultButton);
|
||||
|
||||
final disableButton = DisableButton();
|
||||
disableButton.isDisabled = true;
|
||||
disableButton.position = Vector2(400, 50);
|
||||
disableButton.size = defaultButton.size;
|
||||
add(disableButton);
|
||||
|
||||
final toggleButton = ToggleButton();
|
||||
toggleButton.position = Vector2(50, 150);
|
||||
toggleButton.size = defaultButton.size;
|
||||
add(toggleButton);
|
||||
}
|
||||
}
|
||||
|
||||
class ToggleButton extends ToggleButtonComponent {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
super.onLoad();
|
||||
|
||||
defaultLabel = TextComponent(
|
||||
text: 'Toggle button',
|
||||
textRenderer: TextPaint(
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
color: BasicPalette.white.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
defaultSelectedLabel = TextComponent(
|
||||
text: 'Toggle button',
|
||||
textRenderer: TextPaint(
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
color: BasicPalette.red.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
defaultSkin = RoundedRectComponent()
|
||||
..setColor(const Color.fromRGBO(0, 200, 0, 1));
|
||||
|
||||
hoverSkin = RoundedRectComponent()
|
||||
..setColor(const Color.fromRGBO(0, 180, 0, 1));
|
||||
|
||||
downSkin = RoundedRectComponent()
|
||||
..setColor(const Color.fromRGBO(0, 100, 0, 1));
|
||||
|
||||
defaultSelectedSkin = RoundedRectComponent()
|
||||
..setColor(const Color.fromRGBO(0, 0, 200, 1));
|
||||
|
||||
hoverAndSelectedSkin = RoundedRectComponent()
|
||||
..setColor(const Color.fromRGBO(0, 0, 180, 1));
|
||||
|
||||
downAndSelectedSkin = RoundedRectComponent()
|
||||
..setColor(const Color.fromRGBO(0, 0, 100, 1));
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultButton extends AdvancedButtonComponent {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
super.onLoad();
|
||||
|
||||
defaultLabel = TextComponent(text: 'Default button');
|
||||
|
||||
defaultSkin = RoundedRectComponent()
|
||||
..setColor(const Color.fromRGBO(0, 200, 0, 1));
|
||||
|
||||
hoverSkin = RoundedRectComponent()
|
||||
..setColor(const Color.fromRGBO(0, 180, 0, 1));
|
||||
|
||||
downSkin = RoundedRectComponent()
|
||||
..setColor(const Color.fromRGBO(0, 100, 0, 1));
|
||||
}
|
||||
}
|
||||
|
||||
class DisableButton extends AdvancedButtonComponent {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
super.onLoad();
|
||||
|
||||
disabledLabel = TextComponent(text: 'Disabled button');
|
||||
|
||||
defaultSkin = RoundedRectComponent()
|
||||
..setColor(const Color.fromRGBO(0, 255, 0, 1));
|
||||
|
||||
disabledSkin = RoundedRectComponent()
|
||||
..setColor(const Color.fromRGBO(100, 100, 100, 1));
|
||||
}
|
||||
}
|
||||
|
||||
class RoundedRectComponent extends PositionComponent with HasPaint {
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
canvas.drawRRect(
|
||||
RRect.fromLTRBAndCorners(
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height,
|
||||
topLeft: Radius.circular(height),
|
||||
topRight: Radius.circular(height),
|
||||
bottomRight: Radius.circular(height),
|
||||
bottomLeft: Radius.circular(height),
|
||||
),
|
||||
paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:examples/commons/commons.dart';
|
||||
import 'package:examples/stories/input/advanced_button_example.dart';
|
||||
import 'package:examples/stories/input/double_tap_callbacks_example.dart';
|
||||
import 'package:examples/stories/input/draggables_example.dart';
|
||||
import 'package:examples/stories/input/gesture_hitboxes_example.dart';
|
||||
@ -129,5 +130,11 @@ void addInputStories(Dashbook dashbook) {
|
||||
(_) => GameWidget(game: JoystickAdvancedExample()),
|
||||
codeLink: baseLink('input/joystick_advanced_example.dart'),
|
||||
info: JoystickAdvancedExample.description,
|
||||
)
|
||||
..add(
|
||||
'Advanced Button',
|
||||
(_) => GameWidget(game: AdvancedButtonExample()),
|
||||
codeLink: baseLink('input/advanced_button_example.dart'),
|
||||
info: AdvancedButtonExample.description,
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,8 +13,10 @@ export 'src/components/core/position_type.dart';
|
||||
export 'src/components/custom_painter_component.dart';
|
||||
export 'src/components/fps_component.dart';
|
||||
export 'src/components/fps_text_component.dart';
|
||||
export 'src/components/input/advanced_button_component.dart';
|
||||
export 'src/components/input/joystick_component.dart';
|
||||
export 'src/components/input/keyboard_listener_component.dart';
|
||||
export 'src/components/input/toggle_button_component.dart';
|
||||
export 'src/components/isometric_tile_map_component.dart';
|
||||
export 'src/components/mixins/component_viewport_margin.dart';
|
||||
export 'src/components/mixins/coordinate_transform.dart';
|
||||
|
||||
@ -0,0 +1,292 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/layout.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// The [AdvancedButtonComponent] has different skins for
|
||||
/// different button states.
|
||||
/// The [defaultSkin] must be added to the constructor or
|
||||
/// if you are inheriting - defined in the onLod method.
|
||||
///
|
||||
/// The label is a [PositionComponent] and is added
|
||||
/// to the foreground of the button. The label is automatically aligned to
|
||||
/// the center of the button.
|
||||
///
|
||||
/// Note: You have to set the skins that you want to use ([defaultSkin],
|
||||
/// [downSkin], [hoverSkin], [disabledSkin], [defaultLabel]) in [onLoad]
|
||||
/// if you are not passing them in through the constructor.
|
||||
class AdvancedButtonComponent extends PositionComponent
|
||||
with HoverCallbacks, TapCallbacks {
|
||||
AdvancedButtonComponent({
|
||||
this.onPressed,
|
||||
this.onChangeState,
|
||||
PositionComponent? defaultSkin,
|
||||
PositionComponent? downSkin,
|
||||
PositionComponent? hoverSkin,
|
||||
PositionComponent? disabledSkin,
|
||||
PositionComponent? defaultLabel,
|
||||
PositionComponent? disabledLabel,
|
||||
super.size,
|
||||
super.position,
|
||||
super.scale,
|
||||
super.angle,
|
||||
super.anchor,
|
||||
super.children,
|
||||
super.priority,
|
||||
}) {
|
||||
this.defaultSkin = defaultSkin;
|
||||
this.downSkin = downSkin;
|
||||
this.hoverSkin = hoverSkin;
|
||||
this.disabledSkin = disabledSkin;
|
||||
this.defaultLabel = defaultLabel;
|
||||
this.disabledLabel = disabledLabel;
|
||||
size.addListener(_updateSizes);
|
||||
}
|
||||
|
||||
/// Callback for what should happen when the button is pressed.
|
||||
void Function()? onPressed;
|
||||
|
||||
/// Callback when button state changes
|
||||
void Function(ButtonState state)? onChangeState;
|
||||
|
||||
@mustCallSuper
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
super.onLoad();
|
||||
add(skinContainer);
|
||||
add(labelAlignContainer);
|
||||
}
|
||||
|
||||
@protected
|
||||
final skinContainer = Component();
|
||||
|
||||
@protected
|
||||
AlignComponent labelAlignContainer = AlignComponent(alignment: Anchor.center);
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void onMount() {
|
||||
super.onMount();
|
||||
assert(
|
||||
defaultSkin != null,
|
||||
'The defaultSkin has to either be passed '
|
||||
'in as an argument or set in onLoad',
|
||||
);
|
||||
if (_state.isDefault && !contains(defaultSkin!)) {
|
||||
defaultSkin!.parent = skinContainer;
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
bool isPressed = false;
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void onTapDown(TapDownEvent event) {
|
||||
if (_isDisabled) {
|
||||
return;
|
||||
}
|
||||
onPressed?.call();
|
||||
isPressed = true;
|
||||
updateState();
|
||||
}
|
||||
|
||||
@override
|
||||
void onTapUp(TapUpEvent event) {
|
||||
isPressed = false;
|
||||
updateState();
|
||||
}
|
||||
|
||||
@override
|
||||
void onHoverEnter() {
|
||||
updateState();
|
||||
}
|
||||
|
||||
@override
|
||||
void onHoverExit() {
|
||||
isPressed = false;
|
||||
updateState();
|
||||
}
|
||||
|
||||
Map<ButtonState, PositionComponent?> skinsMap = {};
|
||||
|
||||
PositionComponent? get defaultSkin => skinsMap[ButtonState.up];
|
||||
|
||||
set defaultSkin(PositionComponent? value) {
|
||||
skinsMap[ButtonState.up] = value;
|
||||
if (size.isZero()) {
|
||||
size = skinsMap[ButtonState.up]?.size ?? Vector2.zero();
|
||||
}
|
||||
invalidateSkins();
|
||||
}
|
||||
|
||||
set downSkin(PositionComponent? value) {
|
||||
skinsMap[ButtonState.down] = value;
|
||||
invalidateSkins();
|
||||
}
|
||||
|
||||
set hoverSkin(PositionComponent? value) {
|
||||
skinsMap[ButtonState.hover] = value;
|
||||
invalidateSkins();
|
||||
}
|
||||
|
||||
set disabledSkin(PositionComponent? value) {
|
||||
skinsMap[ButtonState.disabled] = value;
|
||||
invalidateSkins();
|
||||
}
|
||||
|
||||
Map<ButtonState, PositionComponent?> labelsMap = {};
|
||||
|
||||
PositionComponent? get defaultLabel => labelsMap[ButtonState.up];
|
||||
|
||||
set defaultLabel(PositionComponent? value) {
|
||||
labelsMap[ButtonState.up] = value;
|
||||
updateLabel();
|
||||
}
|
||||
|
||||
set disabledLabel(PositionComponent? value) {
|
||||
labelsMap[ButtonState.disabled] = value;
|
||||
updateLabel();
|
||||
}
|
||||
|
||||
@protected
|
||||
void invalidateSkins() {
|
||||
_updateSizes();
|
||||
_updateSkin();
|
||||
}
|
||||
|
||||
bool _isDisabled = false;
|
||||
|
||||
bool get isDisabled => _isDisabled;
|
||||
|
||||
set isDisabled(bool value) {
|
||||
if (_isDisabled == value) {
|
||||
return;
|
||||
}
|
||||
_isDisabled = value;
|
||||
updateState();
|
||||
}
|
||||
|
||||
void _updateSizes() {
|
||||
for (final skin in skinsMap.values) {
|
||||
skin?.size = size;
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void updateState() {
|
||||
if (isDisabled) {
|
||||
setState(ButtonState.disabled);
|
||||
return;
|
||||
}
|
||||
if (isPressed) {
|
||||
setState(ButtonState.down);
|
||||
return;
|
||||
}
|
||||
if (isHovered) {
|
||||
setState(ButtonState.hover);
|
||||
return;
|
||||
}
|
||||
setState(ButtonState.up);
|
||||
}
|
||||
|
||||
ButtonState _state = ButtonState.up;
|
||||
|
||||
@protected
|
||||
void setState(ButtonState value) {
|
||||
if (_state == value) {
|
||||
return;
|
||||
}
|
||||
_state = value;
|
||||
_updateSkin();
|
||||
updateLabel();
|
||||
onChangeState?.call(_state);
|
||||
}
|
||||
|
||||
void _updateSkin() {
|
||||
_removeSkins();
|
||||
setSkin(_state);
|
||||
}
|
||||
|
||||
@protected
|
||||
void setSkin(ButtonState state) {
|
||||
(skinsMap[state] ?? defaultSkin)?.parent = skinContainer;
|
||||
}
|
||||
|
||||
void _removeSkins() {
|
||||
for (final skins in skinsMap.values) {
|
||||
skins?.parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void updateLabel() {
|
||||
_removeLabels();
|
||||
addLabel(_state);
|
||||
}
|
||||
|
||||
@protected
|
||||
void addLabel(ButtonState state) {
|
||||
labelAlignContainer.child = labelsMap[state] ?? defaultLabel;
|
||||
}
|
||||
|
||||
void _removeLabels() {
|
||||
for (final label in labelsMap.values) {
|
||||
label?.parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
bool hasSkinForState(ButtonState state) {
|
||||
return skinsMap[state] != null;
|
||||
}
|
||||
}
|
||||
|
||||
enum ButtonState {
|
||||
up,
|
||||
upAndSelected,
|
||||
down,
|
||||
downAndSelected,
|
||||
hover,
|
||||
hoverAndSelected,
|
||||
disabled,
|
||||
disabledAndSelected;
|
||||
|
||||
const ButtonState();
|
||||
|
||||
bool get isDefault {
|
||||
return this == ButtonState.up;
|
||||
}
|
||||
|
||||
bool get isDefaultSelected {
|
||||
return this == ButtonState.upAndSelected;
|
||||
}
|
||||
|
||||
bool get isNotDefault {
|
||||
return !isDefault;
|
||||
}
|
||||
|
||||
bool get isDown {
|
||||
return this == ButtonState.down;
|
||||
}
|
||||
|
||||
bool get isDownAndSelected {
|
||||
return this == ButtonState.downAndSelected;
|
||||
}
|
||||
|
||||
bool get isHover {
|
||||
return this == ButtonState.hover;
|
||||
}
|
||||
|
||||
bool get isHoverAndSelected {
|
||||
return this == ButtonState.hoverAndSelected;
|
||||
}
|
||||
|
||||
bool get isDisabled {
|
||||
return this == ButtonState.disabled;
|
||||
}
|
||||
|
||||
bool get isDisabledAndSelected {
|
||||
return this == ButtonState.disabledAndSelected;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,167 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// The [ToggleButtonComponent] is an [AdvancedButtonComponent] that can switch
|
||||
/// between the selected and not selected state, imagine for example a switch
|
||||
/// widget or a tab that can be selected.
|
||||
///
|
||||
/// Note: You have to set the [defaultSkin], [defaultSelectedSkin]
|
||||
/// and other skins that you want to use in [onLoad] if you are not passed in
|
||||
/// through the constructor.
|
||||
class ToggleButtonComponent extends AdvancedButtonComponent {
|
||||
ToggleButtonComponent({
|
||||
super.onPressed,
|
||||
this.onSelectedChanged,
|
||||
super.onChangeState,
|
||||
super.defaultSkin,
|
||||
super.downSkin,
|
||||
super.hoverSkin,
|
||||
super.disabledSkin,
|
||||
PositionComponent? defaultSelectedSkin,
|
||||
PositionComponent? downAndSelectedSkin,
|
||||
PositionComponent? hoverAndSelectedSkin,
|
||||
PositionComponent? disabledAndSelectedSkin,
|
||||
super.defaultLabel,
|
||||
super.disabledLabel,
|
||||
PositionComponent? defaultSelectedLabel,
|
||||
PositionComponent? disabledAndSelectedLabel,
|
||||
super.size,
|
||||
super.position,
|
||||
super.scale,
|
||||
super.angle,
|
||||
super.anchor,
|
||||
super.children,
|
||||
super.priority,
|
||||
}) {
|
||||
this.defaultSelectedSkin = defaultSelectedSkin;
|
||||
this.downAndSelectedSkin = downAndSelectedSkin;
|
||||
this.hoverAndSelectedSkin = hoverAndSelectedSkin;
|
||||
this.disabledAndSelectedSkin = disabledAndSelectedSkin;
|
||||
this.defaultSelectedLabel = defaultSelectedLabel;
|
||||
this.disabledAndSelectedLabel = disabledAndSelectedLabel;
|
||||
}
|
||||
|
||||
/// Callback when button selected changed
|
||||
ValueChanged<bool>? onSelectedChanged;
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void onMount() {
|
||||
assert(
|
||||
defaultSelectedSkin != null,
|
||||
'The defaultSelectedSkin has to either be passed '
|
||||
'in as an argument or set in onLoad',
|
||||
);
|
||||
super.onMount();
|
||||
}
|
||||
|
||||
PositionComponent? get defaultSelectedSkin =>
|
||||
skinsMap[ButtonState.upAndSelected];
|
||||
|
||||
set defaultSelectedSkin(PositionComponent? value) {
|
||||
skinsMap[ButtonState.upAndSelected] = value;
|
||||
invalidateSkins();
|
||||
}
|
||||
|
||||
set downAndSelectedSkin(PositionComponent? value) {
|
||||
skinsMap[ButtonState.downAndSelected] = value;
|
||||
invalidateSkins();
|
||||
}
|
||||
|
||||
set hoverAndSelectedSkin(PositionComponent? value) {
|
||||
skinsMap[ButtonState.hoverAndSelected] = value;
|
||||
invalidateSkins();
|
||||
}
|
||||
|
||||
set disabledAndSelectedSkin(PositionComponent? value) {
|
||||
skinsMap[ButtonState.disabledAndSelected] = value;
|
||||
invalidateSkins();
|
||||
}
|
||||
|
||||
PositionComponent? get defaultSelectedLabel =>
|
||||
labelsMap[ButtonState.upAndSelected];
|
||||
|
||||
set defaultSelectedLabel(PositionComponent? value) {
|
||||
labelsMap[ButtonState.upAndSelected] = value;
|
||||
updateLabel();
|
||||
}
|
||||
|
||||
set disabledAndSelectedLabel(PositionComponent? value) {
|
||||
labelsMap[ButtonState.disabledAndSelected] = value;
|
||||
updateLabel();
|
||||
}
|
||||
|
||||
@override
|
||||
void onTapUp(TapUpEvent event) {
|
||||
isSelected = !_isSelected;
|
||||
super.onTapUp(event);
|
||||
}
|
||||
|
||||
bool _isSelected = false;
|
||||
|
||||
bool get isSelected => _isSelected;
|
||||
|
||||
set isSelected(bool value) {
|
||||
if (_isSelected == value) {
|
||||
return;
|
||||
}
|
||||
_isSelected = value;
|
||||
updateState();
|
||||
onSelectedChanged?.call(_isSelected);
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
void setSkin(ButtonState state) {
|
||||
var skin = skinsMap[state];
|
||||
if (state.isDisabledAndSelected && !hasSkinForState(state)) {
|
||||
skin = skinsMap[ButtonState.disabled];
|
||||
}
|
||||
if (state.isDownAndSelected && !hasSkinForState(state)) {
|
||||
skin = skinsMap[ButtonState.down];
|
||||
}
|
||||
if (state.isHoverAndSelected && !hasSkinForState(state)) {
|
||||
skin = skinsMap[ButtonState.hover];
|
||||
}
|
||||
if (state.isDownAndSelected && !hasSkinForState(state)) {
|
||||
skin = skinsMap[ButtonState.down];
|
||||
}
|
||||
skin = skin ?? (isSelected ? defaultSelectedSkin : defaultSkin);
|
||||
skin?.parent = skinContainer;
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
void addLabel(ButtonState state) {
|
||||
labelAlignContainer.child =
|
||||
labelsMap[state] ?? (isSelected ? defaultSelectedLabel : defaultLabel);
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
@protected
|
||||
@override
|
||||
void updateState() {
|
||||
if (isDisabled) {
|
||||
setState(
|
||||
_isSelected ? ButtonState.disabledAndSelected : ButtonState.disabled,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isPressed) {
|
||||
setState(
|
||||
_isSelected ? ButtonState.downAndSelected : ButtonState.down,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isHovered) {
|
||||
setState(
|
||||
_isSelected ? ButtonState.hoverAndSelected : ButtonState.hover,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(
|
||||
_isSelected ? ButtonState.upAndSelected : ButtonState.up,
|
||||
);
|
||||
}
|
||||
}
|
||||
BIN
packages/flame/test/_goldens/advanced_button_component.png
Normal file
BIN
packages/flame/test/_goldens/advanced_button_component.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 199 B |
@ -0,0 +1,221 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('AdvancedButtonComponent', () {
|
||||
testGolden(
|
||||
'label renders correctly',
|
||||
(game) async {
|
||||
await game.add(
|
||||
AdvancedButtonComponent(
|
||||
defaultSkin: RectangleComponent(size: Vector2(40, 20)),
|
||||
defaultLabel: RectangleComponent(
|
||||
size: Vector2(10, 5),
|
||||
paint: Paint()..color = const Color(0xFFFF0000),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
size: Vector2(50, 30),
|
||||
goldenFile: '../_goldens/advanced_button_component.png',
|
||||
);
|
||||
|
||||
testWithFlameGame('correctly registers taps', (game) async {
|
||||
var pressedTimes = 0;
|
||||
final initialGameSize = Vector2.all(200);
|
||||
final componentSize = Vector2.all(10);
|
||||
final buttonPosition = Vector2.all(100);
|
||||
late final AdvancedButtonComponent button;
|
||||
game.onGameResize(initialGameSize);
|
||||
await game.ensureAdd(
|
||||
button = AdvancedButtonComponent(
|
||||
defaultSkin: RectangleComponent(size: componentSize),
|
||||
onPressed: () => pressedTimes++,
|
||||
position: buttonPosition,
|
||||
size: componentSize,
|
||||
),
|
||||
);
|
||||
|
||||
expect(pressedTimes, 0);
|
||||
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
|
||||
|
||||
tapDispatcher.handleTapDown(1, TapDownDetails());
|
||||
expect(pressedTimes, 0);
|
||||
|
||||
tapDispatcher.handleTapUp(
|
||||
1,
|
||||
createTapUpDetails(
|
||||
globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),
|
||||
),
|
||||
);
|
||||
expect(pressedTimes, 0);
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: buttonPosition.toOffset()),
|
||||
);
|
||||
expect(pressedTimes, 1);
|
||||
|
||||
tapDispatcher.handleTapUp(
|
||||
1,
|
||||
createTapUpDetails(globalPosition: buttonPosition.toOffset()),
|
||||
);
|
||||
expect(pressedTimes, 1);
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: buttonPosition.toOffset()),
|
||||
);
|
||||
tapDispatcher.handleTapCancel(1);
|
||||
expect(pressedTimes, 2);
|
||||
});
|
||||
|
||||
testWithFlameGame('correctly registers taps onGameResize', (game) async {
|
||||
var pressedTimes = 0;
|
||||
final initialGameSize = Vector2.all(100);
|
||||
final componentSize = Vector2.all(10);
|
||||
final buttonPosition = Vector2.all(100);
|
||||
late final AdvancedButtonComponent button;
|
||||
game.onGameResize(initialGameSize);
|
||||
await game.ensureAdd(
|
||||
button = AdvancedButtonComponent(
|
||||
defaultSkin: RectangleComponent(size: componentSize),
|
||||
onPressed: () => pressedTimes++,
|
||||
position: buttonPosition,
|
||||
size: componentSize,
|
||||
),
|
||||
);
|
||||
final previousPosition =
|
||||
button.positionOfAnchor(Anchor.center).toOffset();
|
||||
game.onGameResize(initialGameSize * 2);
|
||||
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(pressedTimes, 1);
|
||||
|
||||
tapDispatcher.handleTapUp(
|
||||
1,
|
||||
createTapUpDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(pressedTimes, 1);
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: previousPosition),
|
||||
);
|
||||
tapDispatcher.handleTapCancel(1);
|
||||
expect(pressedTimes, 2);
|
||||
});
|
||||
|
||||
testWithFlameGame('correctly work isDisabled', (game) async {
|
||||
var pressedTimes = 0;
|
||||
final initialGameSize = Vector2.all(100);
|
||||
final componentSize = Vector2.all(10);
|
||||
final buttonPosition = Vector2.all(100);
|
||||
late final AdvancedButtonComponent button;
|
||||
game.onGameResize(initialGameSize);
|
||||
await game.ensureAdd(
|
||||
button = AdvancedButtonComponent(
|
||||
defaultSkin: RectangleComponent(size: componentSize),
|
||||
onPressed: () => pressedTimes++,
|
||||
position: buttonPosition,
|
||||
size: componentSize,
|
||||
),
|
||||
);
|
||||
button.isDisabled = true;
|
||||
final previousPosition =
|
||||
button.positionOfAnchor(Anchor.center).toOffset();
|
||||
game.onGameResize(initialGameSize * 2);
|
||||
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(pressedTimes, 0);
|
||||
|
||||
tapDispatcher.handleTapUp(
|
||||
1,
|
||||
createTapUpDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(pressedTimes, 0);
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: previousPosition),
|
||||
);
|
||||
tapDispatcher.handleTapCancel(1);
|
||||
expect(pressedTimes, 0);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'[#1723] can be pressed while the engine is paused',
|
||||
(tester) async {
|
||||
final game = FlameGame();
|
||||
game.add(
|
||||
AdvancedButtonComponent(
|
||||
defaultSkin: CircleComponent(radius: 40),
|
||||
downSkin: CircleComponent(radius: 40),
|
||||
position: Vector2(400, 300),
|
||||
anchor: Anchor.center,
|
||||
onPressed: () {
|
||||
game.pauseEngine();
|
||||
game.overlays.add('pause-menu');
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
GameWidget(
|
||||
game: game,
|
||||
overlayBuilderMap: {
|
||||
'pause-menu': (context, _) {
|
||||
return SimpleStatelessWidget(
|
||||
build: (context) {
|
||||
return Center(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
game.overlays.remove('pause-menu');
|
||||
game.resumeEngine();
|
||||
},
|
||||
child: const Text('Resume'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tapAt(const Offset(400, 300));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(game.paused, true);
|
||||
|
||||
await tester.tapAt(const Offset(400, 300));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(game.paused, false);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class SimpleStatelessWidget extends StatelessWidget {
|
||||
const SimpleStatelessWidget({
|
||||
required Widget Function(BuildContext) build,
|
||||
super.key,
|
||||
}) : _build = build;
|
||||
|
||||
final Widget Function(BuildContext) _build;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _build(context);
|
||||
}
|
||||
254
packages/flame/test/components/toogle_button_component_test.dart
Normal file
254
packages/flame/test/components/toogle_button_component_test.dart
Normal file
@ -0,0 +1,254 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('ToggleButtonComponent', () {
|
||||
testWithFlameGame('correctly registers taps', (game) async {
|
||||
var pressedTimes = 0;
|
||||
final initialGameSize = Vector2.all(200);
|
||||
final componentSize = Vector2.all(10);
|
||||
final buttonPosition = Vector2.all(100);
|
||||
late final ToggleButtonComponent button;
|
||||
game.onGameResize(initialGameSize);
|
||||
await game.ensureAdd(
|
||||
button = ToggleButtonComponent(
|
||||
defaultSkin: RectangleComponent(size: componentSize),
|
||||
defaultSelectedSkin: RectangleComponent(size: componentSize),
|
||||
onPressed: () => pressedTimes++,
|
||||
position: buttonPosition,
|
||||
size: componentSize,
|
||||
),
|
||||
);
|
||||
|
||||
expect(pressedTimes, 0);
|
||||
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
|
||||
|
||||
tapDispatcher.handleTapDown(1, TapDownDetails());
|
||||
expect(pressedTimes, 0);
|
||||
|
||||
tapDispatcher.handleTapUp(
|
||||
1,
|
||||
createTapUpDetails(
|
||||
globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),
|
||||
),
|
||||
);
|
||||
expect(pressedTimes, 0);
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: buttonPosition.toOffset()),
|
||||
);
|
||||
expect(pressedTimes, 1);
|
||||
|
||||
tapDispatcher.handleTapUp(
|
||||
1,
|
||||
createTapUpDetails(globalPosition: buttonPosition.toOffset()),
|
||||
);
|
||||
expect(pressedTimes, 1);
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: buttonPosition.toOffset()),
|
||||
);
|
||||
tapDispatcher.handleTapCancel(1);
|
||||
expect(pressedTimes, 2);
|
||||
});
|
||||
|
||||
testWithFlameGame('correctly registers taps onGameResize', (game) async {
|
||||
var pressedTimes = 0;
|
||||
final initialGameSize = Vector2.all(100);
|
||||
final componentSize = Vector2.all(10);
|
||||
final buttonPosition = Vector2.all(100);
|
||||
late final ToggleButtonComponent button;
|
||||
game.onGameResize(initialGameSize);
|
||||
await game.ensureAdd(
|
||||
button = ToggleButtonComponent(
|
||||
defaultSkin: RectangleComponent(size: componentSize),
|
||||
defaultSelectedSkin: RectangleComponent(size: componentSize),
|
||||
onPressed: () => pressedTimes++,
|
||||
position: buttonPosition,
|
||||
size: componentSize,
|
||||
),
|
||||
);
|
||||
final previousPosition =
|
||||
button.positionOfAnchor(Anchor.center).toOffset();
|
||||
game.onGameResize(initialGameSize * 2);
|
||||
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(pressedTimes, 1);
|
||||
|
||||
tapDispatcher.handleTapUp(
|
||||
1,
|
||||
createTapUpDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(pressedTimes, 1);
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: previousPosition),
|
||||
);
|
||||
tapDispatcher.handleTapCancel(1);
|
||||
expect(pressedTimes, 2);
|
||||
});
|
||||
|
||||
testWithFlameGame('correctly work isDisabled', (game) async {
|
||||
var pressedTimes = 0;
|
||||
final initialGameSize = Vector2.all(100);
|
||||
final componentSize = Vector2.all(10);
|
||||
final buttonPosition = Vector2.all(100);
|
||||
late final ToggleButtonComponent button;
|
||||
game.onGameResize(initialGameSize);
|
||||
await game.ensureAdd(
|
||||
button = ToggleButtonComponent(
|
||||
defaultSkin: RectangleComponent(size: componentSize),
|
||||
defaultSelectedSkin: RectangleComponent(size: componentSize),
|
||||
onPressed: () => pressedTimes++,
|
||||
position: buttonPosition,
|
||||
size: componentSize,
|
||||
),
|
||||
);
|
||||
button.isDisabled = true;
|
||||
final previousPosition =
|
||||
button.positionOfAnchor(Anchor.center).toOffset();
|
||||
game.onGameResize(initialGameSize * 2);
|
||||
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(pressedTimes, 0);
|
||||
|
||||
tapDispatcher.handleTapUp(
|
||||
1,
|
||||
createTapUpDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(pressedTimes, 0);
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: previousPosition),
|
||||
);
|
||||
tapDispatcher.handleTapCancel(1);
|
||||
expect(pressedTimes, 0);
|
||||
});
|
||||
|
||||
testWithFlameGame('toggle works correctly', (game) async {
|
||||
var pressedTimes = 0;
|
||||
final initialGameSize = Vector2.all(100);
|
||||
final componentSize = Vector2.all(10);
|
||||
final buttonPosition = Vector2.all(100);
|
||||
late final ToggleButtonComponent button;
|
||||
game.onGameResize(initialGameSize);
|
||||
await game.ensureAdd(
|
||||
button = ToggleButtonComponent(
|
||||
defaultSkin: RectangleComponent(size: componentSize),
|
||||
defaultSelectedSkin: RectangleComponent(size: componentSize),
|
||||
onPressed: () => pressedTimes++,
|
||||
position: buttonPosition,
|
||||
size: componentSize,
|
||||
),
|
||||
);
|
||||
final previousPosition =
|
||||
button.positionOfAnchor(Anchor.center).toOffset();
|
||||
game.onGameResize(initialGameSize * 2);
|
||||
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(button.isSelected, false);
|
||||
|
||||
tapDispatcher.handleTapUp(
|
||||
1,
|
||||
createTapUpDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(button.isSelected, true);
|
||||
|
||||
tapDispatcher.handleTapDown(
|
||||
1,
|
||||
TapDownDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(button.isSelected, true);
|
||||
|
||||
tapDispatcher.handleTapUp(
|
||||
1,
|
||||
createTapUpDetails(globalPosition: previousPosition),
|
||||
);
|
||||
expect(button.isSelected, false);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'[#1723] can be pressed while the engine is paused',
|
||||
(tester) async {
|
||||
final game = FlameGame();
|
||||
game.add(
|
||||
ToggleButtonComponent(
|
||||
defaultSkin: CircleComponent(radius: 40),
|
||||
downSkin: CircleComponent(radius: 40),
|
||||
defaultSelectedSkin: CircleComponent(radius: 40),
|
||||
position: Vector2(400, 300),
|
||||
anchor: Anchor.center,
|
||||
onPressed: () {
|
||||
game.pauseEngine();
|
||||
game.overlays.add('pause-menu');
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
GameWidget(
|
||||
game: game,
|
||||
overlayBuilderMap: {
|
||||
'pause-menu': (context, _) {
|
||||
return SimpleStatelessWidget(
|
||||
build: (context) {
|
||||
return Center(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
game.overlays.remove('pause-menu');
|
||||
game.resumeEngine();
|
||||
},
|
||||
child: const Text('Resume'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tapAt(const Offset(400, 300));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(game.paused, true);
|
||||
|
||||
await tester.tapAt(const Offset(400, 300));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(game.paused, false);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class SimpleStatelessWidget extends StatelessWidget {
|
||||
const SimpleStatelessWidget({
|
||||
required Widget Function(BuildContext) build,
|
||||
super.key,
|
||||
}) : _build = build;
|
||||
|
||||
final Widget Function(BuildContext) _build;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _build(context);
|
||||
}
|
||||
Reference in New Issue
Block a user