Search code examples
javajavafxflowpane

honeycomb layout in javafx (with flowpane?)


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

Solution

  • Predetermined columns/ rows

    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 Buttons 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 behavior

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