flame
flame copied to clipboard
Row and Column for Flame Components
Problem to solve
With Row
and Column
classes children Component
s could be placed and layouted correctly without overlapping and without having to care about repeating size and position calculations.
In my games i often have Component
s (Sprites, buttons, etc.) which have to be placed either in rows or columns, where it's always quite an effort to calculate their positions and sizes.
The goal of the suggested two classes would be to make these calculations transparent for the developer.
Proposal
Flutter-like Row
and Column
classes with alignment options for both axes (i.e. mainAxisAlignment
and crossAxisAlignment
).
Some ideas:
- the new classes could extend
PositionComponent
themselves - children can possibly be all
PositionComponent
extending classes (since getting their size is important) - adding a child dynamically to a
Row
orColumn
would need to re-layout the component - another new class for adding gaps would make sense too (like
SizedBox
) - ok this can be accomplished by just adding aPositionComponent
with asize
defined
Sgtm, do you want to work on this?
@spydon I don't have that much spare time at the moment, therefore rather no, sorry.
I've a few things on my queue, but I could try tackling this after clear some space on it
Here are my first attempts:
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/widgets.dart';
enum Direction { horizontal, vertical }
/// Super class for [ComponentRow] and [ComponentColumn]
abstract class LayoutComponent extends PositionComponent with HasGameRef {
LayoutComponent(this.direction, this.mainAxisAlignment);
final Direction direction;
MainAxisAlignment mainAxisAlignment;
}
/// Allows laying out children in a row by defining an [MainAxisAlignment] type.
/// A relayout is performed when
/// - a new child is added
/// - an existing child changes its size
class ComponentRow extends LayoutComponent {
ComponentRow({MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start})
: super(Direction.horizontal, mainAxisAlignment);
@override
Future<void>? add(Component component) {
assert(
component is PositionComponent,
"The added component has to be a child of PositionComponent",
);
(component as PositionComponent).size.addListener(() {
_layoutChildren();
});
component.mounted.then((_) => _layoutChildren());
return super.add(component);
}
@override
void remove(Component component) {
assert(
contains(component),
"This component is not a child of this class",
);
(component as PositionComponent).size.removeListener(_layoutChildren);
super.remove(component);
// hack which needs to be resolved
Future.delayed(const Duration(milliseconds: 50), () {
_layoutChildren();
});
}
void _layoutChildren() {
final list = children.whereType<PositionComponent>().toList();
Vector2 currentPosition = Vector2.zero();
double componentsWidth =
list.fold(0, (previousValue, element) => previousValue + element.width);
final widthAvailable = size.x != 0.0
? size.x - absoluteTopLeftPosition.x
: gameRef.canvasSize.x - absoluteTopLeftPosition.x;
if (mainAxisAlignment == MainAxisAlignment.start) {
for (var child in list) {
child.position = Vector2(currentPosition.x, currentPosition.y);
currentPosition.x += child.width;
}
} else if (mainAxisAlignment == MainAxisAlignment.end) {
for (var child in list.reversed) {
currentPosition.x -= child.width;
child.position = Vector2(currentPosition.x, currentPosition.y);
}
} else if (mainAxisAlignment == MainAxisAlignment.spaceBetween) {
final freeSpacePerComponent =
(widthAvailable - componentsWidth) / list.length;
for (var child in list) {
child.position = Vector2(currentPosition.x, currentPosition.y);
currentPosition.x += (freeSpacePerComponent + child.width);
}
} else if (mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
final freeSpacePerComponent =
(widthAvailable - componentsWidth) / (list.length + 2);
currentPosition.x += freeSpacePerComponent;
for (var child in list) {
child.position = Vector2(currentPosition.x, currentPosition.y);
currentPosition.x += (freeSpacePerComponent + child.width);
}
} else if (mainAxisAlignment == MainAxisAlignment.spaceAround) {
final freeSpacePerComponent =
(widthAvailable - componentsWidth) / (list.length + 1);
currentPosition.x += freeSpacePerComponent / 2;
for (var child in list) {
child.position = Vector2(currentPosition.x, currentPosition.y);
currentPosition.x += (freeSpacePerComponent + child.width);
}
} else if (mainAxisAlignment == MainAxisAlignment.center) {
final freeSpace = widthAvailable - componentsWidth;
currentPosition.x += freeSpace / 2;
for (var child in list) {
child.position = Vector2(currentPosition.x, currentPosition.y);
currentPosition.x += child.width;
}
}
}
}
It can be used like this:
final row = ComponentRow(mainAxisAlignment: MainAxisAlignment.start)
//..size = Vector2(500, 200)
..position = Vector2(50,50);
add(row);
row.add(
TextComponent(text: 'One',)
..textRenderer = TextPaint(
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w600,
))
);
row.add(button);
row.add(PositionComponent(size: Vector2(20,0))); // gap
row.add(TextComponent(text: 'Two',)
..textRenderer = TextPaint(
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w800, color: Colors.yellow
))
);
I'm sure there are lots of things i didn't think of and which need to be considered. But hopefully it's a start.
Now supports a constant gap between the children which can be changed dynamically.
/// Super class for [ComponentRow] and [ComponentColumn]
abstract class LayoutComponent extends PositionComponent with HasGameRef {
LayoutComponent(this.direction, this.mainAxisAlignment, this._gap);
final Direction direction;
final MainAxisAlignment mainAxisAlignment;
/// gap between components
double _gap;
set gap(double gap) {
_gap = gap;
layoutChildren();
}
double get gap => _gap;
@override
Future<void>? add(Component component) {
assert(
component is PositionComponent,
"The added component has to be a child of PositionComponent",
);
(component as PositionComponent).size.addListener(() {
layoutChildren();
});
component.mounted.then((_) => layoutChildren());
return super.add(component);
}
@override
void remove(Component component) {
assert(
contains(component),
"This component is not a child of this class",
);
(component as PositionComponent).size.removeListener(layoutChildren);
super.remove(component);
// hack which needs to be resolved
// https://github.com/flame-engine/flame/issues/1956
Future.delayed(const Duration(milliseconds: 50), () {
layoutChildren();
});
}
@protected
void layoutChildren();
}
/// Allows laying out children in a row by defining a [MainAxisAlignment] type.
/// A relayout is performed when
/// - a new child is added
/// - an existing child changes its size
/// - the [gap] parameter is changed
class ComponentRow extends LayoutComponent {
ComponentRow({
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
double gap = 0.0,
}) : super(Direction.horizontal, mainAxisAlignment, gap);
@override
void layoutChildren() {
final list = children.whereType<PositionComponent>().toList();
Vector2 currentPosition = Vector2.zero();
double componentsWidth =
list.fold(0, (previousValue, element) => previousValue + element.width);
double gapWidth = gap * (list.length - 1);
final widthAvailable = size.x != 0.0
? size.x - absoluteTopLeftPosition.x
: gameRef.canvasSize.x - absoluteTopLeftPosition.x;
if (mainAxisAlignment == MainAxisAlignment.start) {
for (var child in list) {
child.position = Vector2(currentPosition.x, currentPosition.y);
currentPosition.x += (child.width + gap);
}
} else if (mainAxisAlignment == MainAxisAlignment.end) {
for (var child in list.reversed) {
currentPosition.x -= (child.width + gap);
child.position = Vector2(currentPosition.x, currentPosition.y);
}
} else if (mainAxisAlignment == MainAxisAlignment.spaceBetween) {
final freeSpacePerComponent =
(widthAvailable - componentsWidth - gapWidth) / list.length;
for (var child in list) {
child.position = Vector2(currentPosition.x, currentPosition.y);
currentPosition.x += (freeSpacePerComponent + child.width + gap);
}
} else if (mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
final freeSpacePerComponent =
(widthAvailable - componentsWidth - gapWidth) / (list.length + 2);
currentPosition.x += freeSpacePerComponent;
for (var child in list) {
child.position = Vector2(currentPosition.x, currentPosition.y);
currentPosition.x += (freeSpacePerComponent + child.width + gap);
}
} else if (mainAxisAlignment == MainAxisAlignment.spaceAround) {
final freeSpacePerComponent =
(widthAvailable - componentsWidth - gapWidth) / (list.length + 1);
currentPosition.x += freeSpacePerComponent / 2;
for (var child in list) {
child.position = Vector2(currentPosition.x, currentPosition.y);
currentPosition.x += (freeSpacePerComponent + child.width + gap);
}
} else if (mainAxisAlignment == MainAxisAlignment.center) {
final freeSpace = widthAvailable - componentsWidth - gapWidth;
currentPosition.x += freeSpace / 2;
for (var child in list) {
child.position = Vector2(currentPosition.x, currentPosition.y);
currentPosition.x += (child.width + gap);
}
}
}
}
enum Direction { horizontal, vertical }
To be used like this:
row = ComponentRow(mainAxisAlignment: MainAxisAlignment.center, gap: 20.0)
//..size = Vector2(500, 200)
..position = Vector2(50,50);
add(row);
row.add(
TextComponent(text: 'One',)
..textRenderer = TextPaint(
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w600,
))
);
row.add(button);
//row.add(PositionComponent(size: Vector2(20,0)));
row.add(TextComponent(text: 'Two',)
..textRenderer = TextPaint(
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w800, color: Colors.yellow
))
);
Future.delayed(const Duration(milliseconds: 2200), () {
row.gap = 50;
});
Can we rename the Component to be RowComponent instead of ComponentRow?
That way it follows the current naming convention.
@alestiago Done
Is this feature available?
@mrbeardad Not really. At the moment there's only AlignComponent available, where additional layout components shall be added later.
Waiting for use now....
interesting... +1