I need to make a short m4a sound file play continuously for 30 minutes at a designated start time. I am having difficulty to make it play continuously at the designated start time. When I researched this problem, it seems that I may have to use separate threads within JavaFX to make this work but I was unable to write it with threads such as Platform.runLater. I am also open to not having to use threads to achieve this effect. And I am unable to make it loop continuously for 30 minutes with the MediaPlayer methods provided within JavaFX (https://docs.oracle.com/javafx/2/api/javafx/scene/media/MediaPlayer.html).
Here is my code so far, which is only able to play the sound file once:
package MyAppPackage;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.scene.text.Text;
import javafx.application.Platform;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.util.Duration;
import java.io.File;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class MyApp extends Application {
private void playAppTone(MediaPlayer mp, ZonedDateTime start, ZonedDateTime stop) {
while(ZonedDateTime.now(ZoneId.of("America/New_York")).isAfter(start) &&
ZonedDateTime.now(ZoneId.of("America/New_York")).isBefore(stop) ) {
mp.setOnEndOfMedia(new Runnable() {
#Override public void run() {
mp.seek(Duration.ZERO);
mp.play(); }
});
mp.setAutoPlay(true);
}
}
#Override
public void start(Stage primaryStage) {
Canvas canvas = new Canvas(400,400);
HBox statusBar = new HBox(new Label("Start the App Tone"));
BorderPane borderPane = new BorderPane(canvas, statusBar, null, null, null);
StackPane root = new StackPane(borderPane);
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Timed Playback of Sound Clip");
primaryStage.show();
statusBar.setOnMouseClicked((event) -> {
ZoneId zoneId = ZoneId.of( "America/New_York" );
ZonedDateTime appStartTime = ZonedDateTime.now( zoneId );
System.out.println(appStartTime);
ZonedDateTime start = appStartTime.plusMinutes(10);
ZonedDateTime stop = appStartTime.plusMinutes(40);
String path = "media/PlaybackClip.m4a";
Text playStatusText = new Text("A sound clip will be played continuously at the designated time...");
Media media = new Media(new File(path).toURI().toString());
MediaPlayer mp = new MediaPlayer(media);
MediaView mv = new MediaView(mp);
StackPane stackPane = new StackPane();
stackPane.getChildren().add(mv);
stackPane.getChildren().add(playStatusText);
primaryStage.setScene(new Scene(stackPane, 400, 400));
mp.play();
mp.setAutoPlay(true);
/*The Platform.runLater doesn't work
Platform.runLater(() -> {
playAppTone(mp, start, stop);
}); */
});
}
public static void main(String[] args) {
launch(args);
}
}
I would avoid using another thread to implement this. Multiple threads will add unnecessary complexity, especially since everything involved needs to be (or at least can be) executed on the JavaFX Application Thread anyway. Instead, I would use a javafx.animation.Timeline with a delay. Animations keep everything on the FX thread and are a good way to delay or periodically execute (or both) simple actions on said thread.
Here's an example (some explanations in code comments):
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.stage.Stage;
import javafx.util.Duration;
public class App extends Application {
#Override
public void start(Stage primaryStage) {
MediaPlayer player = new MediaPlayer(new Media(/* YOUR MEDIA URL */));
/*
* Set to INDEFINITE since the media may not be long enough to play
* for the entire duration you want to. This will cause the media
* to loop until stopped.
*/
player.setCycleCount(MediaPlayer.INDEFINITE);
Timeline timeline =
new Timeline(
/*
* The first KeyFrame's time is set to Duration.ZERO in order
* to start playing the MediaPlayer immediately after the
* Timeline starts playing.
*/
new KeyFrame(Duration.ZERO, e -> player.play()),
/*
* The next KeyFrame's time is set to however long you want the
* media to play for. When the time elapses this KeyFrame will
* stop the MediaPlayer. Depending on your use case you may want
* to call MediaPlayer#dispose() to release its resources.
*/
new KeyFrame(Duration.minutes(30.0), e -> player.stop())
);
/*
* Set the delay of the Timeline to schedule playing the MediaPlayer
* at some point in the future. The duration used here should be equal
* to the duration between now and whatever wall clock time you want to
* start playing the media.
*
* The delay should be calculated and set just before calling play. If you wait
* then the delay will overshoot the wall clock time you expect the media to
* start playing.
*/
timeline.setDelay(durationUntil(LocalDateTime.now().plusSeconds(10L)));
timeline.play();
// provides some simple feedback
Label statusLabel = new Label();
statusLabel.textProperty().bind(player.statusProperty().asString("Current status: %s"));
primaryStage.setScene(new Scene(new StackPane(statusLabel), 500.0, 300.0));
primaryStage.show();
}
/**
* Computes and returns the duration between {#linkplain LocalDateTime#now() now} and {#code
* then}.
*
* #param then the time to compute the duration until
* #return the duration between now and {#code then}
* #throws IllegalArgumentException if {#code then} is in the past
*/
private static Duration durationUntil(LocalDateTime then) {
/*
* Note: Will use the system Clock which may not be the same Clock
* used to create 'then'. May want to add a way to specify
* a custom java.time.Clock if needed.
*/
LocalDateTime now = LocalDateTime.now();
if (then.isBefore(now)) {
throw new IllegalArgumentException("then < now");
}
// Note: Using javafx.util.Duration, not java.time.Duration
return Duration.millis(now.until(then, ChronoUnit.MILLIS));
}
}
Related
The test program below reproduces the problem. I understand why the exception is thrown but I would like to know how can I work around it or use a different construct in JavaFX to get what I want.
The full application is a robot simulator with multiple robots that move autonomously, independently, and simultaneously around a field. Each robot has its own SequentialTransition for its particular set of movements. The program adds the SequentialTransitions to a ParallelTransition, which it then plays. Everything was fine until I put in a listener that notices if a robot runs into an obstacle. I've simplified the collision detection in the test program to apply to only one robot and one wall. The point of the error is marked with //** BROKEN!! IllegalStateException on next line.
I really do want to stop the SequentialTransition for a robot that runs into an obstacle but let the other robot(s) continue. How can I do this?
The error comes up in Java 8 but also in Java 11 and JavaFX 15.
package sample;
import javafx.animation.ParallelTransition;
import javafx.animation.SequentialTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
private static final double FIELD_WIDTH = 600;
private static final double FIELD_HEIGHT = 600;
private Pane field = new Pane();
ParallelTransition parallel = new ParallelTransition();
SequentialTransition sequentialRobot1 = new SequentialTransition();
SequentialTransition sequentialRobot2 = new SequentialTransition();
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
// Place one field boundary for testing.
Line northBoundary = new Line(0, 0, FIELD_WIDTH, 0);
northBoundary.setStrokeWidth(5.0);
field.getChildren().add(northBoundary);
// Place the robots on the field.
// The first robot.
Rectangle robotBody1 = new Rectangle(100, 300, 60, 60);
robotBody1.setArcHeight(15);
robotBody1.setArcWidth(15);
robotBody1.setStroke(Color.BLACK);
robotBody1.setFill(Color.CRIMSON);
field.getChildren().add(robotBody1);
robotBody1.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
if (northBoundary.getBoundsInParent().intersects(robotBody1.getBoundsInParent())) {
//** BROKEN!! IllegalStateException on next line
sequentialRobot1.stop();
System.out.println("Collision detected");
parallel.play();
}
});
TranslateTransition translateTransition1 = new TranslateTransition();
translateTransition1.setNode(robotBody1);
translateTransition1.setByX(0);
translateTransition1.setByY(-300);
translateTransition1.setDuration(Duration.seconds(1));
translateTransition1.setOnFinished(event -> {
robotBody1.setLayoutX(robotBody1.getLayoutX() + robotBody1.getTranslateX());
robotBody1.setLayoutY(robotBody1.getLayoutY() + robotBody1.getTranslateY());
robotBody1.setTranslateX(0);
robotBody1.setTranslateY(0);
});
sequentialRobot1.getChildren().add(translateTransition1);
// The second robot.
Rectangle robotBody2 = new Rectangle(300, 300, 60, 60);
robotBody2.setArcHeight(15);
robotBody2.setArcWidth(15);
robotBody2.setStroke(Color.BLACK);
robotBody2.setFill(Color.CYAN);
field.getChildren().add(robotBody2);
TranslateTransition translateTransition2 = new TranslateTransition();
translateTransition2.setNode(robotBody2);
translateTransition2.setByX(0);
translateTransition2.setByY(-100);
translateTransition2.setDuration(Duration.seconds(1));
translateTransition2.setOnFinished(event -> {
robotBody2.setLayoutX(robotBody2.getLayoutX() + robotBody2.getTranslateX());
robotBody2.setLayoutY(robotBody2.getLayoutY() + robotBody2.getTranslateY());
robotBody2.setTranslateX(0);
robotBody2.setTranslateY(0);
});
sequentialRobot2.getChildren().add(translateTransition2);
parallel.getChildren().addAll(sequentialRobot1, sequentialRobot2);
parallel.play();
primaryStage.setTitle("Field");
primaryStage.setScene(new Scene(field, FIELD_WIDTH, FIELD_HEIGHT, Color.GRAY));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
#Slaw's idea of using an AnimationTimer is probably the best direction to go, but the obvious answer is to not use the ParallelTransition at all. Since the robot animations are going to be independent, just use the SequentialTransitions and start them all at the same time by calling play() on each.
I am trying to implement the Slider such that user can drag only by given increments. I tried in different ways by using the Slider API, but didnt get the desired results. Below is a quick demo of what I had tried. I am expecting to drag the thumb only in increments of 10 not with intermediate values. snapToTicks is doing what I required, but only after finishing the drag. I am trying to not move the thumb till the next desired block increment is reached.
Can anyone let me know how can i achieve this. Below is the screenshot while dragging.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class SliderDemo extends Application {
public static void main(String... args){
Application.launch(args);
}
#Override
public void start(Stage stage) throws Exception {
Label label = new Label();
label.setStyle("-fx-font-size:30px");
Slider slider = new Slider(5,240,5);
slider.setBlockIncrement(10);
slider.setMajorTickUnit(10);
slider.setMinorTickCount(0);
slider.setShowTickLabels(true);
slider.setShowTickMarks(true);
slider.setSnapToTicks(true);
slider.valueProperty().addListener((obs,old,val)->label.setText((int)Math.round(val.doubleValue())+""));
VBox root = new VBox(slider,label);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(20));
root.setSpacing(20);
Scene scene = new Scene(root,600,200);
stage.setScene(scene);
stage.show();
}
}
The solution is to set the value of the slider directly inside of the listener. The listener will not be called again
final ChangeListener<Number> numberChangeListener = (obs, old, val) -> {
final double roundedValue = Math.floor(val.doubleValue() / 10.0) * 10.0;
slider.valueProperty().set(roundedValue);
label.setText(Double.toString(roundedValue));
};
slider.valueProperty().addListener(numberChangeListener);
If you use Math.floor() instead of round you get a more intuatuive behavior of the thumb.
I a m creating a java application and I have a problem.
Here is the code.
package javastackoverflow;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class Javastackoverflow extends Application {
TextField deduct2;
Label text;
double ammount = 0.0;
#Override
public void start(Stage primaryStage) {
Button btn = new Button();
btn.setText("Apply");
text = new Label(Double.toString(ammount));
btn.setOnAction((e->{
double getamount = Double.parseDouble(deduct2.getText());
text.setText(Double.toString(getamount)+ ammount);
//this is where the program is suppose to get the amount and add it to amount, notice the + sign.
}))
;
deduct2 = new TextField();
FlowPane root = new FlowPane();
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(btn,deduct2,text);
Scene scene = new Scene(root, 400, 450);
primaryStage.setTitle("Yo Stack");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
What it is suppose to do
When the user enters a number into the textfield the program is suppose to
to take that number and add it to ammount = 0;
My problem
But when ever the using enters a new number the text changes to That number. Remember I would like it to add to the currant number like (currant number = 23; user enters new Number) new number = 3; I would like results to equal = 26 but right now the program shows the result 3;
What I think
I think the problem is in the onAction() method.
I think that the text.setText() method is displaying the currant text entered into the textfield, rather then adding it to ammount.
I alse don't think I am using the correct operator from this line of code. That may be part of the problem.
text.setText(Double.toString(getamount)+ ammount);
notice how I use the + sign, that + adds the getamount to ammount..or its supposed to. But when I change that plus sign to - or * I get this error
===============================
bad operand types for binary operator '*'
first type: String
second type: TextField
===============================
As you can probably tell I really would like this code to be correct so if you don't understand something please comment before you report me. Then I can change it fast. Thank you!
You are adding the amount to the getAmount variable when amount = 0.0
Try adding the new value to getAmount
package javastackoverflow;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class Javastackoverflow extends Application {
TextField deduct2;
Label text;
double getamount = 0.0; //Edit 1
#Override
public void start(Stage primaryStage) {
Button btn = new Button();
btn.setText("Apply");
text = new Label(Double.toString(ammount));
btn.setOnAction((e->{
getamount += Double.parseDouble(deduct2.getText()); //Edit 2
text.setText(Double.toString(getamount));
//this is where the program is suppose to get the amount and add it to amount, notice the + sign.
}))
;
deduct2 = new TextField();
FlowPane root = new FlowPane();
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(btn,deduct2,text);
Scene scene = new Scene(root, 400, 450);
primaryStage.setTitle("Yo Stack");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
Is there a way to determine the first and last visible row of a listview? In other words I'm looking for two indexes into an array that populates a listview which represent the top and the bottom row of the 'display window'.
You could get the VirtualFlow of the ListView which has methods for getting the first and last rows.
Example:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import com.sun.javafx.scene.control.skin.VirtualFlow;
public class ListViewSample extends Application {
#Override
public void start(Stage stage) {
VBox box = new VBox();
ListView<Integer> list = new ListView<>();
ObservableList<Integer> items = FXCollections.observableArrayList();
for( int i=0; i < 100; i++) {
items.add(i);
}
list.setItems(items);
box.getChildren().add(list);
VBox.setVgrow(list, Priority.ALWAYS);
Scene scene = new Scene(box, 200, 200);
stage.setScene(scene);
stage.show();
VirtualFlow flow = (VirtualFlow) list.lookup( ".virtual-flow");
flow.addEventFilter(Event.ANY, event -> {
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println( list.getItems().get( first.getIndex()) + " - " + list.getItems().get( last.getIndex()) );
});
}
public static void main(String[] args) {
launch(args);
}
}
You see the fully visible first and last items in the console.
ps: I leave the no data check and event handling to you
Alternate version without css lookup:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import com.sun.javafx.scene.control.skin.VirtualFlow;
public class ListViewSample extends Application {
ListView<String> list = new ListView<String>();
#Override
public void start(Stage stage) {
VBox box = new VBox();
ListView<Integer> list = new ListView<>();
ObservableList<Integer> items = FXCollections.observableArrayList();
for( int i=0; i < 100; i++) {
items.add(i);
}
list.setItems(items);
box.getChildren().add(list);
VBox.setVgrow(list, Priority.ALWAYS);
Scene scene = new Scene(box, 200, 200);
stage.setScene(scene);
stage.show();
VirtualFlow virtualFlow = null;
for( Node node: list.getChildrenUnmodifiable()) {
if( node instanceof VirtualFlow) {
virtualFlow = (VirtualFlow) node;
}
}
final VirtualFlow flow = virtualFlow;
flow.addEventFilter(Event.ANY, event -> {
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println( list.getItems().get( first.getIndex()) + " - " + list.getItems().get( last.getIndex()) );
});
}
public static void main(String[] args) {
launch(args);
}
}
UPDATE
VirtualFlow is available only after the ListView has been rendered, because it uses Layout parameters which are not available until after the ListView is visible on the stage. So I had to make sure that I got the VirtualFlow when it was certain that the ListView had been rendered. Since I was manipulating the list with various methods I call this method at the end of each method:
private VirtualFlow flow;
private void updateListView(int centreIndex) {
if (flow == null)
flow = (VirtualFlow) myListView.lookup(".virtual-flow");
if (flow != null){
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println(first.getIndex() + " - " + last.getIndex());
}
// Now the list can be selectively 'redrawn' using the scollTo() method,
// and using the .getSelectionModel().select(centreIndex) to set the
// desired cell
}
It's bit of a hack, but it works. Using layout parameters does have a drawback though that needs to be considered. If the height of the ListView is only 1 pixel less than the total height of all rows, n number of rows will be visible, but the flow will report n-1 rows which will appear to be a discrepancy at first. Hence keeping a fixed layout height is imperative. At least now by using scrollTo(..) I have control over the position of the selected item in the list (I want to keep it centred in the list display when an item is dragged through the list). This solution leaves me feeling uneasy, but it seems to be the only 'simple' way.
Just a note on the odd-looking logic. It seems that getting the flow takes time, while the program keeps executing. The second (flow != null) is necessary to avoid a NullPointerException.
UPDATE 2
My hack turns out not to work. The whole hack is dependent on timing. Rendering is done on a different thread and as soon as I changed the order of instantiation of classes in my app, I got a NullPointerException again. I turned to the Java doc:
"JavaFX is not thread safe and all JavaFX manipulation should be run on the JavaFX processing thread. If you allow a JavaFX application to interact with a thread other than the main processing thread, unpredictable errors will occur"
And they do! So forget the above - it does not work and will make you scratch your head (and more!) trying to debug it ;-)
Basically I am trying to make a short effect using JavaFX. I have the shape of a heart (added together from two circles and a polygon) that I can vary in size using the double value p. "Standart Size" would be p = 1.0;.
I am trying to add a pumping effect to the heart. I have the method pumpOnce():
public void pumpOnce(){
p = p + 1;
initHeart();
//Here goes what ever it takes to make stuff working!!
p = p - 1;
initHeart();
}
initHeart() draws the heart based on p.
I have found out that Thread.sleep(); or similar methods will not work due to the thread philosophy in JavaFX.
But what can I use instead?
The JavaFX animations are probably the way to go, but the "thread philosophy" in JavaFX isn't hard to work with if you want to roll your own, or do other, more complicated things in background threads.
The following code will pause and change the value in a label (full disclosure, I'm reusing code I wrote for another question):
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javax.xml.datatype.Duration;
public class DelayWithTask extends Application {
private static Label label;
public static void main(String[] args) { launch(args); }
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
label = new Label();
label.setText("Waiting...");
StackPane root = new StackPane();
root.getChildren().add(label);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
delay(5000, () -> label.setText("Hello World"));
}
public static void delay(long millis, Runnable continuation) {
Task<Void> sleeper = new Task<Void>() {
#Override
protected Void call() throws Exception {
try { Thread.sleep(millis); }
catch (InterruptedException e) { }
return null;
}
};
sleeper.setOnSucceeded(event -> continuation.run());
new Thread(sleeper).start();
}
}
The basic JavaFX background tool is the Task, any JavaFX application that actually does anything will probably be littered with these all over. Learn how to use them.
Dave's solution is great for general purpose off thread based work in JavaFX.
If you wish to use the animation facilities of JavaFX, the solutions below demonstrate this using a Timeline or a ScaleTransition. The timeline implements a discrete scale of the UI element, so every quarter of a second the UI element is scaled larger or back to it's original size. The scale transition implements a smooth scale of the UI element, so the UI element gradually gets larger then smaller using an interpolated scale factor with the default easing interpolator.
import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class BeatingHeart extends Application {
public static void main(String[] args) {
launch(args);
}
public void start(Stage stage) {
ImageView heart = new ImageView(HEART_IMAGE_LOC);
animateUsingTimeline(heart);
// animateUsingScaleTransition(heart);
StackPane layout = new StackPane(heart);
layout.setPrefWidth(heart.getImage().getWidth() * 2);
layout.setPrefHeight(heart.getImage().getHeight() * 2);
Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
}
private void animateUsingTimeline(ImageView heart) {
DoubleProperty scale = new SimpleDoubleProperty(1);
heart.scaleXProperty().bind(scale);
heart.scaleYProperty().bind(scale);
Timeline beat = new Timeline(
new KeyFrame(Duration.ZERO, event -> scale.setValue(1)),
new KeyFrame(Duration.seconds(0.5), event -> scale.setValue(1.1))
);
beat.setAutoReverse(true);
beat.setCycleCount(Timeline.INDEFINITE);
beat.play();
}
private void animateUsingScaleTransition(ImageView heart) {
ScaleTransition scaleTransition = new ScaleTransition(
Duration.seconds(1), heart
);
scaleTransition.setFromX(1);
scaleTransition.setFromY(1);
scaleTransition.setFromZ(1);
scaleTransition.setToX(1.1);
scaleTransition.setToY(1.1);
scaleTransition.setToZ(1.1);
scaleTransition.setAutoReverse(true);
scaleTransition.setCycleCount(Animation.INDEFINITE);
scaleTransition.play();
}
private static final String HEART_IMAGE_LOC =
"http://icons.iconarchive.com/icons/mirella-gabriele/valentine/128/Heart-red-icon.png";
// icon obtained from: http://www.iconarchive.com/show/valentine-icons-by-mirella-gabriele/Heart-red-icon.html
// icon license: Free for non-commercial use, commercial use not allowed.
}