Les transitions CSS en JavaFX (partie 1)
par
, 31/10/2024 à 06h31 (278 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 :
Envoyé par https://gluonhq.com/products/javafx/openjfx-23-release-notes/#23Bien 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.Envoyé par https://openjfx.io/highlights/23/
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.
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 :
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; }
É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... ?