Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Row and Column for Flame Components #1944

Open
rivella50 opened this issue Sep 27, 2022 · 11 comments
Open

Row and Column for Flame Components #1944

rivella50 opened this issue Sep 27, 2022 · 11 comments
Assignees

Comments

@rivella50
Copy link
Contributor

rivella50 commented Sep 27, 2022

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 PositionComponent themselves
  • children can possibly be all PositionComponent extending classes (since getting their size is important)
  • adding a child dynamically to a Row or Column 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 a PositionComponent with a size defined
@spydon
Copy link
Member

spydon commented Sep 27, 2022

Sgtm, do you want to work on this?

@rivella50
Copy link
Contributor Author

@spydon I don't have that much spare time at the moment, therefore rather no, sorry.

@erickzanardo
Copy link
Member

I've a few things on my queue, but I could try tackling this after clear some space on it

@rivella50
Copy link
Contributor Author

rivella50 commented Sep 29, 2022

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.

@rivella50
Copy link
Contributor Author

rivella50 commented Sep 30, 2022

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;
});

@alestiago
Copy link
Contributor

Can we rename the Component to be RowComponent instead of ComponentRow?

That way it follows the current naming convention.

@rivella50
Copy link
Contributor Author

@alestiago Done

spydon pushed a commit that referenced this issue Oct 4, 2022
In order to get notified about a change in a parent's children this new function will be called now.
This feature will help simplifying e.g. the implementation of issue #1944 (Row and Column components).
@mrbeardad
Copy link

Is this feature available?

@rivella50
Copy link
Contributor Author

@mrbeardad Not really. At the moment there's only AlignComponent available, where additional layout components shall be added later.

@HaiboLee
Copy link

HaiboLee commented Aug 1, 2023

Waiting for use now....

@NashIlli
Copy link
Contributor

NashIlli commented Jan 5, 2024

interesting... +1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants