JavaFX Animation and Binding: Using the ProgressBar

Our previous post describes a JavaFX countdown timer. Let’s take that same program and add a progress bar control. As the timer counts down, the progress bar gradually fills in—as shown in the screenshot here. When the timer begins (at 15), the progress bar is empty (all gray) and when the timer reaches 0, the progress bar is filled in (all blue).

FXTimer with Progress Bar Screenshot

FXTimer with Progress Bar Screenshot

The progress bar includes progressProperty, a DoubleProperty with values that range between 0 (no progress or 0% complete) and 1 (progress complete or 100%). Half-way is 0.5.

Recall that in our previous post, we bind the numeric label timerLabel to the timer property (timeSeconds). Now to configure the progress bar, we bind its progressProperty to the timer as well. With binding, we don’t have to update progressProperty in the timeline’s event handler.

 

Here’s our first attempt.

private static final Integer STARTTIME = 15;
private IntegerProperty timeSeconds =
        new SimpleIntegerProperty(STARTTIME);
. . .
// Instantiate progressbar and configure binding
ProgressBar progressBar = new ProgressBar();
progressBar.progressProperty().bind(
                timeSeconds.divide(STARTTIME*1.0));
. . .
// Add to vertical box layout component
vb.getChildren().addAll(button, timerLabel, progressBar);
. . .

This is a nice try, but not the result we want at all! We instantiate the progress bar in line 6 and specify its binding in lines 7-8. Recall that timeSeconds is an IntegerProperty, so we must use Property method divide() to make sure the binding is a Double whose range is between 0 and 1. Can you guess the outcome?

There are two problems here. First, because our timer updates once a second, the progress bar chunks through (in 15 evenly spaced chunks) instead of filling smoothly. Second, the progress bar begins filled and empties as the timer counts down (we want the opposite behavior). To fix these problems, we must do the following:

  • Make our timeline cycle faster. Although the label should only change once per second, the progress bar should be updated more often. We do this by making timeSeconds 1500 instead of 15. Then we divide by 100 when binding to timerLabel.
  • Correlate the starting value of the timer (15) to zero for the progress bar and the ending value of the timer (0) to 1.0 for the progress bar. This fills the progress bar as the timer counts down. (In other words, a little bit of math will help us!)

Here’s the updated code.

private static final Integer STARTTIME = 15;
private IntegerProperty timeSeconds =
        new SimpleIntegerProperty(STARTTIME*100);
. . .
// Bind the timerLabel textProperty
// to the timeSeconds property
timerLabel.textProperty().bind(
        timeSeconds.divide(100).asString());
. . .
// Bind the progressBar progressProperty
// to the timeSeconds property
ProgressBar progressBar = new ProgressBar();
progressBar.progressProperty().bind(
        timeSeconds.divide(STARTTIME*100.0)
        .subtract(1).multiply(-1));
. . .
// Inside the button event handler
// Make the timeline duration STARTTIME seconds
timeline.getKeyFrames().add(
        timeSeconds.set((STARTTIME+1)*100);
        timeline = new Timeline();
        timeline.getKeyFrames().add(

                new KeyFrame(Duration.seconds(STARTTIME+1),
                new KeyValue(timeSeconds, 0)));
        timeline.playFromStart();
. . .

This provides the behavior we want. Let’s review these changes.

  • IntegerProperty timeSeconds now starts off at 1500 instead of 15 (100 times the original initial value) (lines 2-3).
  • To compensate for this change to timeSeconds, we must divide timeSeconds by 100 to correctly configure the timerLabel binding (lines 7-8).
  • We divide timeSeconds by 1500 to calibrate the progress bar. The values now progress from 0% complete to 100% since we subtract one and multiply by -1 (lines 13-15).

Download the FXTimer with ProgressBar JavaFX source code here.

4 Comments

  1. Pingback: JavaFX links of the week, October 24 // JavaFX News, Demos and Insight // FX Experience

  2. A couple of thoughts:

    First, thank you very much for this blog post/tutorial. Your writing is concise and your focus on one “feature” makes it much easier to comprehend and build skills with JavaFX.

    Second, I have been using GWT for about a year and despite its ability to make stunning web apps, I have found myself fighting against the framework to get relatively simple things done (especially the new RequestFactory and Editor frameworks). JavaFX looks like a nice alternative (except that it runs as a plugin instead of a website which is both a hindrance for user adoption and a great help for producing an app in a much shorter time span). Do you have or know of any references on how to structure JavaFX apps as web-apps (i.e. that pull and push data to a server)?

    Thirdly, I noticed you have not utilized FXML in these posts. While I find it adds a little complexity it seems to separate concerns quite nicely. I have redone this Timer post in FXML if you are interested.

    • Hi Mike,

      Thanks for taking the time to leave a comment! I appreciate the feedback.

      After I quick search I found the following set of tutorials which explores building a JavaFX enterprise application here: http://www.zenjava.com/series/building-jee-applications-in-javafx-2-0/. The material looks very helpful.

      Yes, I agree using FXML helps separate the view and lets you build an MVC-structured app. Also, the FXML lets you see the hierarchical arrangement of your scenegraph–which helps with maintenance. Here is a link for readers who would like to explore FXML further: http://docs.oracle.com/javafx/2.0/fxml_get_started/why_use_fxml.htm.

      As you noted, I was focusing on the JavaFX API in this post. By all means, I encourage you to post the FXML for this Timer example. I think readers would like to see the implementation differences.

      Again, thanks for the feedback.

  3. Here is the FXML version (I also just tried converting your sketchpad app with a bit more bells and whistles in the FXML and it works fairly well). One thing to note is that there doesn’t seem to be a way to get a hold of the scene variable to base your components’ sizes off of it. So instead I just hardcoded it either into the VBox or made a javascript with variables set to those values and then referenced those in the components (let me know if there is a better way to do this).

    Timer.java:

    package timer;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Group;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class Timer extends Application
    {
       public static void main(String[] args)
       {
          Application.launch(args);
       }
    
       @Override
       public void start(Stage primaryStage) throws Exception
       {
          primaryStage.setTitle("FX Timer Binding/ProgressBar");
    
          Group root = FXMLLoader.load(getClass().getResource("Timer.fxml"));
    
          Scene scene = new Scene(root);
          primaryStage.setScene(scene);
          primaryStage.show();
       }
    }
    

    Timer.fxml:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.Group?>
    <?import javafx.scene.layout.VBox?>
    <?import javafx.scene.control.Button?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.control.ProgressBar?>
    <?import javafx.scene.paint.Color?>
    <?import javafx.geometry.Pos?>
    
    <Group xmlns:fx="http://javafx.com/fxml" fx:controller="timer.TimerController" >
    	<children>
    		<VBox spacing="20" alignment="CENTER" prefWidth="300" prefHeight="250">
    			<children>
    				<Button fx:id="buttonStart" text="Start Timer" onAction="#handleStartAction" />
    				<Label fx:id="timerLabel" text="15" textFill="RED" style="-fx-font-size: 4em;"/>
    				<ProgressBar fx:id="progressBar"/>
    			</children>
    		</VBox>
    	</children>
    </Group>
    

    TimerController.java

    package timer;
    
    import java.net.URL;
    import java.util.ResourceBundle;
    
    import javafx.animation.KeyFrame;
    import javafx.animation.KeyValue;
    import javafx.animation.Timeline;
    import javafx.beans.property.IntegerProperty;
    import javafx.beans.property.SimpleIntegerProperty;
    import javafx.event.ActionEvent;
    import javafx.fxml.FXML;
    import javafx.fxml.Initializable;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.control.ProgressBar;
    import javafx.util.Duration;
    
    public class TimerController implements Initializable
    {
       private static final Integer STARTTIME   = 15;
    
       private IntegerProperty      timeSeconds = new SimpleIntegerProperty(STARTTIME * 100);
    
       private Timeline             timeline;
       
       @FXML
       private Button               buttonStart;
    
       @FXML
       private Label                timerLabel;
    
       @FXML
       private ProgressBar          progressBar;
    
       @Override
       public void initialize(URL location, ResourceBundle resources)
       {
          timerLabel.textProperty().bind(timeSeconds.divide(100).asString());
          progressBar.progressProperty().bind(timeSeconds.divide(STARTTIME * 100.0).subtract(1).multiply(-1));
       }
    
       @FXML
       protected void handleStartAction(ActionEvent event)
       {
          if (timeline != null)
          {
             timeline.stop();
          }
          timeSeconds.set((STARTTIME + 1) * 100);
          timeline = new Timeline();
          timeline.getKeyFrames().add(
    
          new KeyFrame(Duration.seconds(STARTTIME + 1), new KeyValue(timeSeconds, 0)));
          timeline.playFromStart();
       }
    }
    
    

Comments are closed.