Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Refactored sample of an animated clock in JavaFX
/** analogue-clock.css (place in the same directory as AnalogueClock.java and ensure build script copies the file to the same location as AnalogueClock.class) */
#face {
-fx-fill: radial-gradient(radius 180%, burlywood, derive(burlywood, -30%), derive(burlywood, 30%));
-fx-stroke: derive(burlywood, -45%);
-fx-stroke-width: 5;
-fx-effect: dropshadow(three-pass-box, grey, 10, 0, 4, 4);
}
#brand {
-fx-font-size: 14px;
}
#hourHand {
-fx-stroke: darkslategray;
-fx-stroke-width: 4;
-fx-stroke-line-cap: round;
}
#minuteHand {
-fx-stroke: derive(darkslategray, -5%);
-fx-stroke-width: 3;
-fx-stroke-line-cap: round;
}
#secondHand {
-fx-stroke: derive(firebrick, -15%);
-fx-stroke-width: 2;
-fx-stroke-line-cap: round;
}
#spindle {
-fx-fill: derive(darkslategray, +5%);
}
.tick {
-fx-stroke: derive(darkgoldenrod, -15%);
-fx-stroke-width: 3;
-fx-stroke-line-cap: round;
}
import javafx.animation.*;
import javafx.beans.property.DoubleProperty;
import javafx.scene.Group;
import javafx.scene.control.Label;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;
import java.util.Calendar;
/**
* Displays an animated AnalogueClock face.
* Time is the system time for the local timezone.
* see: analogue-clock.css for css formatting rules for the clock.
*/
public class AnalogueClock extends Group {
final int HOUR_HAND_LENGTH = 50;
final int MINUTE_HAND_LENGTH = 75;
final int SECOND_HAND_LENGTH = 88;
final int SECOND_HAND_OFFSET = 15;
AnalogueClock(String brandName, double clockRadius) {
setId("analogueClock");
getStylesheets().add(
ResourceResolver.getResourceFor(
getClass(),
"analogue-clock.css"
)
);
// construct the analogueClock pieces.
final Circle face = createClockFace(clockRadius);
final Label brand = createBrand(face, brandName);
final Line hourHand = createHand(
"hourHand",
clockRadius,
0,
percentOf(HOUR_HAND_LENGTH, clockRadius)
);
final Line minuteHand = createHand(
"minuteHand",
clockRadius,
0,
percentOf(MINUTE_HAND_LENGTH, clockRadius)
);
final Line secondHand = createHand(
"secondHand",
clockRadius,
percentOf(SECOND_HAND_OFFSET, clockRadius),
percentOf(SECOND_HAND_LENGTH, clockRadius)
);
// animate the hands with the time.
bindClockHandsToTime(hourHand, minuteHand, secondHand);
getChildren().addAll(
face,
brand,
createTicks(clockRadius),
createSpindle(clockRadius),
hourHand,
minuteHand,
secondHand
);
}
/** @return radial ticks around the clock center to mark time. */
private Group createTicks(double clockRadius) {
final double TICK_START_OFFSET = percentOf(83, clockRadius);
final double TICK_END_OFFSET = percentOf(93, clockRadius);
final Group ticks = new Group();
for (int i = 0; i < 12; i++) {
Line tick = new Line(0, -TICK_START_OFFSET, 0, -TICK_END_OFFSET);
tick.getStyleClass().add("tick");
tick.setLayoutX(clockRadius);
tick.setLayoutY(clockRadius);
tick.getTransforms().add(new Rotate(i * (360 / 12)));
ticks.getChildren().add(tick);
}
return ticks;
}
/** @return a rendered spindle around which the clockwork rotates */
private Circle createSpindle(double clockRadius) {
final Circle spindle = new Circle(clockRadius, clockRadius, 5);
spindle.setId("spindle");
return spindle;
}
private Circle createClockFace(double clockRadius) {
final Circle face = new Circle(clockRadius, clockRadius, clockRadius);
face.setId("face");
return face;
}
private Line createHand(String handId, double clockRadius, double handOffsetLength, double handLength) {
final Line secondHand = new Line(0, handOffsetLength, 0, -handLength);
secondHand.setLayoutX(clockRadius);
secondHand.setLayoutY(clockRadius);
secondHand.setId(handId);
return secondHand;
}
private Label createBrand(Circle face, String brandName) {
final Label brand = new Label(brandName);
brand.setId("brand");
brand.layoutXProperty().bind(face.centerXProperty().subtract(brand.widthProperty().divide(2)));
brand.layoutYProperty().bind(face.centerYProperty().add(face.radiusProperty().divide(2)));
return brand;
}
private void bindClockHandsToTime(final Line hourHand, final Line minuteHand, final Line secondHand) {
// determine initial rotation for the clock hands.
Calendar time = Calendar.getInstance();
final double initialHourhandDegrees = calculateHourHandDegrees(time);
final double initialMinuteHandDegrees = calculateMinuteHandDegrees(time);
final double initialSecondHandDegrees = calculateSecondHandDegrees(time);
// animate the clock movements using timelines.
createRotationTimeline( // the hour hand rotates twice a day.
createRotate(hourHand, initialHourhandDegrees).angleProperty(),
Duration.hours(12),
initialHourhandDegrees
);
createRotationTimeline( // the minute hand rotates once an hour.
createRotate(minuteHand, initialMinuteHandDegrees).angleProperty(),
Duration.minutes(60),
initialMinuteHandDegrees
);
createRotationTimeline( // move second hand rotates once a minute.
createRotate(secondHand, initialSecondHandDegrees).angleProperty(),
Duration.seconds(60),
initialSecondHandDegrees
);
}
private Rotate createRotate(Line hand, double initialHandDegrees) {
final Rotate hourRotate = new Rotate(initialHandDegrees);
hand.getTransforms().add(hourRotate);
return hourRotate;
}
/**
* Performs a 360 degree rotation of the angleProperty once in every duration.
* rotation starts from initialRotation degrees.
*/
private void createRotationTimeline(DoubleProperty angleProperty, Duration duration, double initialRotation) {
Timeline timeline = new Timeline(
new KeyFrame(
duration,
new KeyValue(
angleProperty,
360 + initialRotation,
Interpolator.LINEAR
)
)
);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
private int calculateSecondHandDegrees(Calendar time) {
return time.get(Calendar.SECOND) * (360 / 60);
}
private double calculateMinuteHandDegrees(Calendar time) {
return (time.get(Calendar.MINUTE) + calculateSecondHandDegrees(time) / 360.0) * (360 / 60);
}
private double calculateHourHandDegrees(Calendar time) {
return (time.get(Calendar.HOUR) + calculateMinuteHandDegrees(time) / 360.0) * (360 / 12);
}
private double percentOf(double percent, double clockRadius) {
return percent / 100 * clockRadius;
}
}
/** clockdemo.css (place in the same directory as ClockDemo.java and ensure build script copies the file to the same location as ClockDemo.class) */
#layout {
-fx-padding: 5;
}
#clocks {
-fx-padding: 8;
}
#backdrop {
-fx-background-color: linear-gradient(to bottom, cornsilk, wheat);
-fx-effect: dropshadow(three-pass-box, wheat, 10, 0, 0, 0);
}
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPaneBuilder;
import javafx.scene.layout.VBoxBuilder;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/** Demonstrates drawing a clock in JavaFX */
public class ClockDemo extends Application {
final String BRAND_NAME = "Splotch";
final double CLOCK_RADIUS = 100;
public static void main(String[] args) throws Exception {
launch(args);
}
public void start(final Stage stage) throws Exception {
// create the scene elements.
final Pane backdrop = makeBackdrop("backdrop", stage);
// layout the scene.
final Scene scene = createClockScene(
StackPaneBuilder.create()
.id("layout")
.children(
backdrop,
VBoxBuilder.create()
.id("clocks")
.pickOnBounds(false)
.children(
makeAnalogueClock(stage),
new DigitalClock())
.alignment(Pos.CENTER)
.build()
)
.build()
);
// size the backdrop to the scene.
sizeToScene(backdrop, scene);
// show the scene.
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
stage.show();
}
private AnalogueClock makeAnalogueClock(Stage stage) {
final AnalogueClock analogueClock = new AnalogueClock(BRAND_NAME, CLOCK_RADIUS);
EffectUtilities.addGlowOnHover(analogueClock);
EffectUtilities.fadeOnClick(analogueClock, closeStageEventHandler(stage));
return analogueClock;
}
private void sizeToScene(Pane pane, Scene scene) {
pane.prefWidthProperty().bind(scene.widthProperty());
pane.prefHeightProperty().bind(scene.heightProperty());
}
private Scene createClockScene(Parent layout) {
final Scene scene = new Scene(layout, Color.TRANSPARENT);
scene.getStylesheets().add(
ResourceResolver.getResourceFor(
ClockDemo.class,
"clock-demo.css"
)
);
return scene;
}
private EventHandler<ActionEvent> closeStageEventHandler(final Stage stage) {
return new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent actionEvent) {
stage.close();
}
};
}
private Pane makeBackdrop(String id, Stage stage) {
Pane backdrop = new Pane();
backdrop.setId(id);
EffectUtilities.makeDraggable(stage, backdrop);
return backdrop;
}
}
/** digitalclock.css (place in the same directory as DigitalClock.java and ensure build script copies the file to the same location as DigitalClock.class) */
#digitalClock {
-fx-font-size: 14px;
-fx-font-family: 'Courier New';
}
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.Label;
import javafx.util.Duration;
import java.util.Calendar;
/**
* Creates a digital clock display as a simple label.
* Format of the clock display is hh:mm:ss aa, where:
* hh Hour in am/pm (1-12)
* mm Minute in hour
* ss Second in minute
* aa Am/pm marker
* Time is the system time for the local timezone.
* see: digital-clock.css for css formatting rules for the clock.
*/
public class DigitalClock extends Label {
public DigitalClock() {
setId("digitalClock");
getStylesheets().add(
ResourceResolver.getResourceFor(
getClass(),
"digital-clock.css"
)
);
bindToTime();
}
// the digital clock updates once a second.
private void bindToTime() {
Timeline timeline = new Timeline(
new KeyFrame(Duration.seconds(0),
new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent actionEvent) {
Calendar time = Calendar.getInstance();
String hourString = StringUtilities.pad(2, ' ', time.get(Calendar.HOUR) == 0 ? "12" : time.get(Calendar.HOUR) + "");
String minuteString = StringUtilities.pad(2, '0', time.get(Calendar.MINUTE) + "");
String secondString = StringUtilities.pad(2, '0', time.get(Calendar.SECOND) + "");
String ampmString = time.get(Calendar.AM_PM) == Calendar.AM ? "AM" : "PM";
setText(hourString + ":" + minuteString + ":" + secondString + " " + ampmString);
}
}
),
new KeyFrame(Duration.seconds(1))
);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
}
import javafx.animation.FadeTransition;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.effect.Glow;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import javafx.util.Duration;
/** Various utilities for applying different effects to nodes. */
public class EffectUtilities {
/** configures the node to fade when it is clicked on performed the onFinished handler when the fade is complete */
public static void fadeOnClick(final Node node, final EventHandler<ActionEvent> onFinished) {
node.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
node.setMouseTransparent(true);
FadeTransition fade = new FadeTransition(Duration.seconds(1.2), node);
fade.setOnFinished(onFinished);
fade.setFromValue(1);
fade.setToValue(0);
fade.play();
}
});
}
/* adds a glow effect to a node when the mouse is hovered over the node */
public static void addGlowOnHover(final Node node) {
final Glow glow = new Glow();
node.setOnMouseEntered(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
node.setEffect(glow);
}
});
node.setOnMouseExited(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
node.setEffect(null);
}
});
}
/** makes a stage draggable using a given node */
public static void makeDraggable(final Stage stage, final Node byNode) {
final Delta dragDelta = new Delta();
byNode.setOnMousePressed(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
// record a delta distance for the drag and drop operation.
dragDelta.x = stage.getX() - mouseEvent.getScreenX();
dragDelta.y = stage.getY() - mouseEvent.getScreenY();
byNode.setCursor(Cursor.MOVE);
}
});
byNode.setOnMouseReleased(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
byNode.setCursor(Cursor.HAND);
}
});
byNode.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
stage.setX(mouseEvent.getScreenX() + dragDelta.x);
stage.setY(mouseEvent.getScreenY() + dragDelta.y);
}
});
byNode.setOnMouseEntered(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
byNode.setCursor(Cursor.HAND);
}
}
});
byNode.setOnMouseExited(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
byNode.setCursor(Cursor.DEFAULT);
}
}
});
}
/** records relative x and y co-ordinates. */
private static class Delta {
double x, y;
}
}
import java.net.URL;
public class ResourceResolver {
/**
* Retrieves an absolute resource path from a path relative to the location of the specified class.
* The requested resource must exist otherwise this method will throw an IllegalArgumentException.
* @param clazz a path relative to the location of this class.
* @param path a path relative to the location of this class.
* @return the absolute resource path.
* @throws IllegalArgumentException if a resource at the specified path does not exist.
*/
public static String getResourceFor(Class clazz, String path) {
URL resourceURL = clazz.getResource(path);
if (resourceURL == null) {
throw new IllegalArgumentException("No resource exists at: " + path + " relative to " + clazz.getName());
}
return resourceURL.toExternalForm();
}
}
public class StringUtilities {
/**
* Creates a string left padded to the specified width with the supplied padding character.
* @param fieldWidth the length of the resultant padded string.
* @param padChar a character to use for padding the string.
* @param s the string to be padded.
* @return the padded string.
*/
public static String pad(int fieldWidth, char padChar, String s) {
StringBuilder sb = new StringBuilder();
for (int i = s.length(); i < fieldWidth; i++) {
sb.append(padChar);
}
sb.append(s);
return sb.toString();
}
}
@jewelsea

This comment has been minimized.

Copy link
Owner Author

@jewelsea jewelsea commented Aug 18, 2012

This gist has been created in response to Per Lundholm's criticism of the coding style used in this Clock demonstration example code. Functionally, in terms of user experience, both clocks are identical. This refactored Clock demonstration code follows the principles espoused in Per's blog entry on JavaFX coding style. Should you have time, I'd appreciate it if you can view both this refactored code version and the original code version and comment on Per's blogs as to which coding style you prefer and why.

@JBKMG

This comment has been minimized.

Copy link

@JBKMG JBKMG commented May 21, 2013

line 28 in DigitalClock.java is = "digital-clock.css" while the css file is declared as 'digitalclock.css'.
line 71 in ClockDemo.java is = "clock-demo.css" while the css file is declared as 'clockdemo.css'.

If this was done on purpose for demonstration of ResourceResolver class then please disregard.

Thank you for your examples, they are quite helpful.

  • Jeff
@jewelsea

This comment has been minimized.

Copy link
Owner Author

@jewelsea jewelsea commented Aug 9, 2013

Thanks for pointing out, the missing hyphens in the filenames JBKMG.
That was a typo, not a clever demo of the ResourceResolver :-)

Unfortunately the github editor is broken for this gist and won't let me rename the files.

If the files are manually renamed as digital-clock.css and clock-demo.css, then everything should work as expected.

@ProfessorX

This comment has been minimized.

Copy link

@ProfessorX ProfessorX commented Jun 26, 2018

VERY GOOD.

100/100.

THANKS!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.