The code for this example can be found on GitHub. Note that this code is still a work-in-progress and is being modified for a future blog post. The code in the first part of the post is going to be commented out and then removed in favor of the JavaFX Binding construct.
This other posts in this series are
This video demonstrates the visual cue provided when navigating through the wizard. The Next Button iterates through a sequence of FXML-based forms. The Back Button reverses this iteration. The handlers verify the bounds; you can't go back before the first step or forward after the last step.
The visual cue is a List of JavaFX Circles added to an HBox. A blue circle indicates the current step and prior steps. A white circle indicates a step that has not been visited or that the user has reversed the sequence with the Back Button.
Both implementations key off of an IntegerProperty marking the currentStep. This property is manipulated and protected by next() and back() methods. The property is zero-based and serves as an index for displaying the correct form in the wizard.
private IntegerProperty currentStep = new SimpleIntegerProperty(-1);
ChangeListener Implementation
The ChangeListener-based implementation is as follows. Iterate through the item, coloring the circles up to and including currentStep blue and coloring the remaining steps white.
The JavaFX Circles are added to an HBox.
@FXML HBox hboxIndicators; @FXML public void initialize() { ... steps.forEach((p) -> hboxIndicators.getChildren().add(createIndicatorCircle()) ); ...The createIndicatorCircle() method is a factory method.
private Circle createIndicatorCircle() { Circle circle = new Circle(INDICATOR_RADIUS, Color.WHITE); circle.setStroke(Color.BLACK); return circle; }
To close out the initialize() method, the currentStep property is assigned a ChangeListener. The listener looks at the new value and forms a loop with logic to determine the color for each Circle.
// continues initialize() currentStep.addListener((obs,oldVal,newVal) -> { for( int i=0; i<steps.size(); i++ ) { Node n = hboxIndicators.getChildren().get(i); if( i<= newVal.intValue() ) { ((Circle)n).setFill( Color.DODGERBLUE ); } else { ((Circle)n).setFill( Color.WHITE ); } }});
Binding Implementation
The Binding-based implementation directly relates a particular Circle to the IntegerProperty. The initialize method is adjusted to use a C-style for loop that passes an int position to the factory method.
for( int i=0; i<steps.size(); i++ ) { hboxIndicators.getChildren().add( createIndicatorCircle(i)); }
Instead of hooking up the visual cue behavior to the IntegerProperty, I hook the behavior up to the Circle. The factory method is rewritten as follows
private Circle createIndicatorCircle(int i) { Circle circle = new Circle(INDICATOR_RADIUS, Color.WHITE); circle.setStroke(Color.BLACK); circle.fillProperty().bind( new When( currentStep.greaterThanOrEqualTo(i)) .then(Color.DODGERBLUE) .otherwise(Color.WHITE)); return circle; }
Both implementations work and the addListener() version will be more familiar to Core Java developers especially those who have worked with Swing. As I get more accustomed to Fluent APIs, I find the Binding syntax easier to read. Design-wise, the difference is whether or not you want a centralized addListener() method managing all the things that happen when currentStep changes or if this can be spread out among different components that register for the currentStep change. In addition to my style preference, this wizard code is going to continue with the latter. A future blog post is going to drive the contentPanel management off of the currentStep property.
No comments:
Post a Comment