I'm trying to make a honeycomb flow with buttons in JavaFX with so far a FlowPane
So far I got it half working by using a negative VGap on my FlowPane, but as soon as I resize it and make the 3-4 row go to 3-3-1 it obviously goes wrong
I'm trying to find a solution that will keep the honeycomb layout with variable amounts of buttons, but so far I havent had much success. So I was wondering if anyone knows a solution for this.
edit: here is the basic code I have at the moment
FlowPane root = new FlowPane();
root.setHgap(3.0); root.setVgap(-23.0);
root.setAlignment(Pos.CENTER);
Button[] array = new Button[7];
for(int i = 0; i <= 6; i++){
Button button = new Button();
button.setShape(polygon); (made a polygon in the same of a hexagon)
array[i] = button;
}
root.getChildren().addAll(array);
This is a bit more convenient than using a FlowPane
to place the fields.
You can observe that using column spans you can place the Button
s in a GridPane
: Every field fills 2 columns of sqrt(3/4)
times the field height; the odd/even rows start at column 0/1 respectively. Every field fills 3 rows and the size of the column constraints alternate between one quarter and one half of the field height.
Example
public static GridPane createHoneyComb(int rows, int columns, double size) {
double[] points = new double[12];
for (int i = 0; i < 12; i += 2) {
double angle = Math.PI * (0.5 + i / 6d);
points[i] = Math.cos(angle);
points[i + 1] = Math.sin(angle);
}
Polygon polygon = new Polygon(points);
GridPane result = new GridPane();
RowConstraints rc1 = new RowConstraints(size / 4);
rc1.setFillHeight(true);
RowConstraints rc2 = new RowConstraints(size / 2);
rc2.setFillHeight(true);
double width = Math.sqrt(0.75) * size;
ColumnConstraints cc = new ColumnConstraints(width/2);
cc.setFillWidth(true);
for (int i = 0; i < columns; i++) {
result.getColumnConstraints().addAll(cc, cc);
}
for (int r = 0; r < rows; r++) {
result.getRowConstraints().addAll(rc1, rc2);
int offset = r % 2;
int count = columns - offset;
for (int c = 0; c < count; c++) {
Button b = new Button();
b.setPrefSize(width, size);
b.setShape(polygon);
result.add(b, 2 * c + offset, 2 * r, 2, 3);
}
}
result.getRowConstraints().add(rc1);
return result;
}
FlowPane
-like behaviorMaking the x position depend on the row the child is added is not a good idea in a FlowPane
. Instead I recommend extending Pane
and overriding layoutChildren
method place the children at custom positions.
In your case the following class could be used:
public class OffsetPane extends Pane {
public interface PositionFunction {
public Point2D getNextPosition(int index, double x, double y, double width, double height);
}
private static final PositionFunction DEFAULT_FUNCTION = new PositionFunction() {
@Override
public Point2D getNextPosition(int index, double x, double y, double width, double height) {
return new Point2D(x, y);
}
};
private final ObjectProperty<PositionFunction> hPositionFunction;
private final ObjectProperty<PositionFunction> vPositionFunction;
private ObjectProperty<PositionFunction> createPosProperty(String name) {
return new SimpleObjectProperty<PositionFunction>(this, name, DEFAULT_FUNCTION) {
@Override
public void set(PositionFunction newValue) {
if (newValue == null) {
throw new IllegalArgumentException();
} else if (get() != newValue) {
super.set(newValue);
requestLayout();
}
}
};
}
public OffsetPane() {
this.hPositionFunction = createPosProperty("hPositionFunction");
this.vPositionFunction = createPosProperty("vPositionFunction");
}
@Override
protected void layoutChildren() {
super.layoutChildren();
double width = getWidth();
List<Node> children = getManagedChildren();
final int childSize = children.size();
if (childSize > 0) {
int row = 0;
Node lastRowStart = children.get(0);
Node lastNode = lastRowStart;
lastRowStart.relocate(0, 0);
PositionFunction hFunc = getHPositionFunction();
PositionFunction vFunc = getVPositionFunction();
int index = 1;
int columnIndex = 0;
while (index < childSize) {
Node child = children.get(index);
Bounds lastBounds = lastNode.getLayoutBounds();
Bounds bounds = child.getLayoutBounds();
Point2D pt = hFunc.getNextPosition(columnIndex, lastNode.getLayoutX(), lastNode.getLayoutY(), lastBounds.getWidth(), lastBounds.getHeight());
if (pt.getX() + bounds.getWidth() > width) {
// break row
lastBounds = lastRowStart.getLayoutBounds();
pt = vFunc.getNextPosition(row, lastRowStart.getLayoutX(), lastRowStart.getLayoutY(), lastBounds.getWidth(), lastBounds.getHeight());
child.relocate(pt.getX(), pt.getY());
lastRowStart = child;
row++;
columnIndex = 0;
} else {
child.relocate(pt.getX(), pt.getY());
columnIndex++;
}
lastNode = child;
index++;
}
}
}
public final PositionFunction getHPositionFunction() {
return this.hPositionFunction.get();
}
public final void setHPositionFunction(PositionFunction value) {
this.hPositionFunction.set(value);
}
public final ObjectProperty<PositionFunction> hPositionFunctionProperty() {
return this.hPositionFunction;
}
public final PositionFunction getVPositionFunction() {
return this.vPositionFunction.get();
}
public final void setVPositionFunction(PositionFunction value) {
this.vPositionFunction.set(value);
}
public final ObjectProperty<PositionFunction> vPositionFunctionProperty() {
return this.vPositionFunction;
}
}
double[] points = new double[12];
for (int i = 0; i < 12; i += 2) {
double angle = Math.PI * (0.5 + i / 6d);
points[i] = Math.cos(angle);
points[i + 1] = Math.sin(angle);
}
Polygon polygon = new Polygon(points);
OffsetPane op = new OffsetPane();
double fieldHeight = 100;
double fieldWidth = Math.sqrt(0.75) * fieldHeight;
for (int i = 0; i < 23; i++) {
Button button = new Button();
button.setShape(polygon);
button.setPrefSize(fieldWidth, fieldHeight);
op.getChildren().add(button);
}
// horizontal placement just right of the last element
op.setHPositionFunction((int index, double x, double y, double width, double height) -> new Point2D(x + width, y));
// vertical position half the size left/right depending on index and
// 1/4 the node height above the bottom of the last node
op.setVPositionFunction((int index, double x, double y, double width, double height) -> new Point2D(x + (index % 2 == 0 ? width : -width) / 2, y + height * 0.75));