Core Java & Languages

JavaFX-Animationen auf dem Prüfstand: Node oder Canvas?

Vergleich der Leistung von JavaFX-Nodes und Canvas auf verschiedenen Systemen.

Frank Delporte

JavaFX bietet vielseitige Möglichkeiten für Benutzeroberflächen, doch die Wahl zwischen Nodes und Canvas ist entscheidend für die Performance. Dieser Artikel vergleicht beide Methoden und zeigt, wie sich Animationen auf Geräten wie dem Raspberry Pi auswirken. Mit einer Demoanwendung wird demonstriert, wie Canvas die Effizienz steigern kann, um die richtige Komponente für leistungsstarke JavaFX-Anwendungen zu wählen.

Kürzlich habe ich mit der Installation eines JDK experimentiert, das JavaFX enthält. Das vereinfacht die Ausführung von JavaFX-Anwendungen, da man die JavaFX-Laufzeitumgebung nicht separat herunterladen muss, beispielsweise von der Gluon-Website [1]. Ich habe diese Experimente auf einem Raspberry Pi durchgeführt und eine kleine Testanwendung verwendet, die eine Menge sich bewegender Punkte auf den Bildschirm bringt. Dabei habe ich bemerkt, dass die Leistung bei vielen dieser Kreisobjekte langsamer wird.

Ich habe schon mehrmals gelesen, dass ein Canvas für diese Art von Anwendungsfall viel effizienter sein kann, und das hat mich dazu veranlasst, eine Testanwendung mit „Bouncing Balls“ (Abb. 1) zu erstellen, die es einfach macht, Nodes und Canvas zu vergleichen.

Abb. 1: Meine Testanwendung

Node versus Canvas

In JavaFX sind sowohl Nodes als auch Canvas Teil des Scene Graphs, aber sie haben unterschiedliche Use Cases. Die Wahl zwischen den beiden hängt oft von den spezifischen Anforderungen Ihrer Anwendung ab. Sie verwenden Nodes für statische Inhalte wie Eingabeformulare, Datentabellen, Dashboards mit Diagrammen … Das ist in der Regel bequemer und effizienter. Das Canvas bietet Ihnen mehr Flexibilität, wenn Sie dynamische oder benutzerdefinierte Inhalte erstellen müssen.

JavaFX Node

javafx.scene.Node ist die Basisklasse und alle visuellen JavaFX-Komponenten erweitern sie. Das geht mehrere „Schichten“ tief. Zum Beispiel Button > ButtonBase > Labeled > Control > Region > Parent > Node.

Zusammengefasst:

  • Ein Node in JavaFX repräsentiert ein Element des Scene Graph.
  • Dazu gehören UI-Steuerelemente wie Buttons, Labels, Text Fields, Shapes, Images, Media, Embedded Web Browser usw.
  • Jeder Node kann im 3D-Raum positioniert und transformiert werden, er kann Events handlen und es können Effekte auf ihn angewendet werden.
  • Node ist eine Basisklasse für alle visuellen Elemente.
  • Die Verwendung von Nodes wird als „Retained Mode Rendering“ bezeichnet.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Das sind einige typische Komponenten, die von Node abgeleitet sind:

Label label = new Label("Hello World!");
Button button = new Button("Click Me!");

JavaFX Canvas

javafx.scene.canvas erweitert ebenfalls Node, fügt aber spezielle Funktionen hinzu. Sie können Ihren eigenen Inhalt auf dem Canvas zeichnen, indem Sie eine Reihe von Grafikbefehlen verwenden, die von einem GraphicsContext bereitgestellt werden.

Zusammengefasst:

  • Sie zeichnen auf einem Canvas mit einem GraphicsContext.
  • Das direkte Zeichnen auf einem Canvas wird als „Immediate Mode Rendering“ bezeichnet.
  • Das gibt Ihnen mehr Flexibilität, ist aber weniger effizient, wenn sich der Inhalt nicht oft ändert.

In diesem Beispiel wird ein Rechteck gezeichnet:

Canvas canvas = new Canvas(400, 300);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.BLUE);
gc.fillRect(50, 50, 100, 70);

Demoanwendung

Die Demoanwendung kann im GitHub Gist unter [2] gefunden werden. Sie enthält Code, um eine Menge sich bewegender Kreise zu erzeugen – sowohl als Nodes als auch gezeichnet auf einem Canvas. Der Wert am Anfang des Codes definiert, welcher Ansatz verwendet wird:

private static int TYPE_OF_TEST = 1; // 1 = Nodes, 2 = Canvas

Nodes verwenden

Wenn Sie Nodes verwenden, wird dem Bildschirm ein Bereich hinzugefügt, in dem Bälle eingefügt werden. Bei jedem Ball handelt es sich um einen Circle Node mit einer Bewegungsmethode (Listing 1).

class BallNode extends Circle {
  private final Color randomColor = Color.color(Math.random(), 
    Math.random(), Math.random());
  private final int size = r.nextInt(1, 10);
  private double dx = r.nextInt(1, 5);
  private double dy = r.nextInt(1, 5);

  public BallNode() {
    this.setRadius(size / 2);
    this.setFill(randomColor);
    relocate(r.nextInt(380), r.nextInt(620));
  }

  public void move() {
     if (hitRightOrLeftEdge()) {
      dx *= -1; // Ball hit right or left 
    }
    if (hitTopOrBottom()) {
      dy *= -1; // Ball hit top or bottom
    }
    setLayoutX(getLayoutX() + dx);
    setLayoutY(getLayoutY() + dy);
  }

  ...
}

Canvas verwenden

Wenn Sie das Canvas verwenden, ist jeder Ball ein Datenobjekt, und alle Bälle werden bei jedem Tick auf das Canvas gezeichnet (Listing 2).

class BallDrawing {
  private final Color fill = Color.color(Math.random(), 
    Math.random(), Math.random());
  private final int size = r.nextInt(1, 10);
  private double x = r.nextInt(APP_WIDTH);
  private double y = r.nextInt(APP_HEIGHT - TOP_OFFSET);
  private double dx = r.nextInt(1, 5);
  private double dy = r.nextInt(1, 5);

  public void move() {
    if (hitRightOrLeftEdge()) {
      dx *= -1; // Ball hit right or left
    }
    if (hitTopOrBottom()) {
      dy *= -1; // Ball hit top or bottom
    }
    x += dx;
    y += dy;
  }

  ...
}

Verschieben der Objekte

Die Anwendung verwendet eine Timeline, um alle fünf Millisekunden weitere Objekte hinzuzufügen und sie zu verschieben (Listing 3).

Timeline timeline = new Timeline(new KeyFrame(Duration.millis(5), t -> onTick()));
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();

private void onTick() {
  if (TYPE_OF_TEST == 1) {
    // Add ball nodes to the pane
    for (var i = 0; i < ADD_BALLS_PER_TICK; i++) {
      paneBalls.getChildren().add(new BallNode());
    }

    // Move all the balls in the pane
    for (Node ballNode : paneBalls.getChildren()) {
      ((BallNode) ballNode).move();
    }
  } else if (TYPE_OF_TEST == 2) {
    // Add balls to the list of balls to be drawn
    for (var i = 0; i < ADD_BALLS_PER_TICK; i++) {
      ballDrawings.add(new BallDrawing());
    }
    
    // Clear the canvas (remove all the previously balls that were drawn)
    context.clearRect(0.0, 0.0, canvas.getWidth(), canvas.getHeight());

    // Move all the balls in the list, and draw them on the Canvas
    for (BallDrawing ballDrawing : ballDrawings) {
      ballDrawing.move();
      context.setFill(ballDrawing.getFill());
      context.fillOval(ballDrawing.getX(), ballDrawing.getY(),
        ballDrawing.getSize(),  ballDrawing.getSize());
    }
  }
}

Ausführen der Anwendung

Zum Ausführen der Anwendung habe ich folgenden Ansatz gewählt:

  • den Code in einer Datei FxNodesVersusCanvas.java speichern
  • eine Java-Laufzeitumgebung mit JavaFX installieren, z. B. von Azul Zulu [3] oder mit SDKMAN [4]: sdk install java 22.0.1.fx-zulu
  • JBang installieren, entweder von [5] oder mit SDKMAN: sdk install jbang
  • die Anwendung starten mit: jbang FxNodesVersusCanvas.java

Leistung im Vergleich

Natürlich hängt die Leistung vom System ab, auf dem Sie die Anwendung ausführen. Wie Sie im Video unter [6] und in Abbildung 2 sehen können, habe ich es sowohl auf einem Apple Mac Studio als auch auf einem Raspberry Pi 5 ausgeführt. Das Ergebnis ist konsistent, da man ungefähr zehnmal mehr Objekte zum Canvas verglichen mit der Anzahl der Nodes hinzufügen kann, bevor die Framerate einbricht. Das ist kein „wissenschaftliches Ergebnis“, aber es vermittelt einen guten Eindruck davon, was mit Canvas erreicht werden kann.

  • Raspberry Pi wird bei 3k Nodes deutlich langsamer als bei 30k Nodes auf Canvas
  • Mac wird bei 15k Nodes langsamer als bei 150k auf Canvas

Das Bild zeigt zwei nebeneinander angeordnete JavaFX-Demos mit der Überschrift „JavaFX Demo, Nodes versus Canvas“. Beide Fenster zeigen eine große Menge bunter Punkte, die zufällig auf weißem Hintergrund verteilt sind. In der oberen Zeile wird Java-Version 17.0.1 und JavaFX-Version 22.0.1 verwendet. Das linke Fenster zeigt eine Anzahl von 7.210 Punkten und eine Bildrate von 5 FPS (Frames per Second), während das rechte Fenster 7.232 Punkte mit einer Bildrate von 16 FPS anzeigt. Es handelt sich offenbar um einen Vergleich der Leistung von JavaFX-Nodes versus Canvas.

Abb. 2: Das laufende Experiment

Fazit

Eine große Anzahl visueller Komponenten in einer typischen JavaFX-Benutzeroberfläche würde eine schlecht gestaltete Anwendung darstellen. Stellen Sie sich ein langes Registrierungsformular mit Hunderten von Eingabefeldern und Beschriftungen vor … Das würde Ihre Benutzer in den Wahnsinn treiben. Aber in anderen Fällen, in denen Sie eine komplexe Animation oder eine fortgeschrittene Benutzerschnittstellenkomponente erzeugen wollen, ist die Möglichkeit, auf dem Canvas zu zeichnen, ein idealer Ansatz.


Links & Literatur

[1] https://gluonhq.com/products/javafx/

[2] https://gist.github.com/FDelporte/c74cdf59ecd9ef1b14df86e08faa0c56

[3] https://www.azul.com/downloads/?package=jdk-fx#zulu

[4] https://sdkman.io

[5] https://www.jbang.dev

[6] https://www.youtube.com/watch?v=nJGRW5xP_AE

[7] https://leanpub.com/gettingstartedwithjavaontheraspberrypi/

[8] https://www.elektor.com/getting-started-with-java-on-the-raspberry-pi

Top Articles About Core Java & Languages

Alle News der Java-Welt:

Behind the Tracks

Agile, People & Culture
Teamwork & Methoden

Clouds & Kubernetes
Alles rund um Cloud

Core Java & Languages
Ausblicke & Best Practices

Data & Machine Learning
Speicherung, Processing & mehr

DevOps & CI/CD
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Software-Architektur
Best Practices

Web & JavaScript
JS & Webtechnologien

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management

ALLE NEWS ZUR JAX!