thanks for reading my question.
I'm currently working with JavaFX-8, SceneBuilder and Eclipse.
I want to do a scatter chart with four quadrants, that has two fixed number axis (the data position is not relevant, I only need to display the dots on each quadrant... only matters in which quadrant a dot is). Each quadrant must have a background with a specific color.
I found this question, so I tried to extend ScatterChart with the aim of overriding the method layoutPlotChildren(). I tried a minimum implementation to see if it will run with my FXML (I did import the new component to the FXML). This was my minimum implementation:
public class ScatterQuadrantChart<X,Y> extends ScatterChart<X,Y> {
public ScatterQuadrantChart(Axis<X> xAxis, Axis<Y> yAxis) {
super(xAxis, yAxis);
} }
And then, I get the NotSuchMethodError init error. I found a similar error but from someone extending LineChart here, but I'm not quite sure of what I need to do on my own class.
I tried adding a no-parameters constructor, but I need to call super and cant because I can't call the "getXAxis()" method either. What should I do here?
Plus, the other issue that remains is, once I solve this, what should the layoutPlotChildren() method do?
Thanks for reading.
The problem you are seeing is arising because the default mechanism for the FXMLLoader to instantiate a class is to call the no-argument constructor. Your ScatterQuadrantChart has no no-argument constructor, hence the NoSuchMethodError.
Prior to Java 8, the only way to fix this was to create a builder class for your class, as in the post you linked. JavaFX 8 introduced (but failed to document) a mechanism to specify values for constructor parameters that would be recognized by the FXMLLoader, using the #NamedArg annotation).
So, in Java 8, you can modify your ScatterQuadrantChart:
public class ScatterQuadrantChart<X,Y> extends ScatterChart<X,Y> {
public ScatterQuadrantChart(#NamedArg("xAxis")Axis<X> xAxis,
#NamedArg("yAxis)Axis<Y> yAxis) {
super(xAxis, yAxis);
}
}
and then your FXML will look like
<ScatterQuadrantChart>
<xAxis>
<NumberAxis ... />
</xAxis>
<yAxis>
<NumberAxis ... />
</yAxis>
</ScatterQuadrantChart>
I have no idea if or how SceneBuilder will interact with this, but the FXML will work.
As for the implementation, you will need to add some nodes to the plot to represent your quadrants. I would probably just use plain regions for these. Create them in the constructor and call getPlotChildren().add(...) to add them. Then in the layoutPlotChildren() method, first call the superclass method (which will lay out the scatter chart nodes), and then resize and reposition the quadrants. You can use getXAxis().getDisplayPosition(...) to figure out the location from the actual divider value.
In real life, you should add style classes to the quadrants so you can style them externally with css, etc, but a very basic implementation might look like
import javafx.beans.NamedArg;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.scene.chart.Axis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.layout.Region;
public class ScatterQuadrantChart<X,Y> extends ScatterChart<X,Y> {
private final Property<X> xQuadrantDivider = new SimpleObjectProperty<>();
private final Property<Y> yQuadrantDivider = new SimpleObjectProperty<>();
private final Region nwQuad ;
private final Region neQuad ;
private final Region swQuad ;
private final Region seQuad ;
public ScatterQuadrantChart(#NamedArg("xAxis") Axis<X> xAxis,
#NamedArg("yAxis") Axis<Y> yAxis) {
super(xAxis, yAxis);
nwQuad = new Region();
neQuad = new Region();
swQuad = new Region();
seQuad = new Region();
nwQuad.setStyle("-fx-background-color: lightsalmon ;");
neQuad.setStyle("-fx-background-color: antiquewhite ;");
swQuad.setStyle("-fx-background-color: aqua ;");
seQuad.setStyle("-fx-background-color: lightskyblue ;");
getPlotChildren().addAll(nwQuad, neQuad, swQuad, seQuad);
ChangeListener<Object> quadListener = (obs, oldValue, newValue) -> layoutPlotChildren();
xQuadrantDivider.addListener(quadListener);
yQuadrantDivider.addListener(quadListener);
}
#Override
public void layoutPlotChildren() {
super.layoutPlotChildren();
X x = xQuadrantDivider.getValue();
Y y = yQuadrantDivider.getValue();
if (x != null && y != null) {
Axis<X> xAxis = getXAxis();
Axis<Y> yAxis = getYAxis();
double xPixels = xAxis.getDisplayPosition(x);
double yPixels = yAxis.getDisplayPosition(y);
double totalWidth = xAxis.getWidth();
double totalHeight = yAxis.getHeight();
nwQuad.resizeRelocate(0, 0, xPixels, yPixels);
swQuad.resizeRelocate(0, yPixels, xPixels, totalHeight - yPixels);
neQuad.resizeRelocate(xPixels, 0, totalWidth - xPixels, yPixels);
seQuad.resizeRelocate(xPixels, yPixels, totalWidth - xPixels, totalHeight - yPixels);
}
}
public final Property<X> xQuadrantDividerProperty() {
return this.xQuadrantDivider;
}
public final X getXQuadrantDivider() {
return this.xQuadrantDividerProperty().getValue();
}
public final void setXQuadrantDivider(final X xQuadrantDivider) {
this.xQuadrantDividerProperty().setValue(xQuadrantDivider);
}
public final Property<Y> yQuadrantDividerProperty() {
return this.yQuadrantDivider;
}
public final Y getYQuadrantDivider() {
return this.yQuadrantDividerProperty().getValue();
}
public final void setYQuadrantDivider(final Y yQuadrantDivider) {
this.yQuadrantDividerProperty().setValue(yQuadrantDivider);
}
}
Test code:
import java.util.Random;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ScatterQuadrantChartTest extends Application {
#Override
public void start(Stage primaryStage) {
final Random rng = new Random();
ScatterQuadrantChart<Number, Number> chart = new ScatterQuadrantChart<>(new NumberAxis(), new NumberAxis());
Series<Number, Number> series = new Series<>();
for (int i=0; i<20; i++) {
series.getData().add(new Data<>(rng.nextDouble() * 100, rng.nextDouble() * 100));
}
chart.getData().add(series);
chart.setXQuadrantDivider(50);
chart.setYQuadrantDivider(50);
BorderPane root = new BorderPane(chart);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Related
I made a chess game that is supposed to run in 2 windows (stages). When a player moves a piece, in the second window the same piece moves accordingly. But the second window is rotated by 180 degrees to simulate a 2 player experience.
To realize this I thought it would be the easiest to use the 1 scene in 2 Windows. Basically a mirror of the first scene.
Problem: Figure doesn't move in the second window but the game knows that it has been moved in the first window because the player can't move the same figures again but the other color.
There is also a Main Menu, which has a play button, that starts the 2 windows.
#FXML
public void play_game(ActionEvent event) throws IOException {
Parent root = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("game_board.fxml")));
Parent second_screen = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("game_board.fxml")));
stage = (Stage)((Node)event.getSource()).getScene().getWindow();
scene = new Scene(root);
stage.setScene(scene);
stage.show();
Stage second_stage = new Stage();
second_stage.setScene(new Scene(second_screen));
second_screen.rotateProperty().set(180);
second_stage.show();
}
The **controller class ** for the first window:
public class board_controller {
#FXML
GridPane chess_board;
#FXML
GridPane second_board;
#FXML
public void initialize(){
game_logic game_logic = new game_logic(chess_board);
}
The controller class for the second window:
public class second_board_controller extends board_controller {
#FXML
public void initialize(){
game_logic new_round = new game_logic(chess_board);
}
}
My question is: How can I use the same exact instance of the scene but only rotated in the second window (Basically a Mirror of the first window)?
Game Example
Thank you!
I tried making the gridpane in the first_board controller static using it in the second board controller with the hopes of them updating automatically but with no results. Setting the main scene in first and second stages but my IDE said it's not allowed. I'm out of ideas...
Share the Model
Since you want to mirror between two windows in the same process, the general idea is to create two instances of your view but share only one instance of the model. The model should be observable in some way so that the view/controller can react to changes in the model by updating the view. With the model being shared and observed, updating it from one window will be seen by the other window.
Models
Note a model should not know about the view. In your code, you do:
game_logic game_logic = new game_logic(chess_board);
From the name of the class, this indicates you're passing a GridPane (the view) to your model. It would be better if your model only modelled a chess game. The controller/view is responsible for translating that state into a visual representation.
Rotating the Second View
The simplest approach to this would be to add state/a method to your controller, and then only on the second instance of the controller configure it to rotate the view. It is at least somewhat justifiable to put this logic in the controller/view because it is only a view thing (it does not affect the game state).
Though instead of rotating the board, you might want to consider "inverting" the location of the pieces (vertically). In other words, for the second view, have it so that a white piece in the bottom-left corner of the board is actually displayed in the top-left corner (and the opposite for black pieces). That way the chess piece images are not rotated along with the rest of the board.
Example
Here's a proof-of-concept for mirroring a draggable rectangle (much simpler than a chess game). Note it only demonstrates the mirroring, it does not show how to e.g., rotate the view in one window but not the other.
RectangleModel.java:
package sample;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
public class RectangleModel {
private final List<Consumer<? super Dimensions>> listeners = new CopyOnWriteArrayList<>();
private Dimensions dimensions;
public RectangleModel(Dimensions dimensions) {
this.dimensions = Objects.requireNonNull(dimensions);
}
public RectangleModel(double x, double y, double width, double height) {
this(new Dimensions(x, y, width, height));
}
public void move(double deltaX, double deltaY) {
if (deltaX != 0.0 || deltaY != 0.0) {
double x = dimensions.x() + deltaX;
double y = dimensions.y() + deltaY;
double w = dimensions.width();
double h = dimensions.height();
dimensions = new Dimensions(x, y, w, h);
notifyListeners();
}
}
public Dimensions getDimensions() {
return dimensions;
}
public void addDimensionsListener(Consumer<? super Dimensions> listener) {
listeners.add(Objects.requireNonNull(listener));
}
public void removeDimensionsListener(Consumer<? super Dimensions> listener) {
listeners.remove(Objects.requireNonNull(listener));
}
private void notifyListeners() {
for (var listener : listeners) {
listener.accept(dimensions);
}
}
public record Dimensions(double x, double y, double width, double height) {}
}
RectangleController.java:
package sample;
import java.util.function.Consumer;
import javafx.fxml.FXML;
import javafx.geometry.Point2D;
import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Rectangle;
public class RectangleController {
private final Consumer<RectangleModel.Dimensions> listener = this::updateRectangle;
#FXML
private Rectangle rectangle;
private Point2D offset;
private RectangleModel model;
public void setModel(RectangleModel model) {
if (this.model != null) {
this.model.removeDimensionsListener(listener);
}
this.model = model;
if (model != null) {
model.addDimensionsListener(listener);
updateRectangle(model.getDimensions());
} else {
updateRectangle(null);
}
}
private void updateRectangle(RectangleModel.Dimensions dims) {
if (dims != null) {
rectangle.setX(dims.x());
rectangle.setY(dims.y());
rectangle.setWidth(dims.width());
rectangle.setHeight(dims.height());
} else {
rectangle.setX(0);
rectangle.setY(0);
rectangle.setWidth(0);
rectangle.setHeight(0);
}
}
#FXML
private void handleMousePressed(MouseEvent event) {
event.consume();
offset = new Point2D(event.getX(), event.getY());
}
#FXML
private void handleMouseDragged(MouseEvent event) {
event.consume();
double deltaX = event.getX() - offset.getX();
double deltaY = event.getY() - offset.getY();
model.move(deltaX, deltaY);
offset = new Point2D(event.getX(), event.getY());
}
#FXML
private void handleMouseReleased(MouseEvent event) {
event.consume();
offset = null;
}
}
RectangleView.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.shape.Rectangle?>
<Pane xmlns="http://javafx.com/javafx/" xmlns:fx="http://javafx.com/fxml/"
fx:controller="sample.RectangleController">
<Rectangle fx:id="rectangle" onMousePressed="#handleMousePressed" onMouseDragged="#handleMouseDragged"
onMouseReleased="#handleMouseReleased"/>
</Pane>
Main.java:
package sample;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
var model = new RectangleModel(0, 0, 100, 50);
primaryStage.setScene(createScene(model));
primaryStage.setTitle("Primary Stage");
primaryStage.show();
var secondStage = new Stage();
secondStage.setScene(createScene(model));
secondStage.setTitle("Second Stage");
secondStage.show();
primaryStage.setX(primaryStage.getX() - primaryStage.getWidth() / 2);
secondStage.setX(primaryStage.getX() + primaryStage.getWidth());
secondStage.setY(primaryStage.getY());
primaryStage.setOnCloseRequest(e -> secondStage.close());
secondStage.setOnCloseRequest(e -> primaryStage.close());
}
private Scene createScene(RectangleModel model) throws IOException {
var loader = new FXMLLoader(Main.class.getResource("RectangleView.fxml"));
var root = loader.<Parent>load();
var controller = loader.<RectangleController>getController();
var scene = new Scene(root, 600, 400);
controller.setModel(model);
return scene;
}
}
Naming Conventions
You should follow the standard naming conventions of Java (or whatever language you're using) when posting on a public forum.
Classes and interfaces use PascalCase.
Methods, fields, parameters, and local variables use camelCase.
Static constants use UPPER_SNAKE_CASE.
I am going to school for programming in java. I received a program where i have to use a scrollbar to change the width of an imageView. My question is it even possible with the scrollbar API in JavaFX?
Alright Here is my code.
sb = new ScrollBar();
sb.setMax(100);
sb.setMin(0);
lastValue = 500;
sb.setValue(lastValue);
sb.setUnitIncrement(1);
sb.blockIncrementProperty();
sb.setOnScroll(e -> FacePart.getPart().scrollAction(lastValue));
this is where i am having the issue. communicating back and forth between the class that this code is in and my method that is in another class.
here is the method in the other class.
Other method
#Override
public void scrollAction(double j) {
/*Global variable*/ lastScrollValue = j;
iv.setFitWidth(300 + 2 * lastScrollValue);
}
This can be done, but it shouldn't be done using the onScroll event.
Add a ChangeListener to the value property instead:
sb.valueProperty().addListener((observable, oldValue, newValue) -> {
FacePart.getPart().scrollAction(newValue.doubleValue());
});
Or simply use bindings:
iv.fitWidthProperty().bind(sb.valueProperty().multiply(2).add(300));
At least I guess that's what you want to do. I'm not sure why you'd use a "global" there.
Furthermore:
sb.blockIncrementProperty();
is effectively a NOP here. If you need to set (or get) the value, you should do it using the provided setter (or the getter):
sb.setBlockIncrement(someValue);
Doing this via the property is less readable and does the same.
Also usually you'd use a Slider for this (since the handle size doesn't seem to have a meaning in this case).
I agree with Fabian, just use a Slider for this task, rather than a ScrollBar. A Slider is a more suitable control.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class SmurfObesityMeter extends Application {
private static final double DEFAULT_SIZE = 128;
private static final double MIN_WIDTH = DEFAULT_SIZE / 2;
private static final double MAX_WIDTH = DEFAULT_SIZE * 2;
#Override
public void start(final Stage stage) throws Exception {
final Image image = new Image(IMAGE_LOC);
final ImageView imageView = new ImageView(image);
StackPane imagePane = new StackPane(imageView);
imagePane.setMinSize(StackPane.USE_PREF_SIZE, StackPane.USE_PREF_SIZE);
imagePane.setPrefSize(MAX_WIDTH, DEFAULT_SIZE);
imagePane.setMaxSize(StackPane.USE_PREF_SIZE, StackPane.USE_PREF_SIZE);
final Slider slider = new Slider(MIN_WIDTH, MAX_WIDTH, DEFAULT_SIZE);
imageView.fitWidthProperty().bind(slider.valueProperty());
final VBox layout = new VBox(10, imagePane, slider);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
private static final String IMAGE_LOC =
"http://icons.iconarchive.com/icons/designbolts/smurfs-movie/128/smurfette-icon.png";
}
I'm struggeling with the Java FX BarChart.. My own implementation of the chart is a class that extends the Java FX GridPane and holds a BarChart as a member variable.
If I initialize the whole thing everything works perfect, but if I change the data dynamically (add one or remove one data) the layout will be destroyed.
Speaking in pictures this means: (sorry i can't upload picture at the moment)
pic1 - initialization
after adding one element
So the 1st pictures shows the chart after initalization, the 2nd after one element has been added and after deleting one element the categories aren't shown anymore. (I Ccan't upload a picture of this)
So here's my code:
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import someCompanyThings.IMyBarChart;
import someCompanyThings.LocaleService;
import someCompanyThings.INlsKey;
/**
* A Chart with vertical or horizontal bars. It is assumed that the Bars represent positive integer numbers.
* Data may be added or removed dynamically but on the first intent it should display static.
*/
public class MyBarChart extends GridPane implements IMyBarChart {
/*
* Due to data binding problems with a generic bar chart, we hold the two possible bar charts as member variables.
* Also each of them get's a list of Series<?, ?>
*/
private BarChart<String, Number> _barChartVertical;
private BarChart<Number, String> _barChartHorizontal;
private final ObservableList<Series<String, Number>> _dataVertical = FXCollections.observableArrayList();
private final ObservableList<Series<Number, String>> _dataHorizontal = FXCollections.observableArrayList();
private long _maxValue = 0;
private boolean _numberAxisInPercent = false;
private boolean _horizontal = false;
public MyBarChart(INlsKey pTitle, INlsKey pXLabel, INlsKey pYLabel, boolean pNumberAxisInPercent, boolean pHorizontal) {
super();
CategoryAxis categoryAxis = new CategoryAxis();
categoryAxis.setId("bar-chart-category-axis");
NumberAxis numberAxis = new NumberAxis(0.0, 1.0, 1.0);
numberAxis.setId("bar-chart-number-axis");
// create bar chart
// horizontal means that the x-axis is a number axis and the y-axis is a category axis
if (pHorizontal) {
categoryAxis.setLabel(LocaleService.getMessage(pYLabel));
numberAxis.setLabel(LocaleService.getMessage(pXLabel));
_barChartHorizontal = new BarChart<Number, String>(numberAxis, categoryAxis);
_barChartHorizontal.setData(_dataHorizontal);
_barChartHorizontal.setTitle(LocaleService.getMessage(pTitle));
getChildren().add(_barChartHorizontal);
}
else {
categoryAxis.setLabel(LocaleService.getMessage(pXLabel));
numberAxis.setLabel(LocaleService.getMessage(pYLabel));
_barChartVertical = new BarChart<String, Number>(categoryAxis, numberAxis);
_barChartVertical.setData(_dataVertical);
_barChartVertical.setTitle(LocaleService.getMessage(pTitle));
getChildren().add(_barChartVertical);
}
_numberAxisInPercent = pNumberAxisInPercent;
_horizontal = pHorizontal;
/*
* layout
*/
setHgrow(getChildren().get(0), Priority.ALWAYS);
setVgrow(getChildren().get(0), Priority.ALWAYS);
}
#Override
public IMyBarChart addSeries(INlsKey pSeriesName, ObservableList<Data<String, Number>> pDataSet) {
final Series<String, Number> series = new Series<String, Number>(LocaleService.getMessage(pSeriesName), pDataSet);
_dataVertical.add(series);
// iterate over the whole data segment and add it to the series
for (final Data<String, Number> data : pDataSet) {
Tooltip tooltip = new Tooltip();
tooltip.setText(data.getXValue());
Tooltip.install(data.getNode(), tooltip);
if (data.getYValue().longValue() > _maxValue) {
_maxValue = data.getYValue().longValue();
}
}
setNumberAxisScale();
return this;
}
#Override
public IMyBarChart addSeriesHorizontal(INlsKey pSeriesName, ObservableList<Data<Number, String>> pDataSet) {
final Series<Number, String> series = new Series<Number, String>(LocaleService.getMessage(pSeriesName), pDataSet);
_dataHorizontal.add(series);
// iterate over the whole data segment and add it to the series
for (final Data<Number, String> data : pDataSet) {
Tooltip tooltip = new Tooltip();
tooltip.setText(data.getYValue());
Tooltip.install(data.getNode(), tooltip);
if (data.getXValue().longValue() > _maxValue) {
_maxValue = data.getXValue().longValue();
}
}
setNumberAxisScale();
return this;
}
private void setNumberAxisScale() {
NumberAxis numberAxis = getNumberAxis();
// set the number axis as a percent axis
if (_numberAxisInPercent) {
numberAxis.setUpperBound(100);
numberAxis.setTickUnit(10);
}
else {
numberAxis.setUpperBound(_maxValue + 1);
numberAxis.setTickUnit(1);
}
}
#Override
public void setLegendVisible(boolean pVisible) {
if (_barChartHorizontal != null) {
_barChartHorizontal.setLegendVisible(pVisible);
}
else {
_barChartVertical.setLegendVisible(pVisible);
}
}
#Override
public void setCategories(ObservableList<String> pCategories) {
getCategoryAxis().getCategories().setAll(pCategories);
}
/**
*
* #return the category axis of the used bar chart
*/
private CategoryAxis getCategoryAxis() {
if (_horizontal) {
return (CategoryAxis)_barChartHorizontal.getYAxis();
}
else {
return (CategoryAxis)_barChartVertical.getXAxis();
}
}
/**
*
* #return the number axis of the used bar chart
*/
private NumberAxis getNumberAxis() {
if (_horizontal) {
return (NumberAxis)_barChartHorizontal.getXAxis();
}
else {
return (NumberAxis)_barChartVertical.getYAxis();
}
}
}
The initialization process:
final IMyBarChart tablespacesChart = MyFactory.createBarChart(NlsKeys.tablespacesTitle, NlsKeys.tablespacesXAxis,
NlsKeys.tablespacesYAxis, true, true);
// first bool -> numberAxisInPercent, second bool -> horizontal ortientation
tablespacesChart.setLegendVisible(false);
tablespacesChart.setCategories(model.getListCategories());
tablespacesChart.addSeriesHorizontal(NlsKeys.tablespacesLegendYAxis, model.getListDataUsedMax());
The data changes are realised by another class that just uses
model.getCategories().setAll(MyNewCatList); // or
model.getListDataUsedMax().setAll(MyNewList);
Well, i also tried to implement the chart with just one member variable (like BarChart _barChart) but this didn't work.
Now i have those layout issues and i dunno where they come from. So i hope you can give me a hint :-)
Here's my solution:
First, create a subclass of bar chart to access the private method updateAxisRange:
class MyBarChart<X, Y> extends BarChart<X, Y> {
public MyBarChart(Axis xAxis, Axis yAxis) {
super(xAxis, yAxis);
}
public void relayout() {
updateAxisRange();
}
}
Next, instantiate your bar chart as MyBarChart:
MyBarChart<String, Number> barChart = new MyBarChart<String, Number>(xAxis, yAxis);
And Lastly, you need to listen to resize events on the parent containing the chart, and when they occur, invoke the relayout of the chart.
For example:
BorderPane pane = new BorderPane(barChart);
pane.widthProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> arg0, Number arg1, Number arg2) {
barChart.relayout();
}
});
I have written the below JavaFX program in which two rectangle nodes are in translate transition:
public class Test extends Application{
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane borderPane = new BorderPane();
borderPane.setStyle("-fx-background-color: green;");
Rectangle rect1 = new Rectangle(20,20,50, 50);
rect1.setArcHeight(15);
rect1.setArcWidth(15);
rect1.setFill(Color.RED);
Rectangle rect2 = new Rectangle(20,20,30, 30);
rect2.setArcHeight(15);
rect2.setArcWidth(15);
rect2.setFill(Color.RED);
TranslateTransition translateTransition1 = new TranslateTransition(Duration.millis(2000), rect1);
translateTransition1.setFromX(0);
translateTransition1.setToX(300);
translateTransition1.setToY(300);
translateTransition1.setCycleCount(Timeline.INDEFINITE);
translateTransition1.setAutoReverse(true);
translateTransition1.play();
TranslateTransition translateTransition2 = new TranslateTransition(Duration.millis(2000), rect2);
translateTransition2.setFromX(300);
translateTransition2.setToX(0);
translateTransition2.setToY(300);
translateTransition2.setCycleCount(Timeline.INDEFINITE);
translateTransition2.setAutoReverse(true);
translateTransition2.play();
borderPane.getChildren().add(rect1);
borderPane.getChildren().add(rect2);
primaryStage.setScene(new Scene(borderPane, 500, 500));
primaryStage.show();
}
}
How can I implement collision detection of the two rectangle nodes which are in Translate Transition?
With rectangles it's pretty easy; just get their bounds in the parent and see if they intersect. The only drawback with this is it doesn't take into account the curved corners: you may need to compute that by hand if you want that level of accuracy. For non-rectangular shapes you can also just observe the bounds in parent properties, but you'd need to do the computation by hand to see if the shapes intersect.
ObservableBooleanValue colliding = Bindings.createBooleanBinding(new Callable<Boolean>() {
#Override
public Boolean call() throws Exception {
return rect1.getBoundsInParent().intersects(rect2.getBoundsInParent());
}
}, rect1.boundsInParentProperty(), rect2.boundsInParentProperty());
colliding.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> obs,
Boolean oldValue, Boolean newValue) {
if (newValue) {
System.out.println("Colliding");
} else {
System.out.println("Not colliding");
}
}
});
TranslateTransition isn't meant to support Collision Detection. It simply moves A to B without any regards to the state of anything but its node.
You would need a Transition mechanism that is aware of the other objects on the board.
The good news is that creating a Transition isn't too hard. You can create a class that inherits Transition and simply implement the interpolate() method.
From the JavaDoc:
Below is a simple example. It creates a small animation that updates
the text property of a Text node. It starts with an empty String and
adds gradually letter by letter until the full String was set when the
animation finishes.
final String content = "Lorem ipsum";
final Text text = new Text(10, 20, "");
final Animation animation = new Transition() {
{
setCycleDuration(Duration.millis(2000));
}
protected void interpolate(double frac) {
final int length = content.length();
final int n = Math.round(length * (float) frac);
text.setText(content.substring(0, n));
}
};
The bad news is that having a successful collision detection mechanism is a bit harder. I'm really no expert on the subject, but I would probably have a ObservableList of Nodes that have collision, pass it to the Transition and on the interpolate method I would do a intersection check of the node that's moving against all the other nodes and leave it still if he cannot move.
If you want anything better than that, you'll probably want to look into a 2D Game Framework like Slick2D.
EDIT: Made a few simple alterations and went with a State based approach, code has been updated.
Well my approach is different that all the above ...
NOTE: I'm using 1.8 source
I created a Collidable interface:
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.shape.Shape;
public interface Collidable{
public enum CollisionState{
WAITING,
TOUCHING;
}
ObjectProperty<CollisionState> state = new SimpleObjectProperty<>(CollisionState.WAITING);
public default ReadOnlyObjectProperty<CollisionState> collisionStateProperty(){return state;}
public default CollisionState getCollisionState(){return state.get();}
BooleanProperty collided = new SimpleBooleanProperty(false){{
addListener((ObservableValue<? extends Boolean> observable1, Boolean oldValue, Boolean touching) -> {
if(touching){
state.set(CollisionState.TOUCHING);
}else{
state.set(CollisionState.WAITING);
}
});
}};
public default boolean hasCollided(){return collided.get();}
public default BooleanProperty collidedProperty(){return collided;}
public default void checkCollision(Shape src, Shape other){
if(Shape.intersect(src, other).getBoundsInLocal().getWidth() > -1 && !getCollisionState().equals(CollisionState.TOUCHING)){
collided.set(true);
handleCollision(other);
}else if(Shape.intersect(src, other).getBoundsInLocal().getWidth() <= 0){
collided.set(false);
}
}
public void handleCollision(Shape other);
}
And a simple implementation:
import javafx.animation.Animation;
import javafx.animation.ParallelTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;
import javafx.util.Duration;
/**
*
* #author Dub-Laptop
*/
public class CollisionTesting extends Application {
private TranslateTransition cAnim;
#Override
public void start(Stage primaryStage) {
Group root = new Group();
Scene scene = new Scene(root);
primaryStage.setTitle("Collision Testing");
primaryStage.setScene(scene);
primaryStage.show();
Rectangle r = new Rectangle(100,50, Color.AQUA);
r.setLayoutX(10);
r.setLayoutY(200);
CollidableCircle c = new CollidableCircle(50, Color.GREEN);
c.setLayoutX(800);
c.setLayoutY(200);
/* can change this to anything you like
I used translateXProperty for simplicity
*/
c.translateXProperty().addListener((Observable observable) -> {
c.checkCollision(c, r);
});
root.getChildren().addAll(r, c);
TranslateTransition rAnim = new TranslateTransition();
rAnim.setToX(600);
rAnim.setAutoReverse(true);
rAnim.setCycleCount(Animation.INDEFINITE);
rAnim.setDuration(Duration.seconds(5));
rAnim.setNode(r);
cAnim = new TranslateTransition();
cAnim.setToX(-590);
cAnim.setAutoReverse(true);
cAnim.setCycleCount(Animation.INDEFINITE);
cAnim.setDuration(Duration.seconds(5));
cAnim.setNode(c);
rAnim.play();
cAnim.play();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
private class CollidableCircle extends Circle implements Collidable{
public CollidableCircle(double radius, Paint fill) {
super(radius, fill);
new AnimationTimer(){
#Override
public void handle(long now) {
root.getChildren().filtered((Node n)->{
return !n.equals(CollidableCircle.this) && n instanceof Shape;
}).forEach(other ->{
checkCollision(CollidableCircle.this, (Shape)other);
});
}
}.start();
// I added this for local property changes to this node
collisionStateProperty().addListener((ObservableValue<? extends CollisionState> observable, CollisionState oldValue, CollisionState newValue) -> {
if(newValue.equals(CollisionState.TOUCHING)){
setScaleX(1.25);
setScaleY(1.25);
setFill(Color.GREENYELLOW);
cAnim.pause();
}else if(newValue.equals(CollisionState.WAITING)){
setScaleX(1.0);
setScaleY(1.0);
setFill(Color.GREEN);
cAnim.play();
}
});
}
#Override
public void handleCollision(Shape other) {
// handle updates that affect other node here
System.out.println("Collided with : " + other.getClass().getSimpleName());
}
}
}
IMHO rather than using Bounds for checking Shape collisions, use the boolean :
(Shape.intersect(s1,s2).getBoundsInLocal().getWidth() > -1)
This approach is more accurate for Shapes as it will check for non-null pixels within the Shape Bounds, rather than the normal rectangular Bounds.
Though if you really want to use Bounds, this should work also:
if(sourceShape.getBoundsInLocal().intersects(otherShape.getBoundsInParent()){
Shape intersect = Shape.intersect(sourceShape, otherShape);
if(intersect.getBoundsInLocal().getWidth > -1){
// handle code here
}
}
though, as you can see it's more verbose and virtually the same as my other method.
Hope this helps.
Very weird problem, I finally managed to distill it into a small piece of code which demonstrates the problem. I have a pane, which contains 1 group, that groups contains a group which contains some ellipses. The top group has a rotate transform applied to it. The ellipses are made draggable.
Try the below example, drag some ellipses downwards (outside the group's bounds), you'll see them disappearing. If you maximize the window, they appear again but you can't drag them anymore, they don't receive any events anymore.
Now for the really strange part, there are three ways I can make the problem go away:
don't apply the transform
remove one ellipse (!?) (I experimented to get to this number, 11)
start ScenicView alongside and select the group containing the ellipses so you can see the bounds of the group
I'm at a total loss here, completely stupefied. Please, does anyone have any idea why this problem is occuring and how to solve it?
Code (JavaFX 2.2.3 and java 1.7.0_09 64bit Windows 7):
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.GroupBuilder;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SceneBuilder;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.EllipseBuilder;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.RotateBuilder;
import javafx.stage.Stage;
public class DragProblem extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
DrawingPane drawingPane = new DrawingPane();
drawingPane.setStyle("-fx-background-color: darkgrey;");
Scene scene = SceneBuilder.create().root(drawingPane).width(1280d).height(1024d).build();
primaryStage.setScene(scene);
primaryStage.show();
}
public class DrawingPane extends Pane {
private Group transformedGroup;
private Group splinePoints;
public DrawingPane() {
transformedGroup = GroupBuilder.create().id("transformedGroup").build();
getChildren().add(transformedGroup);
addPoints();
makePointsDraggable();
}
public void addPoints() {
double[] coords = new double[] {
// comment any one the below x,y coordinates and the problem doesn't occur..
239.28353881835938, 488.2192687988281,
245.04466247558594, 505.30169677734375,
258.56671142578125, 539.49462890625,
267.2294006347656, 563.618408203125,
282.89141845703125, 587.84033203125,
309.6925048828125, 602.2174072265625,
327.4945068359375, 616.4683227539062,
345.25445556640625, 633.718994140625,
371.0416259765625, 649.0819702148438,
393.78704833984375, 667.402587890625,
442.67010498046875, 676.0886840820312 };
splinePoints = GroupBuilder.create().build();
for (int i = 0; i < coords.length; i += 2) {
Ellipse ellipse = EllipseBuilder.create().radiusX(3).radiusY(3).centerX(coords[i]).centerY(coords[i + 1]).build();
splinePoints.getChildren().add(ellipse);
}
transformedGroup.getChildren().add(splinePoints);
Rotate rotateTransform = RotateBuilder.create().build();
rotateTransform.setPivotX(224);
rotateTransform.setPivotY(437);
rotateTransform.setAngle(15);
// ..or comment this line to prevent the problem occuring
transformedGroup.getTransforms().add(rotateTransform);
}
public void makePointsDraggable() {
for (final Node n : splinePoints.getChildren()) {
Ellipse e = (Ellipse) n;
final NodeDragHandler ellipseDragHandler = new NodeDragHandler(e, transformedGroup);
e.setOnMousePressed(ellipseDragHandler);
e.setOnMouseDragged(ellipseDragHandler);
}
}
}
public class NodeDragHandler implements EventHandler<MouseEvent> {
protected final Ellipse node;
private final Node transformedGroup;
private double initialX;
private double initialY;
private Point2D initial;
private boolean dragStarted = false;
public NodeDragHandler(Ellipse node, Group transformedGroup) {
this.node = node;
this.transformedGroup = transformedGroup;
}
#Override
public void handle(MouseEvent event) {
if (!dragStarted) {
initialX = event.getScreenX();
initialY = event.getScreenY();
initial = transformedGroup.localToParent(new Point2D(node.getCenterX(), node.getCenterY()));
dragStarted = true;
} else {
double xDragged = event.getScreenX() - initialX;
double yDragged = event.getScreenY() - initialY;
Point2D newPos = new Point2D(initial.getX() + xDragged, initial.getY() + yDragged);
Point2D p = transformedGroup.parentToLocal(newPos.getX(), newPos.getY());
node.setCenterX(p.getX());
node.setCenterY(p.getY());
}
}
}
}
It's been acknowledged as a bug in JavaFX and will be solved in 2.2.6, see here. I've tested it with the early access release and I can confirm it has been solved.