Created
August 18, 2012 17:37
-
-
Save jewelsea/3388637 to your computer and use it in GitHub Desktop.
Refactored sample of an animated clock in JavaFX
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 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'; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@Ygarr Yes, this code is freeware and you can do with it as you like. Also, yes, this gist is quite old. The builder objects were deprecated and removed from JavaFX some time back, so the code no longer compiles. A more modern implementation would replace the builder usage with directly instantiating the related objects and setting the properties on them via setters and make user of lambdas where appropriate.