I've been trying to write an efficient 'Label' control to render text in a vertical scrolling way (marquee/ticker). I've written 2 versions (loosely based around some patterns I've seen on here) to try and improve the performance but I'm hoping someone can point me to a better way as both of these use about 4-9% CPU.
PC spec: (intel i7-7660U @2.50GHz)/16GB RAM/Graphics Card:Intel(R) Iris(TM) Plus Graphics 640/Win10.
While this would be acceptable if it was all I wanted my application to do they are going to be used in an existing large and complex application and there could be 30+ visible at any more time alongside the rest of detailed views.
The first version I tried uses TimeLine and is the closest to how I want it to look. It still needs to be refined so the text appears/disappears as wanted but it's close enough for benchmarking.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.CacheHint;
import javafx.scene.Scene;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TickerLabelTester extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
List<String> list1 = new ArrayList<String>(
Arrays.asList("Name 1",
"Name 2"));
List<String> list2 = new ArrayList<String>(
Arrays.asList("Name 3",
"Name 4"));
VBox root = new VBox(2.0);
root.setStyle("-fx-background-color:orange;");
for (int i = 0; i < 30; i++) {
TickerLabel tickerLabel = new TickerLabel();
if (i % 2 == 0) {
tickerLabel.setStrings(list1);
} else {
tickerLabel.setStrings(list2);
}
HBox hBox = new HBox(tickerLabel);
hBox.setBorder(new Border(new BorderStroke(Color.BLACK,
BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
root.getChildren().add(hBox);
}
root.setPrefSize(250, 700);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Scrolling Strings");
stage.show();
}
class TickerLabel extends Pane {
private Timeline timeline;
private Text text;
private List<String> strings = new ArrayList<String>();
public TickerLabel() {
Rectangle clip = new Rectangle();
this.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> {
clip.setWidth(newValue.getWidth());
clip.setHeight(newValue.getHeight());
});
this.setClip(clip);
text = new Text();
this.getChildren().add(text);
text.setCache(true);
text.setCacheHint(CacheHint.SPEED);
timeline = new Timeline();
timeline.setCycleCount(Timeline.INDEFINITE);
}
public void setStrings(List<String> strings) {
this.strings = strings;
rerunTransition();
}
private void rerunTransition() {
timeline.stop();
if (strings.size() > 0) {
recalculateTranslateTransition();
timeline.playFromStart();
}
}
private void recalculateTranslateTransition() {
Duration duration = Duration.ZERO;
double startPos = text.getLayoutBounds().getHeight() + 8;
for (String string : strings) {
KeyValue initKeyValue = new KeyValue(text.translateYProperty(), startPos);
KeyValue initTextKeyValue = new KeyValue(text.textProperty(), string);
KeyFrame initFrame = new KeyFrame(duration, initKeyValue, initTextKeyValue);
timeline.getKeyFrames().add(initFrame);
duration = Duration.seconds(duration.toSeconds() + 2);
KeyValue endKeyValue = new KeyValue(text.translateYProperty(), 0);
KeyFrame endFrame = new KeyFrame(duration, endKeyValue);
timeline.getKeyFrames().add(endFrame);
}
}
}
}
The second version uses a Canvas and a GraphicsContext. I'd not used the JavaFX Canvas before and I was hoping it might be like Swing/AWT where I could use some offscreen buffering to improve performance, unfortunately this version is less performant than the TimeLine. Before switching to the AnimationTimer shown below I had tried using my own Task/Service class to perform the rendering but this was less performant. Hopefully I'm missing something about how GraphicsContext should be used?
In the real application the List of Strings will be updated separately for each label from time to time so the TimeLine or AnimationTimer needs to pick the List change up.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.CacheHint;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class TickerCanvasLabelTester extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
List<String> list1 = new ArrayList<String>(
Arrays.asList("Name 1",
"Name 2"));
List<String> list2 = new ArrayList<String>(
Arrays.asList("Name 3",
"Name 4"));
VBox root = new VBox(2.0);
root.setStyle("-fx-background-color:orange;");
for (int i = 0; i < 30; i++) {
TickerLabel tickerLabel = new TickerLabel();
if (i % 2 == 0) {
tickerLabel.setStrings(list1);
} else {
tickerLabel.setStrings(list2);
}
HBox hBox = new HBox(tickerLabel);
hBox.setBorder(new Border(new BorderStroke(Color.BLACK,
BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
root.getChildren().add(hBox);
}
root.setPrefSize(250, 700);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Scrolling Strings");
stage.show();
}
class TickerLabel extends Pane {
private List<String> strings = new ArrayList<String>();
private int stringPointer = 0;
private Canvas canvas;
private GraphicsContext gc;
private AnimationTimer at;
private String string;
public TickerLabel() {
Rectangle clip = new Rectangle();
this.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> {
clip.setWidth(newValue.getWidth());
clip.setHeight(newValue.getHeight());
});
this.setClip(clip);
canvas = new Canvas();
canvas.setCache(true);
canvas.setCacheHint(CacheHint.SPEED);
canvas.setWidth(100);
canvas.setHeight(20);
gc = canvas.getGraphicsContext2D();
string = "";
at = new AnimationTimer() {
private long lastUpdate = 0;
private double i = canvas.getHeight() + 2;
@Override
public void handle(long now) {
// limit how often this is called
if (now - lastUpdate >= 200_000_000) {
if (i >= -2) {
gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
gc.strokeText(string, 0, i);
i--;
} else {
i = canvas.getHeight();
selectNextString();
}
lastUpdate = now;
}
}
};
this.getChildren().add(canvas);
}
public void setStrings(List<String> strings) {
this.strings = strings;
at.stop();
selectNextString();
at.start();
}
private void selectNextString() {
if (strings.size() > 0) {
string = strings.get(stringPointer);
if (stringPointer >= strings.size() - 1) {
stringPointer = 0;
} else {
stringPointer++;
}
}
}
}
}
Thanks in advance for any help.