JavaFX Abacus Tutorial, Part V
This is the fifth part of a basic tutorial on JavaFX. The other parts can be found here and the code is on github. The running example of the tutorial is to build an abacus. If you are curious how the final solution will look like, please
have look at this 7 min video.
Your latest homework was to think about possible solutions for pushing adjacent balls to the right or left just like a physical abacus does it mechanically.
Now, what do you think?
There are two typical solutions that students tend to come up with:
- a full model with rails and balls with move actions that find out each ball that has to be moved
- every ball listens for position changes of his neighbors
The first approach requires quite some work while the latter keeps amount of state that has to be managed pretty small. It is also closer to what happens in the mechanical world. We will start with the listening to our neighbors.
JavaFX provides a ChangeListener for that purpose. For the sake of simplicity we create a new one for every pair. It is cumbersome enough to define the anonymous inner classes and the final variables that Java requires.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
for (int row = 0; row < ROW_COUNT; row++) { .. Circle last = null; for (int column = 0; column < COL_COUNT; column++) { .. if (last != null) { last.translateXProperty().addListener(new ChangeListener<Number>() { public void changed(ObservableValue<? extends Number> observableValue, Number oldX, Number newX) { if ((Double) newX > circle.getTranslateX()) circle.setTranslateX((Double) newX); } }); final Circle finalLast = last; circle.translateXProperty().addListener(new ChangeListener<Number>() { public void changed(ObservableValue<? extends Number> observableValue, Number oldX, Number newX) { if ((Double) newX < finalLast.getTranslateX()) finalLast.setTranslateX((Double) newX); } }); } last = circle; } } |
This code works surprisingly well. Before JavaFX I would have expected this solution to result in a recognizable jitter in the movement of the balls but they move perfectly smooth and synchronous as if we had used some elaborate cropping techniques.
Here is the full code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
package solution; import javafx.animation.TranslateTransition; import javafx.animation.TranslateTransitionBuilder; import javafx.application.Application; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.scene.shape.CircleBuilder; import javafx.scene.shape.Rectangle; import javafx.scene.shape.RectangleBuilder; import javafx.scene.text.Text; import javafx.stage.Stage; import static javafx.util.Duration.millis; public class Abacus_5_Push_neighbors extends Application { private static final int ROW_COUNT = 10; private static final int COL_COUNT = 10; private static final int RADIUS = 20; private static final int DIAMETER = 2 * RADIUS; private static final int MOVE_WAY = 8 * DIAMETER; private static final int WIDTH = COL_COUNT * DIAMETER + MOVE_WAY; private static final int HEIGHT = ROW_COUNT * DIAMETER; private static final int PADDING = 20; private static final int OFFSET = PADDING + RADIUS; private static final int RAIL_HEIGHT = 10; @Override public void start(Stage primaryStage) { primaryStage.setTitle("JavaFX Abacus with push"); Pane root = new Pane(); for (int row = 0; row < ROW_COUNT; row++) { Rectangle rail = RectangleBuilder.create() .width(WIDTH) .height(RAIL_HEIGHT) .x(PADDING) .y(OFFSET - (RAIL_HEIGHT / 2) + (row * DIAMETER)) .build(); root.getChildren().add(rail); Circle last = null; for (int column = 0; column < COL_COUNT; column++) { final Circle circle = makeCircle(OFFSET + (row * DIAMETER), OFFSET + (column * DIAMETER)); root.getChildren().add(circle); Text text = new Text(circle.getCenterX()-3, circle.getCenterY()+4, ""+ ((COL_COUNT - column) % 10)); text.translateXProperty().bind(circle.translateXProperty()); text.setOnMouseClicked(circle.getOnMouseClicked()); text.setFill(Color.WHITE); root.getChildren().add(text); if (last != null) { last.translateXProperty().addListener(new ChangeListener<Number>() { public void changed(ObservableValue<? extends Number> observableValue, Number oldX, Number newX) { if ((Double) newX > circle.getTranslateX()) circle.setTranslateX((Double) newX); } }); final Circle finalLast = last; circle.translateXProperty().addListener(new ChangeListener<Number>() { public void changed(ObservableValue<? extends Number> observableValue, Number oldX, Number newX) { if ((Double) newX < finalLast.getTranslateX()) finalLast.setTranslateX((Double) newX); } }); } last = circle; } } primaryStage.setScene(new Scene(root, WIDTH + 2 * PADDING, HEIGHT + 2 * PADDING)); primaryStage.show(); } private Circle makeCircle(int y, int x) { final Circle ball = CircleBuilder.create().radius(RADIUS-1).centerX(x).centerY(y).build(); ball.setOnMouseClicked(new EventHandler<MouseEvent>() { public void handle(MouseEvent mouseEvent) { double newX = MOVE_WAY; if (ball.getTranslateX() > 1) { newX = 0; } TranslateTransition move = TranslateTransitionBuilder.create().node(ball).toX(newX).duration(millis(200)).build(); move.playFromStart(); } }); return ball; } public static void main(String[] args) { launch(args); } } |
We have now reached a state where we can really use the abacus for calculation. What is still missing is the “overflow” logic that we will add later – when we will also consider a presentation model for better structuring of the code.
Homework
Play with the abacus!
Do some calculations.
Can you build the powers of 2 with your abacus?
This abacus works but it doesn’t look nice, yet. We will explore styling options next week.
Finally
If you are committed to learn JavaFX, please consider joining one of our JavaFX workshops at Canoo.
See you next week!
Dierk
P.S. Tim Yates thankfully posted a GroovyFX version.












