- Basic Transitions
- Compound Transitions
- Custom Transitions
- Conclusion
Custom Transitions
The previously presented transitions might meet all of your needs, or you might require a transition class that isn't available. In this situation, you can easily create your own transition class.
The documentation for the Transition class will help you get started. You need to subclass the abstract Transition class and (at minimum) implement the following method:
protected abstract void interpolate(double frac)
interpolate() is called for every animation frame while the transition runs. The argument passed to the frac parameter defines the current position within the animation. The fraction is 0.0 at the start and 1.0 at the end.
Transition's documentation presents a short code fragment that demonstrates a custom transition class. For convenience, I've reproduced this fragment below:
final String content = "Lorem ipsum"; final Text text = new Text(10, 20, ""); final Animation animation = new Transition() { { setCycleDuration(Duration.millis(2000)); } @Override protected void interpolate(double frac) { final int length = content.length(); final int n = Math.round(length * (float) frac); text.setText(content.substring(0, n)); } }; animation.play();
The fragment subclasses Transition with an anonymous class. In lieu of a constructor, the anonymous class uses an instance initializer to specify a default cycle duration of two seconds.
The overriding interpolate() method obtains the length of the content string and uses frac to identify the fraction of this length. This many characters are extracted from content and assigned to the Text node.
This transition results in a typewriter effect. Because the anonymous class is hardly reusable, I've created a TypewriterTransition class whose source code appears in Listing 11.
Listing 11—TypewriterTransition.java.
import javafx.animation.Transition; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectPropertyBase; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.text.Text; import javafx.util.Duration; public class TypewriterTransition extends Transition { public TypewriterTransition() { this(DEFAULT_DURATION, null, null); } public TypewriterTransition(Duration duration) { this(duration, null, null); } public TypewriterTransition(Duration duration, Text text, String string) { setDuration(duration); setText(text); setString(string); setCycleDuration(duration); } private ObjectProperty<Duration> duration; private static final Duration DEFAULT_DURATION = Duration.millis(400); public final void setDuration(Duration value) { if ((duration != null) || (!DEFAULT_DURATION.equals(value))) durationProperty().set(value); } public final Duration getDuration() { return (duration == null) ? DEFAULT_DURATION : duration.get(); } public final ObjectProperty<Duration> durationProperty() { if (duration == null) { duration = new ObjectPropertyBase<Duration>(DEFAULT_DURATION) { @Override public void invalidated() { try { setCycleDuration(getDuration()); } catch (IllegalArgumentException iae) { if (isBound()) unbind(); set(getCycleDuration()); throw iae; } } @Override public Object getBean() { return TypewriterTransition.this; } @Override public String getName() { return "duration"; } }; } return duration; } private StringProperty string; private static final String DEFAULT_STRING = null; public final void setString(String value) { if ((string != null) || (value != null /* DEFAULT_STRING */)) stringProperty().set(value); } public final String getString() { return (string == null) ? DEFAULT_STRING : string.get(); } public final StringProperty stringProperty() { if (string == null) string = new SimpleStringProperty(this, "string", DEFAULT_STRING); return string; } private ObjectProperty<Text> text; private static final Text DEFAULT_TEXT = null; public final void setText(Text value) { if ((text != null) || (value != null /* DEFAULT_TEXT */)) textProperty().set(value); } public final Text getText() { return (text == null) ? DEFAULT_TEXT : text.get(); } public final ObjectProperty<Text> textProperty() { if (text == null) text = new SimpleObjectProperty<Text>(this, "text", DEFAULT_TEXT); return text; } @Override protected void interpolate(double frac) { final int length = string.get().length(); final int n = Math.round(length * (float) frac); getText().setText(string.get().substring(0, n)); } }
Much of Listing 11 is based on code found in the existing transition classes. Instead of delving into how the code works, I'll focus on the three properties that are exposed:
- duration: the duration of an animation cycle (identical to what you've seen previously in this article)
- string: the String whose characters are extracted in a typewriter fashion
- text: the Text node on which the string's characters are appended
Listing 12 presents the source code to CustomTDemo, an application that centers a Text node on the stage and keeps it centered while a string of characters is typed onto this node.
Listing 12—CustomTDemo.java.
import javafx.animation.Animation; import javafx.animation.Transition; import javafx.application.Application; import javafx.beans.value.ChangeListener; import javafx.geometry.VPos; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.text.Font; import javafx.scene.text.Text; import javafx.stage.Stage; import javafx.util.Duration; public class CustomTDemo extends Application { final static int SCENE_WIDTH = 300; final static int SCENE_HEIGHT = 200; @Override public void start(Stage primaryStage) { final Text text = new Text(0, 0, ""); text.setFont(new Font("Arial Bold", 24)); text.setTextOrigin(VPos.TOP); TypewriterTransition tt = new TypewriterTransition(); tt.setDuration(new Duration(2000)); tt.setString("Lorem ipsum"); tt.setText(text); tt.setOnFinished(e -> { text.setText(""); tt.play(); }); Group root = new Group(); root.getChildren().add(text); Scene scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT); text.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> { text.setX((scene.getWidth() - newValue.getWidth()) / 2); text.setY((scene.getHeight() - newValue.getHeight()) / 2); }); primaryStage.setTitle("CustomTransition Demo"); primaryStage.setScene(scene); primaryStage.show(); tt.play(); } }
The start() method first creates a Text node and configures its font and text origin. It then instantiates TypewriterTransition such that each cycle lasts two seconds, and specifies that the text is to be erased when the cycle ends.
start() then creates the scenegraph and adds a change listener to the text node's layout bounds property so that it can reset the node's location (to keep it centered) whenever its text content changes.
Compile Listings 11 and 12 (javac CustomTDemo.java) and run the application (java CustomTDemo). Figure 10 shows part of the text centered on the scene's white background.
Figure 10 The typewriter transition causes text to appear one character at a time.