JavaFX Tutorials

Wednesday, September 14, 2016

Visual Cues Through JavaFX ChangeListeners

This is the third post in a series on confirming input using JavaFX.  The prior posts demonstrated how to control access to a Save Button and how to integrate a Commons Validator object to validate an email  This post adds a visual cue to the program that guides the user into producing correct input.  A ChangeListener is applied to a custom BooleanBinding which is triggered when the input is changed.

Also, an animation is applied to smooth the display of the visual cue.

The prior posts can be found here

This video demonstrates the latest iteration.



In order for the input to be accept, the TextFields must match and contain a valid email address.  Initially, the controls are empty and no visual cue is provided.  When the user starts typing in Email or Confirm Email, a red rectangle with an X is displayed.  Once the user finishes typing a valid email in both the Email and Confirm Email TextFields, a green rectangle with an = is displayed.  At this point, the Save Button is also enabled.

Both the display of the red rectangle and the brief display of the green rectangle are animated, since it looked far too jarring to have them appear suddenly

The source for the project can be found in a zip file here.

Scene Builder

The column holding the rectangles has been added to the GridPane as a VBox / Label pair that spans both rows.  This screenshot of Scene Builder shows the column.  Before leaving Scene Builder, I set the Opacity of the VBox to 0 which hides it.

Scene Builder with Visual Cue Column Showing
There a margin of 40 around the GridPane.  I've adjusted the right margin to be 26px (40 minus 10 for the column minus 4 for the Hgap).  This keeps the controls centered when the VBox isn't shown.

CSS

I'm using CSS because I plan on keeping the same VBox and Label in place for both the red X and the green equals.  I do this mainly to prevent any type of glitchy resizing that would occur if I attempted to add or remove controls from the container.  So, I toggle between two styles for each of the two controls.

Notice the -fx-border-radius and -fx-background-radius settings that give the VBoxes the same look-and-feel as the standard TextFields.

In Scene Builder, I also put a small top and bottom margin of 2 on the VBox to make it match the TextFields.

.vbox-valid-error {
    -fx-background-color: #ff9999;
    -fx-border-color:  #ff3300;
    -fx-background-radius: 2;
    -fx-border-radius: 2;
}

.label-valid-error {
    -fx-text-fill: #ff0000;
}

.vbox-valid-ok {
    -fx-background-color: #99ff99;
    -fx-border-color:  #33ff00;
    -fx-background-radius: 2;
    -fx-border-radius: 2;
}

.label-valid-ok {
    -fx-text-fill: #00ff00;
}

Code

This is the @FXML initialize() method of the JavaFX Controller class.

@FXML
public void initialize() {

    EmailValidator emailValidator = EmailValidator.getInstance();

    StringBinding validEmailExpr = Bindings.createStringBinding(
            () -> emailValidator.isValid(txtEmail.getText())?txtEmail.getText():"",
            txtEmail.textProperty()
    );

    btnSave.disableProperty().bind(
        txtEmail.textProperty()
            .isEqualTo(txtConfirmEmail.textProperty()).not()
            .or(
                txtEmail.textProperty().isEqualTo( validEmailExpr ).not()
            )
            .or(
                txtEmail.textProperty().isEmpty()
            )
    );

    BooleanBinding okToAnimate =  txtEmail.textProperty()
            .isEqualTo( validEmailExpr ).not()
            .or(
                    txtEmail.textProperty().isEqualTo(txtConfirmEmail.textProperty()).not()
            );

    okToAnimate.addListener( (obs, ov, nv) -> {

        logger.info("txtEmail changed from " + ov + " to " + nv);

        FadeTransition ft = null;

        if( nv ) { // ok to show -> fade in
            ft = new FadeTransition(Duration.millis(500), vboxValid);
            ft.setFromValue(0.0d);
            ft.setToValue(1.0d);
        } else {

            if( !txtEmail.getText().isEmpty() ) {
                vboxValid.getStyleClass().clear();
                vboxValid.getStyleClass().add("vbox-valid-ok");
                lblValid.getStyleClass().clear();
                lblValid.getStyleClass().add("label-valid-ok");
                lblValid.setText("=");
            }

            ft = new FadeTransition(Duration.millis(1000), vboxValid);
            ft.setFromValue(1.0d);
            ft.setToValue(0.0d);
            ft.setOnFinished( (evt) -> {

                        if( !txtEmail.getText().isEmpty() ) {
                            vboxValid.getStyleClass().clear();
                            vboxValid.getStyleClass().add("vbox-valid-error");
                            lblValid.getStyleClass().clear();
                            lblValid.getStyleClass().add("label-valid-error");
                            lblValid.setText("X");
                        }
            });
        }
        ft.play();
    } );
}

okToAnimate is a custom binding on two conditions.  Order is important when using the or() method.

  • The Email TextField does not contain a valid email.
  • The Email TextField and the Confirm Email TextField don't match.

The Binding to the textProperty() updates the okToAnimate value when the TextField conditions change.  A ChangeListener is attached to okToAnimate.

The ChangeListener fades the red X when the user starts to enter the input.  That's because even with a quick two paste operations (CtrlV/CtrlV), the interaction begins with invalid data.  Once the user corrects the data, the red X changes over to a green = via a style class and a new Label.  Recall that this ChangeListener is only invoked when the condition changes rather than the data, so the ChangeListener is not called with each key press.

The Opacity of the red X / green = is animated to give a smoother transition.  FadeTransition is a convenience for this specific case.

In the old days, you might get a popup after-the-fact when submitting data.  These days, validations give instantaneous feedback to the user, with the best validations guiding the user to the correct input.  In this example, it's easy to see if the contents match, but this post could easily be extended to handle PasswordFields where matching contents couldn't be determined just by looking.

No comments:

Post a Comment