Row and Column for Flame Components
Problem to solve
With Row and Column classes children Components 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 Components (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
PositionComponentthemselves - children can possibly be all
PositionComponentextending classes (since getting their size is important) - adding a child dynamically to a
RoworColumnwould 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 aPositionComponentwith asizedefined
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