IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Voir le flux RSS

bouye

[Actualité] Les transitions CSS en JavaFX (partie 1)

Noter ce billet
par , 31/10/2024 à 06h31 (4680 Affichages)
Parmi les notes de mises a jour de JavaFX 23 on peut voir l'info suivante qui s'y est glissée en catimini :

Citation Envoyé par https://gluonhq.com/products/javafx/openjfx-23-release-notes/#23
List of New Features
Issue key Summary Subcomponent
[...]
JDK-8311895 CSS Transitions graphics
Citation Envoyé par https://openjfx.io/highlights/23/
  • CSS Transition support in JavaFX, making it easy to define animated transitions for creating rich and fluid user experiences
Bien que le ticket sur le JDK Bug System date de 2023, on peut trouver des tickets plus anciens datant de 2010 et de 2014 qui demandent essentiellement la meme chose : permettre de définir des animations dans les feuilles de style. Autant dire que c’était une fonctionnalité simple (d'apparence) mais qui était attendue de longue date.

Je ne vais pas m’étendre en long et en large sur les limitations de la syntaxe, des propriétés supportées, ni sur les capacités à capturer les événements d'animation CSS depuis le code, l'excellent article (en anglais) "CSS Transitions" disponible sur PragmaticCode fait tout cela beaucoup plus en détails. L'important est de savoir que, grosso modo, les transitions CSS JavaFX sont inspirées des transitions CSS pour le web définies par par le W3.org.

Placer des transitions (animations) légères dans le fichier CSS permet d'enrichir la présentation visuelle sans devoir alourdir de code du composant. C'est une autre manière de séparer la vue (la presentation) des classes métier, tout comme le fait de placer le design des formulaires dans un fichier FXML externe et de se contenter de mettre le code permettant de faire fonctionner ce formulaire dans une classe contrôleur. Ceci dit rien n'oblige à utiliser un FXML pour mettre en oeuvre ces transitions, elle fonctionnent très bien en l’état sur des écrans définis dans du code Java pur (ou Kotlin ou autre) aussi.

Mise en place du projet
Commençons donc par écrire un simple programme JavaFX qui contient juste une scène, contenant un gestionnaire de mise en page de type BorderPane (la mise en page peut se faire dans 5 emplacements : haut, bas, droite, gauche et centre) :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package test.csstransition;
 
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
 
public final class Main extends Application {
    public static void main(final String... args) {
        launch(args);
    }
 
 
    @Override
    public void start(final Stage stage) throws Exception {
        final var root = new BorderPane();
        final var scene = new Scene(root);
        stage.setTitle("Test");
        stage.setWidth(800);
        stage.setHeight(600);
        stage.setScene(scene);
        stage.show();
    }
}

Nous allons également en profiter pour ajouter quelques nœuds graphiques dans note scene : un contrôle, ici un bouton, plusieurs formes géométriques et une région. Je rappelle qu'une région est un nœud graphique représentant généralement une forme rectangulaire skinnable, et qui sert de base a la plupart des contrôles.

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final var button = new Button("Button");
final var tooBar = new ToolBar();
tooBar.getItems().setAll(button);
final var rectangle = new Rectangle(200, 200, 150, 100);
rectangle.getStyleClass().add("rectangle");
final var circle = new Circle(100);
circle.getStyleClass().add("circle");
circle.setCenterX(150);
circle.setCenterY(150);
final var region = new Region();
region.getStyleClass().add("region");
region.setLayoutX(300);
region.setLayoutY(275);
final var center = new Pane();
center.getChildren().setAll(rectangle, circle, region);
root.setTop(tooBar);
root.setCenter(center);

Pour le moment rien de bien excitant : les 2 formes sont noires (initialisation avec les options par défaut) et la région est invisible car soit transparent, soit avec la la même couleur de fond que celle de notre conteneur parent.

Nom : csstransition-1.jpg
Affichages : 3352
Taille : 23,4 Ko

Ajout de la feuille de style
J’insère ensuite un fichier de feuille de style CSS vide dans le même package, et je charge ce fichier de la manière suivante :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
Optional.ofNullable(getClass().getResource("test.css"))
     .map(URL::toExternalForm)
     .ifPresent(scene.getStylesheets()::add);

Et nous allons commencer à remplir le CSS en donnant une apparence un peu plus colorée à notre contenu en ajoutant les lignes suivantes dans le fichier CSS :
* Le cercle sera bleu avec une bordure noire de 1 pixel d’épaisseur.
* le rectangle sera rempli d'un gradient vertical allant du bleu au vert et avec une bordure affichant un gradient diagonal allant du jaune vers le rouge avec une épaisseur de 2 pixels.
* la région sera dotée d'une taille minimale de 100 × 75 pixels, avec un fond rouge et une bordure grise, le tout semi-transparent.

Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.rectangle {
    -fx-fill: linear-gradient(to bottom, blue, green);
    -fx-stroke: linear-gradient(to bottom right, yellow, red);
    -fx-stroke-width: 2px;
}
.circle {
    -fx-fill: blue;
    -fx-stroke: black;
    -fx-stroke-width: 1px;
}
.region {
    -fx-background-color: red;
    -fx-background-radius: 0px;
    -fx-border-color: grey;
    -fx-border-width: 1px;
    -fx-border-radius: 0px;
    -fx-min-width: 100px;
    -fx-min-height: 75px;
    -fx-opacity: 0.5;
}

Ce qui nous donne désormais ceci :

Nom : csstransition-2.jpg
Affichages : 1349
Taille : 28,1 Ko

Et les transitions ?
Nous y arrivons ! Pour déclencher une animation, nous avons besoin de 2 choses : premièrement un état différent de l’état de base. Ceci se s'accomplit généralement en CSS grâce à une ou plusieurs pseudo classe. Ici nous allons utiliser la pseudo-classe hover qui dénote le fait que le curseur de la souris se trouve au-dessus du nœud ciblé. C'est un des pseudo-classe prédéfinies et qui se trouvent aussi dans les CSS web, mais ne vous inquiétez pas, les transitions CSS en JavaFX fonctionnent aussi avec des pseudo-classes personnalisées.

Et la seconde chose dont nous avons besoin c'est une ou plusieurs propriétés que nous allons faire varier. Ici, nous allons prendre par exemple, la propriété -fx-rotate de la classe Rectangle. Cette propriété définit bien sûr l'angle de la rotation qui est appliquée sur le nœud. Notre CSS va donc devenir :

Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
.rectangle {
    -fx-fill: linear-gradient(to bottom, blue, green);
    -fx-stroke: linear-gradient(to bottom right, yellow, red);
    -fx-stroke-width: 2px;
    -fx-rotate: 0;
}
.rectangle:hover {
    -fx-rotate: 90;
}

Si on relance l'application, on peut voir que le rectangle tourne de 90° sur lui-même dès que le curseur entre dans sa boite englobante. Attention cependant, évitez de mettre votre curseur trop près du bord droit ou gauche du rectangle si vous souffrez épilepsie. En effet, dans ce cas le rectangle va basculer très rapidement sans arrêt d'entre son état par défaut et son état hover provoquant un clignotement assez désagréable à l'écran. Cela est dû au fait que, dès que le rectangle se trouve orienté à 90°, votre curseur est en dehors de la boite englobante, provoquant ainsi un retour à l’état initial, et ainsi de suite, ad vitam.

Changeons maintenant la définition pour rajouter l'animation de la rotation :

Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
.rectangle {
    -fx-fill: linear-gradient(to bottom, blue, green);
    -fx-stroke: linear-gradient(to bottom right, yellow, red);
    -fx-stroke-width: 2px;
    -fx-rotate: 0;
    transition: -fx-rotate 2s;
}
.rectangle:hover {
    -fx-rotate: 90;
}

La ligne transition: -fx-rotate 2s; qui est définie dans le sélecteur de base du rectangle permet de définir une animation d'une durée de 2 secondes sur la propriété -fx-rotate permettant ainsi d'avoir une transition fluide entre l'état par défaut et l'état hover. Quand le curseur de la souris sort de la boite englobante du nœud, l'animation commence par se mettre en pause avant de s'inverser pour tenter de revenir à l’état initial. L'effet est quand même plus agréable que le clignotement précédent.

Il est aussi possible d’écrire cette transition de la manière suivante :

Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
.rectangle {
    -fx-fill: linear-gradient(to bottom, blue, green);
    -fx-stroke: linear-gradient(to bottom right, yellow, red);
    -fx-stroke-width: 2px;
    -fx-rotate: 0;
    transition-property: -fx-rotate;
    transition-duration: 2s;
}

La directive transition et les sous-directives associées permettent donc de spécifier une ou plusieurs propriété, un temps d'animation et également un interpolateur. Il est également possible de specifier un temps d'attente avant démarrage de l'animation. De multiples définitions d'animations doivent être séparées par des virgule.

  • transition - spécifie une ou plusieurs animation de manière condensée. Il est possible d'utiliser le mot-clé all pour signaler des transitions sur toutes les propriétés idoines ;
  • transition-property - spécifie une ou plusieurs propriétés à animer. Il est également possible d'utiliser le mot-clé all ici ;
  • transition-duration - spécifie une ou plusieurs durée d'animation. Le nombre de durées doit correspondre au nombre de propriétés définies ;
  • transition-delay - spécifie un ou plusieurs délais d'attente avant animation. Le nombre de délais doit correspondre au nombre de propriétés définies ;
  • transition-timing-function - spécifie un ou plusieurs interpolateurs à appliquer à l'animation. Voir Guide de reference des CSS de JavaFX 23. Le nombre d'interpolateurs doit correspondre au nombre de propriétés définies ;


Prenons maintenant notre cercle, je vais lancer une transition sur sa position, son opacité, l’épaisseur de sa bordure et ses couleurs de remplissages et de trait. Pour faire simple, je vais mettre toutes ses transitions à la même durée de 3 secondes. Pour la version de base non animée cela done :

Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.circle {
    -fx-fill: blue;
    -fx-stroke: black;
    -fx-stroke-width: 1px;
    -fx-opacity: 1.0;
    -fx-translate-x: 0;
    -fx-translate-y: 0;
}
.circle:hover {
    -fx-fill: cyan;
    -fx-stroke: yellow;
    -fx-stroke-width: 50px;
    -fx-opacity: 0.5;
    -fx-translate-x: 50;
    -fx-translate-y: 50;
}

Et pour rajouter des transitions, n'importe laquelle de ces définitions qui sont équivalentes fera l'affaire :

Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
    transition-property: -fx-fill, -fx-stroke, -fx-stroke-width, -fx-translate-x, -fx-translate-y;
    transition-duration: 3s, 3s, 3s, 3s, 3s;

Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
    transition: -fx-fill 3s, -fx-stroke 3s, -fx-stroke-width 3s, -fx-translate-x 3s, -fx-translate-y 3s;

Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
    transition: all 3s;

Limitations
Actuellement, les transitions CSS en JavaFX fonctionnent avec toutes les propriétés qui utilisent des valeurs numériques ou qui implémentent l'interface javafx.animation.Interpolatable<T>, ce qui inclut les implémentations JavaFX des couleurs (javafx.scene.paint.Color), mais aussi les point 2D (javafx.geometry.Point2D) et les points 3D (javafx.geometry.Point3D). Les couleurs sont utilisées un peu partout dans les CSS fournies par défaut, mais je n'ai pas croisé d’occurrence d'utilisation de point 2D ou 3D. Sur le papier, il devrait être possible de créer des transition avec des propriétés customisées, voir des types interpolables customisés, mais il n'est pas dit que le moteur CSS de JavaFX soit capable d’inférer les bonnes classe lors de la lecture de la feuille de style. À noter que des améliorations sur le parsing des CSS sont en préparation pour les futures version de JavaFX permettant de mieux supporter des types de valeur autres.

Il n'est pas possible de faire des transitions sur les couleurs prédéfinies. Dans les CSS par défaut de JavaFX, beaucoup de couleurs sont prédéfinies ; par exemple -fx-color contient la couleur grise qui est appliquée dans la plupart des conteneurs. Et il est possible de redéfinir ces couleurs dans des CSS customs :

Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
.rectangle {
    -fx-color: green;
    -fx-fill: -fx-color;
}
.rectangle:hover {
    -fx-color: blue;
}

La couleur changera bien du vert au bleu lorsque le curseur entre dans la boite englobante du rectangle mais si on définie une transition CSS ciblant -fx-color (au lieu de -fx-fill), celle-ci ne fonctionnera pas et, à l’écran, on conservera la transition abrupte précédente.

Et en ce qui concerne les contrôles ? En fait c'est là où le bas blesse : la plupart des propriétés stylables visuelles sur les contrôles sont définies dans la classe Region et concernent des choses telles que la bordure et la surface interne du contrôle. Souvent ce sont des propriétés qui définissent un à plusieurs objets complexes qui ne sont pas interpolables. Donc, dans JavaFX 23, en gros, ce qu'on peut animer sur les contrôles via les transitions CSS ce sont surtout les mêmes propriétés d’opacité ou de taille, positionnement, angle, échelle, etc. que sur les formes géométriques ; et il reste impossible de faire des transitions sur les bordures, leurs épaisseurs et angles de coins arrondis ou encore sur la peinture de fond du contrôle.

Par exemple, en appliquant la feuille de style suivante, tous les changements seront appliqués lorsque le curseur passera au-dessus de la région, mais la transition CSS impliquera uniquement l’opacité de la région :

Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.region {
    -fx-background-color: red;
    -fx-background-radius: 0px;
    -fx-border-color: grey;
    -fx-border-width: 1px;
    -fx-border-radius: 0px;
    -fx-opacity: 0.5;
    -fx-min-width: 100px;
    -fx-min-height: 75px;
    transition: all 2s;
}
.region:hover {
    -fx-background-color: purple;
    -fx-background-radius: 21px;
    -fx-border-color: black;
    -fx-border-width: 10px;
    -fx-border-radius: 20px;
    -fx-opacity: 1;
}

Conclusion
Voici le code et la feuille de style finale pour ce bref aperçu de cette nouvelle fonctionnalité. Utilisée avec parcimonie et subtilité, elle peut permettre de rajouter une touche de zestes et de piments à vos interface graphiques JavaFX sans pour autant devoir se lancer dans de lourds travaux de codages d'animations en Java pur. Il suffira de quelques modifications de vos fichiers CSS !

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package test.csstransition;
 
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
 
import java.net.URL;
import java.util.Optional;
 
public final class Main extends Application {
    public static void main(final String... args) {
        launch(args);
    }
 
 
    @Override
    public void start(final Stage stage) throws Exception {
        // Toolbar & button.
        final var button = new Button("Button");
        final var tooBar = new ToolBar();
        tooBar.getItems().setAll(button);
        // Shapes & region.
        final var rectangle = new Rectangle(200, 200, 150, 100);
        rectangle.getStyleClass().add("rectangle");
        final var circle = new Circle(100);
        circle.getStyleClass().add("circle");
        circle.setCenterX(150);
        circle.setCenterY(150);
        final var region = new Region();
        region.getStyleClass().add("region");
        region.setLayoutX(300);
        region.setLayoutY(275);
        final var center = new Pane();
        center.getChildren().setAll(rectangle, circle, region);
        // Layout.
        final var root = new BorderPane();
        root.setTop(tooBar);
        root.setCenter(center);
        final var scene = new Scene(root);
        // load and apply CSS.
        Optional.ofNullable(getClass().getResource("test.css"))
                .map(URL::toExternalForm)
                .ifPresent(scene.getStylesheets()::add);
        stage.setTitle("Test");
        stage.setWidth(800);
        stage.setHeight(600);
        stage.setScene(scene);
        stage.show();
    }
}

Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
.button {
    -fx-opacity: 0.5;
    transition: -fx-opacity 1s;
}
 
.button:hover {
    -fx-opacity: 1.0;
}
.rectangle {
    -fx-fill: linear-gradient(to bottom, blue, green);
    -fx-stroke: linear-gradient(to bottom right, yellow, red);
    -fx-stroke-width: 2px;
    -fx-rotate: 0;
    transition: -fx-rotate 2s;
/*    transition-property: -fx-rotate;*/
/*    transition-duration: 2s;*/
}
.rectangle:hover {
    -fx-rotate: 90;
}
.circle {
    -fx-fill: blue;
    -fx-stroke: black;
    -fx-stroke-width: 1px;
    -fx-opacity: 1.0;
    -fx-translate-x: 0;
    -fx-translate-y: 0;
    transition: all 3s;
/*    transition: -fx-fill 3s, -fx-stroke 3s, -fx-stroke-width 3s, -fx-translate-x 3s, -fx-translate-y 3s;*/
/*    transition-property: -fx-fill, -fx-stroke, -fx-stroke-width, -fx-translate-x, -fx-translate-y;*/
/*    transition-duration: 3s, 3s, 3s, 3s, 3s;*/
}
.circle:hover {
    -fx-fill: cyan;
    -fx-stroke: yellow;
    -fx-stroke-width: 50px;
    -fx-opacity: 0.5;
    -fx-translate-x: 50;
    -fx-translate-y: 50;
}
.region {
    -fx-background-color: red;
    -fx-background-radius: 0px;
    -fx-border-color: grey;
    -fx-border-width: 1px;
    -fx-border-radius: 0px;
    -fx-opacity: 0.5;
    -fx-min-width: 100px;
    -fx-min-height: 75px;
    transition: all 2s;
}
.region:hover {
    -fx-background-color: purple;
    -fx-background-radius: 21px;
    -fx-border-color: black;
    -fx-border-width: 10px;
    -fx-border-radius: 20px;
    -fx-opacity: 1;
}

Nom : csstransition-4.gif
Affichages : 1340
Taille : 1,18 Mo

Épilogue ?
Vous avez pu remarquer que ce blog post est intitulé "partie 1". En effet, comme nous avons pu le voir, bien que sympathiques sur des formes géométriques, les animations CSS ne fonctionnent autant qu'on le voudrait sur des régions ou des contrôles car les éléments CSS de ces nœuds sont plus complexes et ne sont pas interpolables. Cependant, des améliorations du support des animations CSS pour ce genre de propriétés sont annoncées pour le futur JavaFX 24 qui devrait pointer le bout de son nez en mars 2025.

À suivre... ?

Envoyer le billet « Les transitions CSS en JavaFX (partie 1) » dans le blog Viadeo Envoyer le billet « Les transitions CSS en JavaFX (partie 1) » dans le blog Twitter Envoyer le billet « Les transitions CSS en JavaFX (partie 1) » dans le blog Google Envoyer le billet « Les transitions CSS en JavaFX (partie 1) » dans le blog Facebook Envoyer le billet « Les transitions CSS en JavaFX (partie 1) » dans le blog Digg Envoyer le billet « Les transitions CSS en JavaFX (partie 1) » dans le blog Delicious Envoyer le billet « Les transitions CSS en JavaFX (partie 1) » dans le blog MySpace Envoyer le billet « Les transitions CSS en JavaFX (partie 1) » dans le blog Yahoo

Catégories
Java , Java , JavaFX

Commentaires

  1. Avatar de gbegreg
    • |
    • permalink
    Bonjour,

    C'est intéressant mais je trouve cela toujours un peu dommage de mélanger les technologies.
    De plus, avec Delphi, on peut déjà faire des choses équivalentes en utilisant les animations sous forme de composants ou sous forme de code. J'ai reproduit l'exemple donné. La rotation du rectangle ayant un fond en dégradé est faite via le composant rattaché au rectangle (pas de code nécessaire), pour les animations du cercle et du rectangle rouge, je suis passé par les animations via code.

    Sources et exe (pour windows) disponible dans ce zip.

    Sous Delphi, les animations sont threadées, ça fonctionne avec les couleurs, les points de coordonnées 2D et 3D.
    Cet exemple aurait pu être fait sans la moindre ligne de code également mais j'ai voulu montrer les deux manière de faire.

    D'ailleurs, je mettais amusé il y a quelque temps à refaire la démo Amiga Boing Ball en 3D sous Delphi avec les animations et en no-code.
    Les sources (si je puis dire) sont disponibles sur mon github et j'avais fait également une petite vidéo du rendu :
  2. Avatar de bouye
    • |
    • permalink
    Bonjour,
    Bien sur on peut tout faire par code. Justement le but est de ne pas le faire par code !

    Il est tout a fait normal de pouvoir et de vouloir faire ce genre d'animations par code quand on fait une demo de performance, un simple objet qui bouge dans toutes les directions ou un jeu video par exemple.

    Par contre, ca n'a absolument aucun sens de le faire, quand on a une interface graphique avec la meme transition qui se répète partout sur tous les contrôles similaires (ex: ici le bouton) et pour laquelle il faudra dupliquer cette animation partout dans le code. Ici les CSS sont tout a fait adaptées, d'autant plus que lorsqu'il s'agira de changer le style de l'UI (nouvelle maj majeure de l'app, passage sur une autre plateforme avec des autres paradigmes d'UI [ex : Win -> macOS ou Linux], mise a jour du style pour coller le styles actuellement utilise sur la plateforme [ex : W10- > W11], etc.)... on a absolument rien a recoder ou a aller chercher dans plein de classes différentes.
  3. Avatar de gbegreg
    • |
    • permalink
    Bonsoir,

    J'ai fait mon exemple rapidement. Rien n'empêche dans mon exemple de ne faire qu'une animation et de lui changer son parent dynamiquement pour quelle s'applique à un autre composant (exemple les zooms avec changement de couleur et d'épaisseur du contour lorsque la souris survol un autre composant.
    Pour faire une animation qui agisse sur plusieurs propriétés de l'objet à la fois, il faudra coder son animation (comme on code le CSS dans votre cas) ou se faire une timeline par exemple.
    Ça fonctionne avec toutes les propriétés de type entier, flottant, couleur et même image pour gérer par exemple des tilesets d'animations de sprites qu'on soit en 2D ou 3D.