Dal PC al telefono

Per connettere il proprio cellulare al telefono (utile, ad esempio se si sta sviluppando del software per il cellulare) esistono diverse applicazioni.

Nel caso di Sony Ericsson si può usare la buona applicazione messa a disposizione dal produttore sul loro sito o myPhoneExplorer, che si può scaricare gratuitamente da fjsoft.at

Animazione al contrario

L'ultimo articolo correntemente pubblicato nella sezione Animation della pagina Learn di JavaFX, é intitolato Reverse Animation Easily Using the Rate Trick ed é stato scritto da Nancy Hildebrandt.

Lo scopo é quello di mostrarci come si possa usare la variabile rate della classe Timeline per ottenere molto facilmente animazioni al contrario.

Al solito, applicherò qualche piccola variazione al codice proposto nell'articolo, ad esempio uso bottoni standard, invece di crearli dalle forme geometriche di base, e cerco in genere di semplificare il più possibile il codice.

Si può vedere l'applicazione in esecuzione in questa pagina.

In pratica il campo rate della timeline agisce da moltiplicatore per la velocità del nodo legato. E quindi, assegnandogli un valore negativo otteniamo una inversione del moto.

La timeline

Dopo aver definito due costanti, startX e endX, che definiscono il perrcorso del nostro oggetto, e due variabili che tengono traccia della sua posizione corrente e della sua direzione di movimento.

Definiamo allora una timeline il cui rate é legato alla direzione del moto, su cui agiremo con i bottoni:

import javafx.animation.Interpolator;
import javafx.animation.Timeline;

def startX = 100.0;
def endX = 250.0;

var currX = startX;
var forward = true;

def timeline = Timeline { //Timeline for animation of circle
rate: bind (if(forward) 1 else -1)
keyFrames: at(3s) { currX => endX tween Interpolator.LINEAR }
};

Il bottone "go"

Il bottone "go" chiede di muovere l'oggetto in avanti. La direzione in avanti forward é perciò "vera" e si chiama il metodo play() per la timeline. Nel caso ci si trovi a fine corsa, si riporta l'oggetto all'inizio della sua traiettoria e si riparte.

import javafx.scene.control.Button;
...
def go = Button {
translateX: 80, translateY: 140, width: 80
text: "Go"
action: function() {
forward = true;
if(currX == endX)
timeline.playFromStart()
else
timeline.play();
}
}

Il bottone "back"

Il bottone "back" chiede di muovere l'oggetto indietro. Normalmente assegneremo a forward il valore false. Ma c'é una eccezione: quando siamo a inizio traiettoria. In questo caso non possiamo, come nel caso "go", portare l'oggetto dall'altra parte e dare corso al comando richiesto, dato che playFromStart() non permette questo comportamento, ci conviene invece interpretare il comando "back" come se fosse un "go".

Ma c'é un'altra piccola complicazione. Probabilmente a causa di un bug, quando si alternano inversioni di direzione può accadere che il termine del movimento via "back" non porti esattamente alla posizione iniziale, ma a una frazione di unità da essa.

Per questo motivo non si controlla che la posizione corrente sia uguale a quella iniziale ma che la loro differenza sia inferiore all'unità.

def back = Button {
translateX: 180, translateY: 140, width: 80
text: "Back"
action: function() {
forward = if(currX - startX > 1) true else false;
timeline.play();
}
}

Ci resta da definire l'oggetto che vogliamo muovere, un cerchio, e metterlo, insieme ai bottoni che ne regolano il movimento, sulla scena.

La cosa principale da notare nel codice che definisce il cerchio e che il suo centro é legato a currX, la variabile modificata dalla timeline.

Vediamo quindi qui a seguire il codice completo dello script:
package reverseanimation;

import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.scene.control.Button;
import javafx.scene.shape.Circle;
import javafx.scene.paint.Color;
import javafx.scene.effect.DropShadow;

def startX = 100.0;
def endX = 250.0;

var currX = startX; // current x for the circle
var forward = true; // current movement direction

def timeline = Timeline { //Timeline for animation of circle
rate: bind (if(forward) 1 else -1)
keyFrames: at(3s) { currX => endX tween Interpolator.LINEAR }
};

def go = Button {
translateX: 80, translateY: 140, width: 80
text: "Go"
action: function() {
forward = true;
if(currX == endX)
timeline.playFromStart()
else
timeline.play();
}
}

def back = Button {
translateX: 180, translateY: 140, width: 80
text: "Back"
action: function() {
forward = if(currX - startX > 1) true else false;
timeline.play();
}
}

def mover = Circle {
centerX: bind currX, centerY: 80, radius: 20
fill: Color.GREEN
effect: DropShadow { offsetX: 3, offsetY: 3 }
}

Stage {
title: "Simple Animation"
width: 350, height: 220
scene: Scene {
content: [ mover, go, back ]
}
}

Capire le trasformazioni

Altro articolo nella sezione Animation della pagina Learn di JavaFX, dedicato questo a una introduzione alle trasformazioni, il titolo é Understanding Transformations.

JavaFX fornisce un notevole supporto alle funzionalità per la trasformazione di oggetti UI. Come in molte librerie grafiche, le tre principali possibilità di trasformare un oggetto sono relative alla traslazione, scalabilità e rotazione . Ce ne sono poi altre, ma é importante avere una buona conoscenza di queste tre, che sono alla base delle altre possibili trasformazioni.

Il risultato dei nostri esperimenti sarà questa serie di oggetti variamente colorati posti sulla nostra scena ognuno dei quali, tranne il quadrato nero in alto a sinistra, sono modificati per mezzo di traslazione, cambio scala o rotazione, rispetto a un nodo.

Traslazione

Per traslare un oggetto si può assegnare un valore ai suoi attributi translateX e translateY, valori positivi spostano l'oggetto verso destra e il basso rispettivamente.
Nell'esempio il rettangolo rt viene spostato verso destra e verso il basso di 100 pixel rispetto a r:

def r = Rectangle {
x: 30, y: 10, width: 20, height: 20
}

def rt = Rectangle {
x: 30, y: 10, width: 20, height: 20
translateX: 100, translateY: 100
fill: Color.BROWN
}

Cambio scala

Per far crescere un nodo sulla scena basta agire sulle sue proprietà scaleX e scaleY che agiscono da moltiplicatori per la larghezza e l'altezza, rispettivamente.

Si noti che, al contrario di quanto specificato nell'articolo, il punto fermo dell'oggetto da cui si applica la trasformazione é il suo centro. Probabilmente questo vale per JavaFX 1.2, e nella versione precedente non era così.

Per verificare il comportamento di questa funzionalità ho creato quattro rettangoli in questo modo:

def r12 = Rectangle {
x: 80, y: 10, width: 20, height: 20
scaleX: 2
fill: Color.DARKBLUE;
}

def r13 = Rectangle {
x: 30, y: 40, width: 20, height: 20
scaleX: 2
fill: Color.DARKBLUE;
}

def r31 = Rectangle {
x: 30, y: 80, width: 20, height: 20
fill: Color.DARKCYAN;
}

def r32 = Rectangle {
x: 80, y: 80, width: 20, height: 20
scaleX: 2
scaleY: 2
fill: Color.DARKCYAN;
}

Rotazione

Per far rotare un nodo sulla scena basta assegnare il numero di gradi della rotazione all'attribuito rotate dell'oggetto. Si noti che la rotazione avviene rispetto al centro dell'oggetto stesso.

def rr = Rectangle {
x: 130, y: 10, width: 20, height: 20
fill: Color.AQUA
rotate: 45
}

Insetti luminosi in volo

Continuiamo a lavorare sulla serie di articoli sull'animazione indicati nella sezione Learn di JavaFX.

Vediamo ora quello titolato "Using Paths and Timelines Together (Firefly)"

Scopo dell'applicazione é quello di usare le classi Timeline e Path per creare un fondale su cui si muovono in modo casuale una quantità di cerchi (a simboleggiare degli insetti) che si accendono e spengono in modo anch'esso causale.

Si può vedere il codice in esecuzione in questa pagina.

Il codice originale é organizzato in quattro file:
  • Main: dichiara lo stage e la scena su cui agiscono gli insetti;
  • Fly: classe che estende CustomNode per definire i nostri insetti;
  • Lighter: classe per dire a un insetto quando si deve illuminare;
  • Flight: classe per definire il volo casuale di un insetto

Usare due classi per definire il moto dell'insetto m'é sembrato sovrabbondante, e perciò ho spostato il codice di Lighter in Flight, che ora si occupa di tutte le caratteristiche del volo.

Poi ho cambiato il moto degli insetti, in originale si muovono tracciando una cubica Bezier (classe CubicCurveTo), io ho preferito una semplice retta, ispirandomi al volo della mosca; ho ridotto il numero degli oggetti volanti da venti a sei, e ho fatto altre piccole modifiche.

Insetto: prima versione

Diamo una prima semplice definizione della classe Fly, che rappresenta il nostro insetto, al fine di permetterci di scrivere il main del nostro progetto:

package fireflies;

import javafx.scene.CustomNode;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;

public class Fly extends CustomNode {
public-init var where: Rectangle;

def fly: Circle = Circle {
radius: 6, strokeWidth: 2, stroke: Color.AQUAMARINE
}

override function create() : Node {
return fly
}
}

La variabile where, che identifica l'area in cui é confinato al volo l'insetto, al momento non é utilizzata.

Stage e scena

Nella scena dello stage mettiamo un rettangolo e gli insetti che sono creati "on the fly" (é il caso di dirlo) con un ciclo for.

package fireflies;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;

def area = Rectangle {
x: 0, y: 0, height: 400, width: 400
fill: Color.DARKBLUE
}

Stage {
title: "Fire Flies"
resizable: false
scene: Scene {
fill: Color.BLACK
content: [
area,
for(i in [1..6]) { Fly { where: area } }
]
}
}

Interessante notare che Main.fx é già completo. La logica é tutta nella classe Flight che viene pilotata dalla classe Fly.

Il volo: prima versione

La classe Flight richiede che vengano inizializzate queste variabili:
  • fly: l'insetto corrente, oggetto del volo
  • maxX: massima ascissa raggiungibile in volo
  • maxY: massima ordinata
  • min: minima ascissa/ordinata

E rende disponibile flyColor, che é il colore dell'insetto, che faremo variare nel tempo.

Definiamo poi tre variabili private:
  • randomLightTime: tempo in cui l'insetto non s'illumina
  • endX: ascissa del punto terminale del percorso corrente
  • endY: ordinata del punto terminale del percorso corrente


Definiamo infine due funzioni, che al momento non fanno nulla, ma che gestiranno il movimento e l'illuminazione dell'insetto:

import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.util.Math;

public class Flight {
public-init var fly : Node;

public-init var maxX;
public-init var maxY;
public-init var min : Number;

public var flyColor = Color.BLACK;

var randomLightTime : Duration = Duration.valueOf((3000 * Math.random()) + 2000);
var endX = maxX/2;
var endY = maxY/2;

public function createAndPlayAnimation() : Void {
}

public function startLighter() : Void {
}
}


Insetto completo

Aggiungiamo il colore riempitivo dell'insetto, che é legato al colore come viene determinato nella classe Flight.
Definiamo il volo, passando i parametri richiesti. Nota che max e min sono generati in modo che l'insetto non possa attraversare i limiti del rettangolo in cui é ristretto.
La funzione create(), prima di ritornare l'insetto, fa partire il volo e la gestione del colore.

public class Fly extends CustomNode {
public-init var where: Rectangle;

def fly: Circle = Circle {
radius: 6, strokeWidth: 2, stroke: Color.AQUAMARINE
fill: bind flight.flyColor;
}

def flight = Flight {
fly: fly;
maxX: where.width - fly.radius;
maxY: where.height - fly.radius;
min: fly.radius;
}

override function create() : Node {
flight.createAndPlayAnimation();
flight.startLighter();
return fly
}
}


Il volo dell'insetto

Vediamo la funzione createAndPlayAnimation() nella classe Flight.

Generiamo il punto iniziale e finale del percorso e generiamolo usando la classe PathTransition, nota che la durata di questo tratto di volo é casuale, ma dura almeno un decimo di secondo (per evitare movimenti eccessivamente veloci), nel campo action viene specificata la funzione da eseguire al termine dell'esecuzione della funzione corrente - che é poi ancora la stessa.


import javafx.animation.transition.AnimationPath;
import javafx.animation.transition.PathTransition;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.LineTo;

...
public function createAndPlayAnimation() : Void {
def startX = endX;
def startY = endY;

endX = Math.max(min, maxX * Math.random());
endY = Math.max(min, maxY * Math.random());

PathTransition {
node: fly;
path: AnimationPath.createFromPath(
Path {
elements: [
MoveTo { x: startX, y: startY },
LineTo { x: endX, y: endY }
]
}
)

duration: Duration.valueOf(100 + (1000 * Math.random()))
action: function() {createAndPlayAnimation();}
}.play();
}

Aggiungendo questa funzione la nostra applicazione prende vita e i nostri insetti si metto a volare per tutto il loro spazio.

Luci in volo

Scriviamo il codice della startLighter() nella classe Flight.

In realtà é una funzione piuttosto semplice: crea un timeline e la fa partire.
La timeline si ripete in modo indefinito con autoreverse.
Da notare che per la definizione dei KeyFrame non si é potuta usare la costruzione semplificata "at() ..." dato che come tempo vogliamo usare una variabile, mentre l'at richiede una duration letterale.


import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;

public function startLighter() : Void {
Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: [
KeyFrame {
time: randomLightTime
values: [ flyColor => Color.BLACK ]
},
KeyFrame {
time: randomLightTime + .5s;
values: [ flyColor => Color.YELLOW tween Interpolator.EASEBOTH ]
}
];
}.play();
}

Con questa funzione il nostro programma é completo.

Più timeline per un oggetto

L'articolo Basic Timeline Animation nella sezione Animation di JavaFX Learn mostra un esempio semplice ma di effetto su come animare un oggetto in due dimensioni, usando due differenti timeline.

Come bonus noi ci aggiungiamo pure una variazione delle dimensioni nostro oggetto, gestita da una terza timeline.

Si può vedere l'applicazione in esecuzione in questa pagina.

Stage, scena e attore

Definiamo uno stage con la sua scena e mettiamo al suo interno un cerchio:

package timeline;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;

def circle = Circle {
centerX: 50, centerY: 50, radius: 10
fill: Color.DARKORCHID
}

Stage {
title: "Animator"

scene: Scene {
width: 100, height: 100
fill: Color.LIGHTGREEN
content: circle
}
}


Doppia azione

Definiamo ora due timeline che operano su due variabili numeriche, x e y, assegnando a ognuna delle due un valore crescente tra 10 e 90, la timeline continurà indefinitivamente e, ogni volta che viene raggiunto un estremo, si invertirà la direzione di crescita (come dire: la sequenza sarà ... 88, 89, 90, 89, 88 ...), usiamo l'interpolatore EASEBOTH così, al raggiungere degli estremi, il movimento risulta rallentato. Notiamo ancora che i tempi impiegati per compiere il ciclo sono differenti tra le due timeline, questo renderà il movimento lungo x e y più indipendente col passar del tempo.
Notiamo infine che le timeline sono fatte partire immediatamente:

import javafx.animation.Interpolator;
import javafx.animation.Timeline;

var x = 10;
var y = 10;

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (1.5s) {x => 90 tween Interpolator.EASEBOTH}
}.play();

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (2.7s) {y => 90 tween Interpolator.EASEBOTH}
}.play();

Per rendere ancora più bizzarra la situazione, aggiungiamo una terza timeline che regolerà il raggio del nostro cerchio, facendolo variare tra 5 e 10, in mezzo secondo e in modo lineare:

var r = 5;
...
Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (0.5s) {r => 10 tween Interpolator.LINEAR}
}.play();


Ci resta ora di associare x, y e r al cerchio, in modo che si muova e cambi dimensione al loro variare:

def circle = Circle {
centerX: bind x, centerY: bind y, radius: bind r
...
}

E così il nostro codice é completo, rivediamolo qui a seguire:

package timeline;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;

var x = 10;
var y = 10;
var r = 5;

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (1.5s) {x => 90 tween Interpolator.EASEBOTH}
}.play();

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (2.7s) {y => 90 tween Interpolator.EASEBOTH}
}.play();

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (1s) {r => 10 tween Interpolator.LINEAR}
}.play();

def circle = Circle {
centerX: bind x, centerY: bind y, radius: bind r
fill: Color.DARKORCHID
}

Stage {
title: "Animator"

scene: Scene {
width: 100, height: 100
fill: Color.LIGHTGREEN
content: circle
}
}

Animare su un percorso arbitrario

Sempre nella pagina Learn di JavaFX.com, sezione animazione, passo all'articolo Animation Along an Arbitrary Path di Peter Zhelezniakov.

Si tratta qui dell'uso della classe javafx.animation.transition.PathTransition che permette una delle transizioni più sofisticate tra quelle disponibili in JavaFX. Nonostante ciò é anche una classe facile da usare: si crea un percorso, un oggetto che lo deve percorrere, li si passa alla PathTransition che fornisce la logica per l'animazione.

Il codice completo sviluppato da Peter viene messo a disposizione qui. Teniamolo da traccia e sviluppiamo passo dopo passo l'applicazione.

Il risultato finale viene mostrato qui.

Finestra principale

Peter ha evidentemente un profondo background in java, e la strutturazione del suo codice lo riflette. La cosa più evidente é che Peter, invece di lasciare a JavaFX il compito di interpolare la funzione run() da chiamare al momento dell'esecuzione, creando lo stage e le funzioni solitamente sparse nello script, preferisce esplicitarla. Seguiamo volentieri la sua impostazione, creandoci lo stage e la scena in questo modo:

package pathanim;

import javafx.stage.Stage;
import javafx.scene.Scene;

function run(): Void {
Stage {
title: "Path Animation"
scene: Scene {
width: 400, height: 300
}
};
}

Oggetto animato

Creamo un oggetto che faremo muovere lungo un percorso:

import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;

def bar1 = Rectangle {
width: 5, height: 50
fill: Color.RED
};

Il percorso

E questo é il percorso che vogliamo che il nostro oggetto segua:

import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;

def track1 = Path {
stroke: Color.BLUE
strokeWidth: 4
elements: [
MoveTo { x: 50, y: 100 },
CubicCurveTo {
controlX1: 100, controlY1: -50
controlX2: 300, controlY2: 250
x: 350, y: 100
}
]
};

Specifichiamo il punto iniziale con MoveTo, e poi il percorso é la cubica definita da CubicCurveTo.

L'animazione

Ci pensa PathTransition ad associare oggetto a percorso, usando i parametri passati per definire le modalità dell'animazione:

def anim1 = PathTransition {
node: bar1
path: AnimationPath.createFromPath(track1)
orientation: OrientationType.NONE
interpolator: Interpolator.LINEAR
duration: 5s
repeatCount: Timeline.INDEFINITE
};

Come orientamento abbiamo specificato OrientationType.NONE che é il default e che significa che l'oggetto si muove lungo il percorso senza cambiare orientamento. Più interessante é OrientationType.ORTHOGONAL_TO_TANGENT, in cui l'oggetto trasla come farebbe chiunque seguendo un percorso, seguendo il proprio naso. Il secondo oggetto sul secondo percorso (vedi codice completo più sotto) segue appunto questa modalità.

Esecuzione

Aggiorniamo la funzione run() in modo da mettere sulla scena i nostri oggetti e far partire l'animazione:

function run(): Void {
Stage {
title: "Path Animation"
scene: Scene {
width: 400, height: 300
content: [ track1, bar1 ]
}
};

anim1.play();
}

Codice completo

Nel codice completo raddoppiamo gli oggetti in scena:

package pathanim;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.animation.transition.AnimationPath;
import javafx.animation.transition.OrientationType;
import javafx.animation.transition.PathTransition;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.Rectangle;

def bar1 = Rectangle {
width: 5, height: 50
fill: Color.RED
};

def bar2 = Rectangle {
width: 5, height: 50
fill: Color.BLUE
};

def track1 = Path {
stroke: Color.BLUE
strokeWidth: 4
elements: [
MoveTo { x: 50, y: 100 },
CubicCurveTo {
controlX1: 100, controlY1: -50
controlX2: 300, controlY2: 250
x: 350, y: 100
}
]
};

def track2 = Path {
stroke: Color.RED
strokeWidth: 4
elements: [
MoveTo { x: 50, y: 200 },
CubicCurveTo {
controlX1: 100, controlY1: 50
controlX2: 300, controlY2: 350
x: 350, y: 200
}
]
};

def anim1 = PathTransition {
node: bar1
path: AnimationPath.createFromPath(track1)
orientation: OrientationType.NONE
interpolator: Interpolator.LINEAR
duration: 5s
repeatCount: Timeline.INDEFINITE
};

def anim2 = PathTransition {
node: bar2
path: AnimationPath.createFromPath(track2)
orientation: OrientationType.ORTHOGONAL_TO_TANGENT
interpolator: Interpolator.LINEAR
duration: 5s
repeatCount: Timeline.INDEFINITE
};

function run(): Void {
Stage {
title: "Path Animation"
scene: Scene {
width: 400, height: 300
content: [ track1, track2, bar1, bar2 ]
}
};

anim1.play();
anim2.play();
}

Le basi dell'animazione

All'interno della sezione Learn di JavaFX.com si trova una utile serie di articoli che offrono interessanti spunti di approfondimento sul linguaggio.

In questa serie di post trattiamo gli articoli che si riferiscono all'animazione di oggetti con JavaFX.

Nell'articolo Animation Basics for JavaFX Beginners di Nancy Hildebrandt, si offre una introduzione molto lineare alle basi dell'animazione. In particolare si mostra come animare, muovendo da un punto ad un altro, in modo concorrente e sequenziale, semplici oggetti.

In questo post prendiamo l'articolo e lo variamo leggermente secondo il nostro opinabile gusto.

Il risultato finale viene mostrato qui.

Creare un rettangolo

Prima cosa, creiamo un istanza della classe Rectangle che, in realtà, ci darà un quadrato.

package basicanimation;

import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

def redSquare = Rectangle {
x: 20, y: 60
width: 20, height: 20
fill: Color.RED
}

Stage {
title: "One Red Square"
width: 240, height: 200

scene: Scene {
content: redSquare
}
}

Al solito, ci siamo creati stage e scena, e dentro ci siamo messi l'oggetto che ci siamo precedentemente definiti. In questo caso redSquare.

Ci dovrebbe essere ormai ben chiaro che lo stage definisce la finestra principale della nostra applicazione e la scena contiene gli oggetti che ne fanno l'azione.
Avessimo dubbi su questi concetti chiave, ci converrebbe rivederci i tutorial di base core e ui.

Un po' di animazione

Vogliamo ora far muovere il nostro rettangolo dal suo punto iniziale x:20 a x:100, in 3 secondi netti.
Per far questo usiamo la classe Timeline, implementandone un oggetto in questo modo:

import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;

var redSlider: Integer;

Timeline {
keyFrames: [
KeyFrame {
time: 0s
values: [ redSlider => 20 ]
}
KeyFrame {
time: 3s
values: [ redSlider => 100 tween Interpolator.LINEAR ]
}
]
}.play();

def redSquare = Rectangle {
x: bind redSlider, y: 60
...
}

La nostra timeline ha due stati, istanze della classe KeyFrame, quello iniziale che é la "foto" della situazione all'istante zero e determina che il valore della variabile redSlider, che abbiamo appositamente dichiarato sopra, sia 20. Il secondo stato é rappresenta la situazione al terzo secondo, e ci dice che redSlider vale a quel momento 100. Quello che succede da 0 a 3 ci viene detto da tween, che sarebbe poi l'operatore che genera tutti gli stati intermedi usando l'interpolazione specificata, nel nostro caso lineare.

Dopo che abbiamo definito il nostro oggetto timeline, che va a impattare su redSlider, abbiamo modificato la definizione di redSquare, in modo che l'ascissa del nostro rettangolo sia legata a redSlider, e quindi vari nel tempo.

Risultato: redSquare si muove in accordo con quanto definito in timeline.

Timeline succinta

Chi ha un background in linguaggi che amanno la concisione, apprezzerà particolarmente la possibilità di utilizzare l'operatore at nella definizione di una timeline che, combinato al fatto che si può evitare di specificare cosa succede all'istante zero in una timeline, sempre che definiamo opportunamente la variabili che vengono impattate, ci permette di riscrivere il codice sovrastante in modo decisamente più snello:

var redSlider: Integer = 20;

Timeline {
keyFrames: at (3s) { redSlider => 100 tween Interpolator.LINEAR }
}.play();

Non male, davvero.

Movimento simultaneo

Mettiamo in scena un secondo attore, un quadrato nero, che si muove in parallelo al rosso:

def blackSquare = Rectangle {
translateX: bind redSlider, y: 90
width: 20, height: 20
fill: Color.BLACK
}

Stage {
title: "Moving around"
...

scene: Scene {
content: [redSquare, blackSquare]
}
}

Ma un timeline può controllare molteplici linee di sviluppo nel tempo, approfittiamone per muovere blackSquare in modo indipendente da redSquare:

...
var blackSlider: Integer = 110;

Timeline {
keyFrames: [
at (3s) { redSlider => 100 tween Interpolator.LINEAR }
at (3s) { blackSlider => 200 tween Interpolator.EASEBOTH }
]
}.play();

def blackSquare = Rectangle {
translateX: bind blackSlider, y: 60
...
}

Nota che in keyFrames ora c'é un array di elementi, che quindi sono inclusi tra parentesi quadre. Per il movimento del rettangolo nero ho usato l'interpolazione EASEBOTH, giusto per variare.

Animazione sequenziale

Vogliamo ora usare la nostra timeline per animare oggetti diversi in tempi diversi. Aggiungiamo una lineetta e una palla verde sulla scena:

import javafx.scene.shape.Line;
import javafx.scene.shape.Circle;

def line = Line {
startX: 110, startY: 40
endX: 110, endY: 100
strokeWidth: 1
stroke: Color.BLACK
}

def ball = Circle {
translateX: 110, centerY: 70, radius: 10
fill: Color.GREEN
}

Stage {
...
scene: Scene {
content: [redSquare, blackSquare, line, ball]
}
}

E adesso animiamo la palla. Definiamo ballSlider con la posizione iniziale della nostra palla che, come descritto in Timeline, non fa nulla fino al quarto secondo, quando inizia il suo moto lineare verso la sua destinazione x:10, dove arriverà in 3 secondi.

...
var ballSlider: Integer = 110;

Timeline {
keyFrames: [
...
at (4s) { ballSlider => 110 }
at (7s) { ballSlider => 10 tween Interpolator.LINEAR }
]
}.play();
...
def ball = Circle {
translateX: bind ballSlider, centerY: 70, radius: 10
...
}

Animation reloaded

Aggiungiamo un bottone per far ripartire l'animazione. Evitiamo la complicazione di costruirci un bottone custom e usiamone invece uno standard, l'azione associata al bottone é la playFromStart() della timeline, che rendiamo perciò disponibile come oggetto:

import javafx.scene.control.Button;
...
def timeline = Timeline {
...
};
timeline.play();
...
def button = Button {
layoutX: 80, layoutY: 130
text: "Reload"
action: function() { timeline.playFromStart(); }
}

Stage {
...
scene: Scene {
content: [redSquare, blackSquare, line, ball, button]
}
}

E con questo la nostra applicazione é completa. Segue il listato del codice che abbiamo sviluppato:

package basicanimation;

import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.scene.shape.Line;
import javafx.scene.shape.Circle;
import javafx.scene.control.Button;

var redSlider: Integer = 20;
var blackSlider: Integer = 110;
var ballSlider: Integer = 110;

def timeline = Timeline {
keyFrames: [
// first step: squares move
at (3s) { redSlider => 100 tween Interpolator.LINEAR }
at (3s) { blackSlider => 200 tween Interpolator.EASEBOTH }
// second step: ball moves
at (4s) { ballSlider => 110 }
at (7s) { ballSlider => 10 tween Interpolator.LINEAR }
]
};
timeline.play();

def redSquare = Rectangle {
x: bind redSlider, y: 60
width: 20, height: 20
fill: Color.RED
}

def blackSquare = Rectangle {
translateX: bind blackSlider, y: 60
width: 20, height: 20
fill: Color.BLACK
}

def line = Line {
startX: 110, startY: 40
endX: 110, endY: 100
strokeWidth: 1
stroke: Color.BLACK
}

def ball = Circle {
translateX: bind ballSlider, centerY: 70, radius: 10
fill: Color.GREEN
}

def button = Button {
layoutX: 80, layoutY: 130
text: "Reload"
action: function() { timeline.playFromStart(); }
}

Stage {
title: "Moving around"
width: 240, height: 200

scene: Scene {
content: [redSquare, blackSquare, line, ball, button]
}
}

PerspectiveTransform

Altra applicazione di esempio per JavaFX fornita da Netbeans é PhotoFlip, che mostra l'uso della classe PerspectiveTransform.

Mi vien da pensare che questa applicazione sia stata scritta da uno che ha una ottima conoscenza della classe in questione ma non sia così addentro nelle logiche di JavaFX, dato che il codice é un po' meno leggibile di quanto si veda normalmente.

Inoltre a me, che poco ne capisco di grafica, l'uso della classe m'é risultato oscuro. Quindi sono costretto a fare atto di fede su questo punto, che sarebbe dovuto essere fondamentale, e mi dedico piuttosto a capire il funzionamento dell'applicazione al contorno.

Useremo due file, FlipMain.fx e FlipView.fx, contentente codice proveniente dall'originale Main.fx e riarrangiato per rendermelo più adatto al mio gusto (fra l'altro, ho rimosso il codice per costruire la simulazione di un bottone per usare un vero bottone - si rimanda al codice originale se si preferisce una vita più complicata del necessario).
Le foto sono state mosse in una sottodirectory del package corrente, a nome image, per mantenere le convenzioni utilizzate solitamente negli altri tutorial.

Stage e scena

Iniziamo creando la scena su cui avverrà la nostra azione, dovrebbe essere ormai una cosa che ci viene quasi naturale:

package fliptransition;

import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

Stage {
title: "Flip Transition"
resizable: false

scene: Scene {
fill: Color.BLACK
width: 300, height: 340
}
}

Unico punto degno di nota é che si é specificato che non vogliamo che la finestra possa cambiar dimensione, si tratta della proprietà resizable dello stage.

FlipView e bottone

Vogliamo mettere in scena due oggetti: un bottone e un nodo custom che ci creiamo noi, FlipView che ci permetterà di vedere due immagine, alternandole con un simpatico movimento di rotazione.

Diamo quindi una prima implementazione di FlipView, nel file FlipView.fx:

package fliptransition;

import javafx.scene.CustomNode;
import javafx.scene.Node;

public class FlipView extends CustomNode {
public-init var frontNode : Node;
public-init var backNode : Node;

override public function create() : Node {
frontNode
}

public function flip() : Void {
}
}

La nostra classe FlipView specializza un CustomNode ed é visibile a chiunque abbia accesso al package.
Al suo interno ci sono due nodi che possono essere inizializzati ma mai più modificati (le immagini che vogliamo visualizzare) e un boolean che può essere solo letto e ci dice se stiamo vedendo la foto di fronte o quella del retro.
Abbiamo poi definito la funzione create() che, al momento, torna l'immagine frontale e la funzione flip() che implementerà il cambio di foto visualizzata.

Torneremo più avanti su questa classe, al momento questo ci basta per andare avanti nello sviluppo del nostro script fx nel main.

Definiamo infatti un oggetto di tipo FlipView e piazziamolo in basso al centro della nostra scena. Le immagini che mettiamo in FlipView sono quelle fornite dall'esempio (ma spostate nella sottodirectory image):

import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

def flipView = FlipView {
translateX: 50, translateY: 90
backNode: ImageView { image: Image { url: "{__DIR__}image/lion1.png" } }
frontNode: ImageView { image: Image { url: "{__DIR__}image/lion2.png" } }
};


L'oggetto di tipo bottone sarà invece definito così:

import javafx.scene.control.Button;
import javafx.scene.text.Font;

def button = Button {
translateX: 50, translateY: 10
width: 200, height: 35
text: "Click Here to Flip", font: Font { size: 18 }
action: flipView.flip;
}

Nota che abbiamo definito che l'azione associata al bottone é la funzione flip() del nostro oggetto flipView. Quindi cliccare sul bottone vuol dire richiamare quella funzionalità.

Aggiungiamo il bottone e la vista alla scena:

Stage {
...
scene: Scene {
...
content: [ flipView, button ]
}
}

E abbiamo già una prima versione della nostra applicazione. Mancano la funzionalità per cambiare le immagini ma il nostro script nel main é oramai completo. Ecco a seguire il suo codice:

package fliptransition;

import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.control.Button;
import javafx.scene.text.Font;

def flipView = FlipView {
translateX: 50, translateY: 90
backNode: ImageView { image: Image { url: "{__DIR__}image/lion1.png" } }
frontNode: ImageView { image: Image { url: "{__DIR__}image/lion2.png" } }
};

def button = Button {
translateX: 50, translateY: 10
width: 200, height: 35
text: "Click Here to Flip", font: Font { size: 18 }
action: flipView.flip;
}

Stage {
title: "Flip Transition"
resizable: false

scene: Scene {
fill: Color.BLACK
width: 300, height: 340
content: [ flipView, button ]
}
}

Passiamo quindi a considerare la classe FlipView. Iniziamo dalla funzione cardine, quella che calcola la trasformazione da applicare alla nostra immagine mentre passiamo da una all'altra foto. In pratica si tratta di costruire un oggetto PerspectiveTransform in funzione del tempo da quando é iniziata la trasformazione. Non dico di più su come sia fatta perché a me la cosa é piuttosto oscura:

import javafx.scene.effect.PerspectiveTransform;
import javafx.util.Math;
...
function getPT(t : Number) : PerspectiveTransform {
def width = 200;
def height = 200;
def radius = width/2;
def back = height/10;

return PerspectiveTransform {
ulx: radius - Math.sin(t)*radius, uly: 0 - Math.cos(t)*back
urx: radius + Math.sin(t)*radius, ury: 0 + Math.cos(t)*back
lrx: radius + Math.sin(t)*radius, lry: height - Math.cos(t)*back
llx: radius - Math.sin(t)*radius, lly: height + Math.cos(t)*back
}
}

Dichiariamo ora una variabile, where, che tiene traccia della rotazione che applichiamo alla nostra immagine. La posizione iniziale la chiamiamo pi greco mezzi, e rotiamo fino ad arrivare a meno pi greco mezzi. Quando siamo a zero, l'immagine é perpendicolare allo schermo. Quando l'angolo é positivo si vedrà l'immagine frontale, quando l'angolo é negativo si vedrà l'immagine sul retro.

Per gestire questo algoritmo usiamo un Timeline, che ci farà variare where linearmente in un secondo:

import javafx.animation.Interpolator;
import javafx.animation.Timeline;

...

var where = Math.PI/2;
var anim = Timeline {
keyFrames: [
at(0s) { where => Math.PI/2}
at(1s) { where => -Math.PI/2 tween Interpolator.LINEAR}
]
}

Fatto questo, possiamo scrivere l'implementazione della funzione flip(), che viene richiamata pigiando il bottone per cambiare immagine:

public function flip() : Void {
if(where < 0) {
anim.rate = -1.0;
anim.time = 1s;
}
else {
anim.rate = 1.0;
anim.time = 0s;
}
anim.play();
}

Se l'angolo é negativo, dobbiamo fare un movimento inverso, quindi facciamo partire l'animazione in senso contrario, altrimenti la facciamo partire nel verso giusto.

Ultima cosa, dobbiamo completare la funzione create(), per far sì che siano disponibili le due immagini che saranno distorte a seconda del valore di where (e quindi a seconda del tempo trascorso dall'inizio dello spostamento), usando l'algoritmo definito dalla getPT().

import javafx.scene.Group;
...
override public function create() : Node {
return Group {
content: [
Group {
content: backNode
visible: bind (where < 0)
effect: bind getPT(where)
},
Group {
content: frontNode
visible: bind (where > 0)
effect: bind getPT(where)
}
]
}
}

Questo completa il codice della classe FlipView, che vediamo ora al completo:

package fliptransition;

import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.effect.PerspectiveTransform;
import javafx.util.Math;

protected class View extends CustomNode {
public-init var frontNode : Node;
public-init var backNode : Node;

var where = Math.PI/2;
var anim = Timeline {
keyFrames: [
at(0s) { where => Math.PI/2}
at(1s) { where => -Math.PI/2 tween Interpolator.LINEAR}
]
}

public function flip() : Void {
if(where < 0) {
anim.rate = -1.0;
anim.time = 1s;
}
else {
anim.rate = 1.0;
anim.time = 0s;
}
anim.play();
}

override public function create() : Node {
return Group {
content: [
Group {
content: backNode
visible: bind (where < 0)
effect: bind getPT(where)
},
Group {
content: frontNode
visible: bind (where > 0)
effect: bind getPT(where)
}
]
}
}

function getPT(t : Number) : PerspectiveTransform {
def width = 200;
def height = 200;
def radius = width/2;
def back = height/10;

return PerspectiveTransform {
ulx: radius - Math.sin(t)*radius, uly: 0 - Math.cos(t)*back
urx: radius + Math.sin(t)*radius, ury: 0 + Math.cos(t)*back
lrx: radius + Math.sin(t)*radius, lry: height - Math.cos(t)*back
llx: radius - Math.sin(t)*radius, lly: height + Math.cos(t)*back
}
}
}

Drag and drop

Netbeans fornisce una serie di applicazioni di esempio per JavaFX. La prima che vediamo é sull'uso del drag and drop.

Delle due versioni presentate, analizziamo la seconda, basata sullo script Main2.fx che richiama la classe DragBehavior definita nell'omonimo file.

E' stato fatto qualche cambiamento al codice, per renderlo più adeguato al gusto di chi scrive, ma sostanzialmente ci si può ritrovare anche guardando il codice originale.

Si tratta di visualizzare una palla, che potrà essere trascinata su tutta la scena, ma senza che esca dai limiti, nemmeno parzialmente.

Cominciamo a definire lo stage e la scena della nostra applicazione in Main.fx, che alla fine risulterà molto simile a Main2.fx:

package draganddrop;

import javafx.scene.Scene;
import javafx.stage.Stage;

def scene : Scene = Scene {
width: 240, height: 320
}

Stage {
title: "Drag And Drop"
scene: scene
}

Coloriamo il fondale della scena con una sfumatura che va dal bianco sporco al marrone.

import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
...
def scene : Scene = Scene {
...
fill: LinearGradient {
proportional: true, startX: 0, startY: 0, endX: 0, endY: 1

stops: [
Stop { offset: 0.0, color: Color.WHITESMOKE },
Stop { offset: 1.0, color: Color.BROWN },
]
}
}

Creiamo un immagine e mettiamola nella scena. l'immagine é fornita con l'esempio, noi ci aggiungiamo una ombreggiatura per darle un effetto tridimensionale. Nota che nell'esempio di Netbeans si usa l'altro .png disponibile, che ha un effetto incluso nell'immagine stessa, che però la rende di gestione più complessa per il problema del movimento sui bordi. L'uso di ball2.png permette di risovere la questione in modo decisamente più elegante.

import javafx.scene.effect.InnerShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
...
def ball = ImageView {
image: Image { url: "{__DIR__}images/ball2.png" }
effect: InnerShadow{offsetX: 8, offsetY: 8, color: Color.PINK}
};

def scene : Scene = Scene {
...
content: ball
...
}

Quando ridimensionsioniamo la finestra della applicazione vogliamo evitare che la palla resti fuori dalla nostra visuale. Dunque creiamo due variabili collegate alle dimensioni della scena al fine di associare un trigger che, in caso di restringimento, riporti la palla al bordo estremo.

var width : Number = bind scene.width on replace oldValue {
if(width < oldValue) {
ball.translateX = 0;
}
};

var height : Number = bind scene.height on replace oldValue {
if(height < oldValue) {
ball.translateY = 0;
}
};

La nostra scena é a posto, resta ora da definire la modalità di funzionamento del drag and drop sulla palla, che deleghiamo a una apposita classe, DragDrop - basata sulla DragBehavior fornita da Netbeans, che definiamo nel suo specifico file DragDrop.fx:

package draganddrop;

import javafx.scene.Node;

public class DragDrop {
public-init var target : Node;
public-init var targetWidth : Number;
public-init var targetHeight : Number;

public var maxX;
public var maxY;
}

Dichiariamo tre variabili che definiscono il bersaglio del drag and drop:
  • target, rappresenta l'oggetto come viene gestito all'interno della scena
  • targetWidth e targetHeight, che rappresentano le dimensioni del bersaglio
é stata data loro una visibilità public-init, dato che una volta inizializzate non le si vuole più cambiare.
Poi abbiamo altre due variabili, maxX e maxY, che rappresentano la dimensione della scena, e sono quindi il limite estremo che i nostro oggetto può raggiungere nella finestra verso destra e verso il basso. Dato che é possibile ridimensionare la finestra, il loro valore può cambiare nel corso dell'esecuzione del programma, e quindi abbiamo dato loro visibilità public.

Data questa definizione della classe DragDrop, possiamo completare il nostro script aggiungendo la sua instaziazione, che renderà disponibile all'applicazione le funzionalità di drag and drop.

DragDrop {
target: ball
targetWidth: ball.image.width
targetHeight: ball.image.height
maxX: bind scene.width
maxY: bind scene.height
};

Il obiettivo del drag and drop é la palla che abbiamo messo nella scena. La dimensione dell'obiettivo viene ricavata dall'ampiezza e altezza dell'immagine associata alla palla.
Leghiamo poi maxX e maxY all'ampiezza e altezza della scena, in modo che l'aggiornamento delle dimensioni della scena si rifletta automaticamente sulle variabili membro della classe DragDrop.

E quindi questo é il codice completo per main.fx:

package draganddrop;

import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.stage.Stage;

import javafx.scene.effect.InnerShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

var width : Number = bind scene.width on replace oldValue {
if(width < oldValue) {
ball.translateX = 0;
}
};

var height : Number = bind scene.height on replace oldValue {
if(height < oldValue) {
ball.translateY = 0;
}
};

def ball = ImageView {
image: Image { url: "{__DIR__}images/ball2.png" }
effect: InnerShadow{offsetX: 8, offsetY: 8, color: Color.PINK}
};

def scene : Scene = Scene {
width: 240
height: 320
content: ball

fill: LinearGradient {
proportional: true, startX: 0, startY: 0, endX: 0, endY: 1

stops: [
Stop { offset: 0.0, color: Color.WHITESMOKE },
Stop { offset: 1.0, color: Color.BROWN },
]
}
}

DragDrop {
target: ball
targetWidth: ball.image.width
targetHeight: ball.image.height
maxX: bind scene.width
maxY: bind scene.height
};

Stage {
title: "Drag And Drop"
scene: scene
}

Ci resta ora da implementare le funzionalità per il drag and drop. Per far questo definiamo un blocco init all'interno della DragDrop in cui dichiariamo per il nostro target il comportamento che deve tenere per due eventi del mouse, quando lo si preme - tramite la funzione onMousePressed(), e quando lo si trascina - funzione onMouseDragged():

import javafx.scene.input.MouseEvent;
...
init {
target.onMousePressed = function(e : MouseEvent) : Void {
println("click!");
}

target.onMouseDragged = function(e : MouseEvent) : Void {
println("moving");
}
}

Il nostro target (e solo lui) ora risponde ai click del mouse. Vediamo di fare in modo che risponda muovendo l'oggetto. Quando clicchiamo il mouse sull'oggetto vogliamo registrare la posizione in cui lo facciamo relativamente all'oggetto stesso. Per far questo dichiariamo due variabili startX e startY e assegnamo loro un valore nella onMousePressed() nel modo seguente:

var startX;
var startY;
...
target.onMousePressed = function(e : MouseEvent) : Void {
startX = e.sceneX - target.translateX;
startY = e.sceneY - target.translateY;
println("click [{startX}, {startY}]");
}

Definiamo startX come la ascissa del punto dove abbiamo cliccato meno l'ascissa del target (ovvero del suo punto in alto a sinistra), ottenendo in questo modo l'ascissa del click relativa all'oggetto. Lo stesso per startY rispetto alle ordinate. Dunque, se clicco in mezzo alla palla otterrò sempre un valore della coppia di 35, 35. Ovunque sia la palla nella scena.


Riscriviamo ora la onMouseDragged(), per muovere il nostro oggetto sulla scena mentre il mouse viene trascinato.

target.onMouseDragged = function(e : MouseEvent) : Void {
var tx = e.sceneX - startX;
var ty = e.sceneY - startY;

if(tx < 0) { tx = 0; }
else if(tx > maxX - targetWidth) { tx = maxX - targetWidth; }

if(ty < 0) { ty = 0; }
else if(ty > maxY - targetHeight) { ty = maxY - targetHeight; }

target.translateX = tx;
target.translateY = ty;
}

Per prima cosa ci calcoliamo le nuove coordinate dell'oggetto. La sua ascissa sarà pari all'ascissa del mouse sulla scena meno il valore dell'ascissa del mouse relativa all'oggetto. E facciamo lo stesso con l'ordinata.

Prima di spostare l'oggetto (cosa che si fa semplicemente cambiando i suoi valori translateX e translateY - ci pensa JavaFX ai dettagli) vogliamo fare in modo di restringere lo spostamento alla scena stessa.

Per cui, se l'ascissa del target che abbiamo calcolato risulta inferiore di zero, la mettiamo a zero (non é possibile andare più a sinistra del margine sinistro della finestra), se é più grande dell'ampiezza della finestra meno l'ampiezza dell'oggetto, allora le assegnamo questo valore limite (non é possibile che il nostro oggetto "trasbordi" a destra del margine della finestra). E facciamo lo stesso con le ordinate.

Il codice risultante per DragDrop.fx é questo:

package draganddrop;

import javafx.scene.Node;
import javafx.scene.input.MouseEvent;

public class DragDrop {
// the target won't change
public-init var target : Node;
public-init var targetWidth : Number;
public-init var targetHeight : Number;
// the scene size may vary
public var maxX;
public var maxY;

// for target dragging, relative cursor position in the target
var startX;
var startY;

init {
// register the mouse position within the target
target.onMousePressed = function(e : MouseEvent) : Void {
startX = e.sceneX - target.translateX;
startY = e.sceneY - target.translateY;
// println("click [{startX}, {startY}]");
}

// move the target, but not outside the scene!
target.onMouseDragged = function(e : MouseEvent) : Void {
var tx = e.sceneX - startX;
var ty = e.sceneY - startY;

if(tx < 0) { tx = 0;}
else if(tx > maxX - targetWidth) { tx = maxX - targetWidth; }

if(ty < 0) { ty = 0; }
else if(ty > maxY - targetHeight) { ty = maxY - targetHeight; }

target.translateX = tx;
target.translateY = ty;
}
}
}

Interattività per elementi GUI

Ultima lezione, la numero 8, del corso introduttivo SDN (Sun Developer Network) sulla costruzione di applicazioni GUI con JavaFX.

Per gestire l'interazione con l'utente, JavaFX usa un meccanismo di gestione degli eventi, dove ogni oggetto che può essere di interfaccia ha variabili locali che mappano funzioni che hanno a che fare con eventi.

Common Profile

Lo script che scriviamo visualizzerà un bottone a due stati (Play-Pause) che potrà essere mosso per la scena.

Quattro immagini per un bottone sulla scena

Scarichiamo dal sito sun le quattro immagini che ci serviranno per visualizzare il nostro bottone. Il play normale e premuto, lo stop normale e premuto, e mettiamole nel folder del nostro progetto.

Quindi iniziamo a scrivere il nostro script fx. Importiamo le classi per il nostro stage e la scena, poi consideriamo che avremo un gruppo come contenuto della scena, e infine le classi per l'immagine e la sua vista.
Carichiamo le nostre quattro immagini in altrettante costanti, e assegnamo una di queste alla variabile image.
Creiamo una vista per un immagine, che sarà legata alla variabile immagine appena definita. Il trucco, evidentemente, sarà quello di sfruttare il binding tra queste due variabili per gestire il cambiamento di stato del bottone.

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.Group;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

def playNormal = Image { url: "{__DIR__}play_onMouseExited.png" };
def playPressed = Image { url: "{__DIR__}play_onMousePressed.png" };
def stopNormal = Image { url: "{__DIR__}stop_onMouseExited.png" };
def stopPressed = Image { url: "{__DIR__}stop_onMousePressed.png" };

var image = playNormal;
var button = ImageView {image: bind image}

Stage {
title: "Play Button"
scene: Scene {
width: 300
height: 240
content: Group {
content: button
}
}
}

Gestione degli eventi

Vogliamo gestire tre eventi del mouse: la sua pressione, il suo rilascio e il dragging.

Definiamo quindi, prima del nostro stage, tre variabili per tener traccia dello stato del bottone e della sua posizione, in caso lo si trascini per la scena.

var mode = true; //true for Play, false for Stop
var X: Number;
var Y: Number;

Assegnamo ora al Gruppo che é il content della scena i valori relativi alla gestione degli eventi del mouse.

Gestire la pressione del mouse

Quando il mouse viene premuto (onMousePressed) teniamo traccia delle coordinate correnti del bottone e cambiamo l'immagine visualizzata.

onMousePressed: function(event) {
X = event.sceneX - event.node.translateX;
Y = event.sceneY - event.node.translateY;
image = if (mode) { playPressed; } else { stopPressed; };
}

Gestire il rilascio del mouse

Quando rilasciamo il tasto del mouse (onMouseReleased) dobbiamo cambiare l'immagine del bottone e cambiarne lo stato:

onMouseReleased: function(event) {
image = if(mode) stopNormal else playNormal;
mode = not mode;
}

Gestire il trascinamento del mouse

In questo caso non cambiamo l'immagine del bottone, inoltre il movimento é limitato dai limiti stabiliti della scena, per cui controlliamo se l'offset risultante sia esterno alla scena (minore di zero o tale che il bottone sia anche solo parzialmente esterno all'altro lato), nel qual caso forziamo il valore al limite accetabile:

onMouseDragged: function(event) {
def offsetX = event.sceneX - X;
event.node.translateX = if(offsetX <> 300 - image.width) 300 - image.width
else event.sceneX - X;

def offsetY = event.sceneY - Y;
event.node.translateY = if(offsetY <> 240 - image.height) 240 - image.height
else event.sceneY - Y;
}

L'applicazione é così completa. Rivediamo ora il codice completo:

package uiTutotial;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.Group;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

def playNormal = Image { url: "{__DIR__}play_onMouseExited.png" };
def playPressed = Image { url: "{__DIR__}play_onMousePressed.png" };
def stopNormal = Image { url: "{__DIR__}stop_onMouseExited.png" };
def stopPressed = Image { url: "{__DIR__}stop_onMousePressed.png" };

var image = playNormal;
var button = ImageView {image: bind image}

// event handling
var mode = true; //true for Play, false for Stop
var X: Number;
var Y: Number;
// event handling

Stage {
title: "Play Button"
scene: Scene {
width: 300
height: 240
content: Group {
content: button

// event handling
onMousePressed: function(event) {
X = event.sceneX - event.node.translateX;
Y = event.sceneY - event.node.translateY;
image = if (mode) { playPressed; } else { stopPressed; };
}
onMouseReleased: function(event) {
image = if(mode) stopNormal else playNormal;
mode = not mode;
}
onMouseDragged: function(event) {
def offsetX = event.sceneX - X;
event.node.translateX = if(offsetX <> 300 - image.width) 300 - image.width
else event.sceneX - X;

def offsetY = event.sceneY - Y;
event.node.translateY = if(offsetY <> 240 - image.height) 240 - image.height
else event.sceneY - Y;
} // function
} // group
} // scene
}

Desktop Profile

Scarichiamoci dal sito Sun altre due immagini che ci torneranno utili adesso, la rappresentazione del bottone al passaggio del mouse quando in stato Play e in stato Stop, e salviamole nel folder del nostro progetto.

...
def playHover = Image { url: "{__DIR__}play_onMouseEntered.png"};
def stopHover = Image { url: "{__DIR__}stop_onMouseEntered.png"};


Tooltip

Per aggiungere un Tooltip abbiamo bisogno di far riferimento alle classi Text, Font e Color, che quindi importiamo nel nostro script, dichiariamo quindi il nostro tooltip che avrà il testo definito in funzione della modalità corrente, e la sua posizione sarà legata alla posizione corrente del nostro bottone:

import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
...
var tooltip = Text {
content: bind if (mode) "Play Button" else "Stop Button"
translateX: bind button.translateX
translateY: bind button.translateY + 80
opacity: 0.0
font: Font { size: 12, name: "Tahoma" }
fill: Color.BLACK
...
Stage {
...
scene: Scene {
...
content: Group {
content: [button, tooltip]
...
}
}
};

L'ingresso del puntatore

Vogliamo che il tooltip che abbiamo definito venga mostrato all'entrata del puntatore nell'area del bottone, quindi gestiamo l'evento onMouseEntered. Per rendere più piacevole l'effetto usiamo una Timeline (classe che importiamo) che rende visibile il tooltip gradualmente in mezzo secondo, inoltre cambiamo anche la gestione dell'evento onMouseReleased, visto che vogliamo visualizzare le nuove immagini del bottone:

import javafx.animation.Timeline;
...
def appear = Timeline {
keyFrames: [
at(0s) { tooltip.opacity => 0.0 },
at(0.5s) { tooltip.opacity => 1.0 }
]
}
...
Stage {
...
scene: Scene {
...
content: Group {
...
onMouseReleased: function(event) {
image = if(mode) stopHover else playHover;
mode = not mode;
}
onMouseEntered: function(event) {
image = if (mode) playHover else stopHover;
appear.rate = 1;
appear.play();
}
} // group
} // scene
}

L'uscita del puntatore

In pratica, quando il puntatore esce dall'area del bottone, facciamo l'opposto di quello che abbiamo fatto all'ingresso:

onMouseExited: function(event) {
image = if (mode) playNormal else stopNormal;
appear.rate = -1;
appear.play();
}

Rivediamo il codice con le ultime modifiche, e con questo chiudamo lezione e tutorial.

package uiTutorial;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.Group;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

import javafx.scene.paint.Color; // tooltip
import javafx.scene.text.Font; // tooltip
import javafx.scene.text.Text; // tooltip
import javafx.animation.Timeline; // tooltip

def playNormal = Image { url: "{__DIR__}play_onMouseExited.png" };
def playPressed = Image { url: "{__DIR__}play_onMousePressed.png" };
def stopNormal = Image { url: "{__DIR__}stop_onMouseExited.png" };
def stopPressed = Image { url: "{__DIR__}stop_onMousePressed.png" };
def playHover = Image { url: "{__DIR__}play_onMouseEntered.png"};
def stopHover = Image { url: "{__DIR__}stop_onMouseEntered.png"};

var image = playNormal;
var button = ImageView {image: bind image}

// event handling
var mode = true; //true for Play, false for Stop
var X: Number;
var Y: Number;
// event handling

var tooltip = Text {
content: bind if (mode) "Play Button" else "Stop Button"
translateX: bind button.translateX
translateY: bind button.translateY + 80
opacity: 0.0
font: Font { size: 12, name: "Tahoma" }
fill: Color.BLACK
};

def appear = Timeline {
keyFrames: [
at(0s) { tooltip.opacity => 0.0 },
at(0.5s) { tooltip.opacity => 1.0 }
]
}

Stage {
title: "Play Button"
scene: Scene {
width: 300
height: 240
content: Group {
content: [button, tooltip]

// event handling
onMousePressed: function(event) {
X = event.sceneX - event.node.translateX;
Y = event.sceneY - event.node.translateY;
image = if (mode) { playPressed; } else { stopPressed; };
}
onMouseReleased: function(event) {
image = if(mode) stopHover else playHover;
mode = not mode;
}
onMouseDragged: function(event) {
def offsetX = event.sceneX - X;
event.node.translateX = if(offsetX <> 300 - image.width) 300 - image.width
else event.sceneX - X;

def offsetY = event.sceneY - Y;
event.node.translateY = if(offsetY <> 240 - image.height) 240 - image.height
else event.sceneY - Y;
}
onMouseEntered: function(event) {
image = if (mode) playHover else stopHover;
appear.rate = 1;
appear.play();
}
onMouseExited: function(event) {
image = if (mode) playNormal else stopNormal;
appear.rate = -1;
appear.play();
}
} // group
} // scene
}

Oggetti animati

Lezione numero sette del corso introduttivo SDN (Sun Developer Network) sulla costruzione di applicazioni GUI con JavaFX.

Le librerie JavaFX forniscono un supporto integrato alla creazione di animazioni. In questa lezione si mostra come costruire un oggetto UI e animarlo usando l'interpolazione lineare. E' un esempio piuttosto completo, usando la sintassi dichiarativa, il data binding, grafica e funzionalità specifiche dei nodi.

Iniziamo creando un applicazione per il common profile, aggiungeremo poi funzionalità specifiche dell'ambiente desktop.

Common Profile

L'applicazione che creiamo mostrerà una nuvoletta che si muove su un fondale stabilito, rimbalzando sui bordi.

La finestra dell'applicazione con un immagine sullo sfondo

Come al solito, partiamo definendo stage e scena. La nostra scena ha come colore di sfondo il bianco e come contenuto iniziale l'immagine di sfondo che
abbiamo scaricato qua dal sito Sun e messo nella nostra directory locale del progetto:

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

Stage {
title: "Cloud"
scene: Scene {
fill: Color.WHITE
content: [
ImageView {
image: Image { url: "{__DIR__}sun.jpg" }
} // imageView
] // content
} // scene
} // stage

Non avendo specificato la dimensione della nostra scena, questa si adatterà al contenuto, e sarà quindi determinata dalla dimensione dell'immagine.

La nuvola

Per disegnare la nuvola usiamo la classe Path, che ci permette di creare una forma custom, e la coloriamo dandole sfumature d'azzurro, usando la classe LinearGrandient.

import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
...
// nel content della scene, dopo l'immagine di sfondo:
Path {
stroke: Color.DODGERBLUE
fill: LinearGradient {
proportional: false, startX:60, startY:10, endX:10, endY:80
stops: [
Stop { offset: 0.0, color: Color.DODGERBLUE },
Stop { offset: 0.5, color: Color.LIGHTSKYBLUE },
Stop { offset: 1.0, color: Color.WHITE }
]
}
elements: [
MoveTo {x: 15, y: 15 },
ArcTo {x: 50, y: 10, radiusX: 20, radiusY: 20, sweepFlag: true},
ArcTo {x: 70, y: 20, radiusX: 20, radiusY: 20, sweepFlag: true},
ArcTo {x: 50, y: 60, radiusX: 20, radiusY: 20, sweepFlag: true},
ArcTo {x: 20, y: 50, radiusX: 10, radiusY: 5, sweepFlag: true},
ArcTo {x: 15, y: 15, radiusX: 10, radiusY: 10, sweepFlag: true},
]
} // path

La nostra nuvola ha un bordo (stroke) dodgerblue, é colorata in tre colori, da dodgerblue a bianco, a partire da (60, 10) a (10, 80).
Il bordo é costruito a partire dal punto (15,15) (la classe MoveTo crea il punto iniziale di un Path) per proseguire a (50, 10) tirando un arco che va verso l'esterno della superficie determinata (sweepFlag é true, se fosse false ci sarebbe come un "morso" nella nuvola)

Movimento orizzontale

Diamo movimento alla nuvola, per cominciare orizzontalmente. Per far ciò assegnamo una traslazione al nostro oggetto Path che definisce la nuvola fatta così:

// nel content della scena
...
Path {
translateX: bind x, translateY: 100
...
}

Dunque, la traslazione sull'asse y é fissata a 100, mentre sull'asse x é legata alla variabile x, che dovremmo definire prima.

In JavaFX viene supportato il concetto di animazione key frame. Lo stato di animazione di un oggetto viene definito dichiarando i key frame della scena in certi momenti. Specificato questo, il sistema può eseguire automaticamente l'animazione.

Vogliamo muovere la nuvola dall'ascissa 0 a 158. Il punto finale é determinato dalla dimensione del fondale (largo 241 pixel) e della nostra nuvola (che ha dimensioni 83x64), e 241 - 83 = 158.

Creiamo dunque un oggetto Timeline, che definisce al suo interno due KeyFrame, che saranno legati fra loro mediante interpolazione lineare.
Definiamo inoltre la variabile x, che verrà utilizzata nell'oggetto Path per permettere la traslazione della nostra nuvola.

import javafx.animation.Interpolator;
import javafx.animation.Timeline;

var x: Number;

Timeline {
keyFrames: [
at (0s) {x => 0.0},
at (4s) {x => 158.0 tween Interpolator.LINEAR}
]

repeatCount: Timeline.INDEFINITE
autoReverse: true
}.play();

L'oggetto Timeline che abbiamo costruito definisce la nostra animazione. Abbiamo dichiarato nella sua componente keyFrames due KeyFrame, usando la sintassi specifica JavaFX per questo tipo di oggetti, il primo al tempo 0s (dove s sta per secondi) associa alla variabile x il valore 0 (si noti l'operatore => che indica la chiamata ad un costruttore per una lista di valori), il secondo al tempo 4s associa a x il valore 158. Inoltre viene specificato mediante l'operatore tween che tipo di interpolazione andrà fatta tra i due KeyFrame definiti, in questo caso, una interpolazione lineare.

Inoltre specifichiamo che l'animazione va ripetuta indefinitivamente e che si applica l'autoreverse.

Nota che sull'oggetto costruito é applicato il metodo play(), per far partire immediatamente l'animazione.

Ci sono diversi tipi di interpolazione, a seconda dell'effetto che di vuole ottenere, ad esempio Interpolator.EASEBOTH, causa un simpatico rallentamento dell'azione avvicinandosi al KeyFrame.

Movimento verticale

E' semplice ora aggiungere movimento verticale, dovremmo semplicemente modificare il codice aggiungendo una variabile numerica (y) che sarà gestita da un'altro oggetto Timeline e che sarà legata alla proprietà translateY dell'oggetto Path:

var y: Number;
...
Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: [
at (0s) {y => 0.0},
at (7s) {y => 258.0 tween Interpolator.LINEAR},
]
}.play();
...
// nel content della scena
Path {
...
translateY: bind y
...
}

A seguire, il codice completo per lo script JavaFX realizzato fino a questo momento:

package uiTutorial;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

import javafx.scene.paint.LinearGradient; // cloud
import javafx.scene.paint.Stop; // cloud
import javafx.scene.shape.ArcTo; // cloud
import javafx.scene.shape.MoveTo; // cloud
import javafx.scene.shape.Path; // cloud

import javafx.animation.Interpolator; // key frame animation
import javafx.animation.Timeline; // key frame animation

// key frame animation
var x: Number;
var y: Number;

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true

keyFrames: [
at (0s) {x => 0.0},
at (4s) {x => 158.0 tween Interpolator.LINEAR }
]
}.play();

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: [
at (0s) {y => 0.0},
at (7s) {y => 258.0 tween Interpolator.LINEAR},
]
}.play();
// key frame animation done

Stage {
title: "Cloud"
scene: Scene {
fill: Color.WHITE
content: [
ImageView {
image: Image { url: "{__DIR__}sun.jpg" }
} // imageView

Path {
// animation
translateX: bind x, translateY: bind y

stroke: Color.DODGERBLUE
fill: LinearGradient {
proportional: false, startX:60, startY:10, endX:10, endY:80
stops: [
Stop { offset: 0.0, color: Color.DODGERBLUE },
Stop { offset: 0.5, color: Color.LIGHTSKYBLUE },
Stop { offset: 1.0, color: Color.WHITE }
]
}
elements: [
MoveTo {x: 15, y: 15 },
ArcTo {x: 50, y: 10, radiusX: 20, radiusY: 20, sweepFlag: true},
ArcTo {x: 70, y: 20, radiusX: 20, radiusY: 20, sweepFlag: true},
ArcTo {x: 50, y: 60, radiusX: 20, radiusY: 20, sweepFlag: true},
ArcTo {x: 20, y: 50, radiusX: 10, radiusY: 5, sweepFlag: true},
ArcTo {x: 15, y: 15, radiusX: 10, radiusY: 10, sweepFlag: true},
]
} // path
] // content
} // scene
} // stage

Desktop Profile

Approfittiamo dei migliori effetti messi a disposizione dal profilo desktop per rendere la nostra applicazione un poco più piacevole.

import javafx.scene.effect.Lighting;
import javafx.scene.effect.light.DistantLight;
...
// nel content della scena
Path {
// desktop enhancement
effect: Lighting{ light: DistantLight{azimuth: 90} }
...
}

L'effetto di una fonte luminosa distante dà una prospettiva alla nostra nuvola che risulta così più realistica.

Layout di elementi GUI

La sesta lezione del tutorial introduttivo sun sulla costruzione di applicazioni GUI con JavaFX é dedicato ai layout. Lo scopo di un layout é quello di permetterci di gestire componenti GUI senza specificare coordinate assolute. Questo ci dà, generalmente, una maggiore flessibilità.

Per dimostrare ciò costruiamo un piccolo script fx che visualizza un simulazione di un semaforo regolato da un gruppo di bottoni radio. Quando un bottone é selezionato il cerchietto che rappresenta il colore ad esso collegato appare colorato, altrimenti viene lasciato in grigio.

La finestra dell'applicazione

Creiamo uno script, a nome trafficLight.fx ad esempio, e dichiariamo lo stage con la annessa scena su cui lavoriamo

import javafx.stage.Stage;
import javafx.scene.Scene;

Stage {
title: "Traffic lights"
scene: Scene {
width: 210, height: 90
} // scene
}

Bottoni

Creiamo un gruppo di bottoni, vogliamo che si possa scegliere un solo bottone alla volta, quindi importiamo e usiamo la classe ToggleGroup. I bottoni utilizzati sono di tipo RadioButton.

import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
...
def group = ToggleGroup { };
def choiceText = ["STOP", "READY", "GO"];

def choices = for (text in choiceText) {
RadioButton {
toggleGroup: group
text: text
} // radioButton
}

choices[0].selected = true;

La costante choices contiene un array di radio buttons generati dal for che cicla sugli elementi di choiceText, a sua volta un array di stringhe.
Ogni radio button ha quindi la corrispondente stringa definita sopra come text, e tutti quanti hanno come toggleGroup la costante group sopra definita.
Nota che il toggleGroup sarà gestito implicitamente dai singoli bottoni.
Come ultima istruzione marchiamo come attivo il primo bottone, ovvero il semaforo sarà inizializzato col rosso.

Cerchi

Definiamo ora la costante che si occuperà di visualizzare il semaforo. Avremo bisogno delle classi Circle, Color, RadiantGradient e Stop, che importiamo.

import javafx.scene.shape.Circle;
import javafx.scene.paint.Color;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
...
def colors = ["RED", "GOLD", "GREEN"];

def lights = for (color in colors) {
Circle {
centerX: 12, centerY: 12, radius: 12
stroke: Color.DARKGRAY
fill: bind RadialGradient {
centerX: 8, centerY: 8, radius: 12,
proportional: false
stops: [
Stop {offset: 0.0 color: Color.WHITE},
Stop {offset: 1.0 color:
if (choices[indexof color].selected)
then Color.web(color)
else Color.GRAY
} // stop
] // stops
} // radialGradient
} // circle
} // lights

Con un meccanismo simile a quello utilizzato per creare l'array di bottoni creiamo l'array lights che contiene oggetti di tipo Circle.
Da notare il binding dei cerchi ai bottoni: il colore che riempe (fill) il cerchio é determinato dallo stato dell'elemento di choices il cui indice é corrispondente all'indice del cerchio che stiamo colorando. Interessante notare anche l'uso del costrutto if then else, che agisce da operatore ternario, come il buon vecchio "? :".

Box

Inscatoliamo ora le nostre componenti in HBox (orizzontale, le componenti sono piazzate da sinistra verso destra) e VBox (verticale, le componenti vanno dall'alto in basso).
L'idea é quella di avere un HBox esterno che contiene un VBox per i bottoni e un HBox per i cerchi. L'HBox risultante sarà il content della nostra scena:

import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
...
HBox {
spacing: 20
content: [
VBox {
spacing: 10
content: [ for (i in [0..2]) choices[i] ]
}, // vbox
HBox {
spacing: 15,
content: lights,
translateY: 25
}
]
}

Nota che nell'HBox dei cerchi effettuiamo una traslazione verso il basso per far apparire il semaforo centrato, e non appiattito sull'alto della finestra.

Tile

A dire il vero risulta più naturale mantenere bottoni e luci in parallelo, e per far ciò, risulta comodo usare l'inscatolamento via Tile, una sorta di array bidimensionale di cui specifichiamo righe, colonne e contenuto di ogni cella.

import javafx.scene.layout.VBox;
...
Tile {
columns: 2, rows: 3, vgap: 5
content: for (i in [0..2]) [choices[i], lights[i]]
}

Anche come codide risulta decisamente più elegante.

Questo il codice complessivo che descrive lo script che abbiamo sviluppato. Nota l'uso del booleano isBox per permettere di testare i due differenti layout.

package uiTutorial;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.RadioButton; // choices
import javafx.scene.control.ToggleGroup; // choices
import javafx.scene.shape.Circle; // lights
import javafx.scene.paint.Color; // lights
import javafx.scene.paint.RadialGradient; // lights
import javafx.scene.paint.Stop; // lights
import javafx.scene.layout.HBox; // layout
import javafx.scene.layout.VBox; // layout
import javafx.scene.layout.Tile; // layout

// choices definition
def group = ToggleGroup { };
def choiceText = ["STOP", "READY", "GO"];

def choices = for (text in choiceText) {
RadioButton {
toggleGroup: group
text: text
} // radioButton
}

choices[0].selected = true;
// choices ready for use

// lights definition
def colors = ["RED", "GOLD", "GREEN"];

def lights = for (color in colors) {
Circle {
centerX: 12, centerY: 12, radius: 12
stroke: Color.DARKGRAY
fill: bind RadialGradient {
centerX: 8, centerY: 8, radius: 12,
proportional: false
stops: [
Stop {offset: 0.0 color: Color.WHITE},
Stop {offset: 1.0 color:
if (choices[indexof color].selected) // binding applies here
then Color.web(color)
else Color.GRAY
} // stop
] // stops
} // radialGradient
} // circle
} // lights

def isBox = true;
def content = if(isBox)
then HBox {
spacing: 20
content: [
VBox {
spacing: 10
content: [ for (i in [0..2]) choices[i] ]
}, // vbox
HBox {
spacing: 15,
content: lights,
translateY: 25
}
]
} // scene content version 1
else Tile {
columns: 2, rows: 3, vgap: 5
content: for (i in [0..2]) [choices[i], lights[i]]
} // scene content version 2

Stage {
title: "Traffic lights"
scene: Scene {
width: 210, height: 90
content: content;
}
}

Legare dati e oggetti UI

Quinta lezione del corso sun sulla creazione di applicazioni GUI con JavaFX, dedicata al data binding, ovvero il meccanismo secondo cui il cambiamento di una variabile determina l'aggiornamento di un'altra variabile in accordo con la relazione che si é definita tra le due.

E' una necessità comune dello sviluppo software, aggiornare un certo parametro ogni qual volta un altro parametro cambia. Nella programmazione con JavaFX, si può ottenere questo effetto col meccanismo del data binding. Si definisce una relazione tra due variabili qualunque in modo che ogni volta che una cambia, l'altra venga aggiornata. Ci pensa JavaFX a tener traccia di ogni cambiamento e di fare ogni aggiornamento necessario.

Per vedere come il data binding funziona creiamo una semplice applicazione il cui scopo é visualizza una sorta di emulazione di una eclisse: un cerchio il cui bordo é evidenziato in giallo ha il suo contenuto sfumato e colorato in rosso. Muovendo una barra di controllo, vogliamo che il cerchio dia l'impressione che si muova solo il bordo mentre il suo contenuto resti fermo.

La finestra dell'applicazione

Creiamo il nostro script JavaFX, dichiarando lo stage e la scena in questo modo:

import javafx.stage.Stage;
import javafx.scene.Scene;

Stage {
title: "Data Binding"
scene: Scene {
width: 220, height: 170
} // scene
}

Il cerchio

Aggiungiamo un import per le classi Circle e Color, e mettiamo il nostro cerchio con bordo giallo sulla scena:

...
import javafx.scene.shape.Circle;
import javafx.scene.paint.Color;

Stage {
...
scene: Scene {
...
content: Circle {
centerX: 60, centerY: 60, radius: 50
stroke: Color.YELLOW
} // circle
} // scene
}

Dato che non abbiamo specificato i colori della scena e dell'interno del cerchio, ce li troviamo entrambi nel loro default, rispettivamente bianco e nero.

Colore riempitivo per il cerchio

Usiamo la classe RadialGradient per dare un piacevole effetto al colore interno al cerchio. Aggiungiamo perciò l'import per questa classe e per Stop, usata per definire il gradiente e definiamo il riempitivo del cerchio assegnando alla sua proprietà fill il gradiente radiale nei colori bianco e rosso:

...
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;


Stage {
...
scene: Scene {
...
content: Circle {
...
fill: RadialGradient {
centerX: 60, centerY : 60, radius: 50
focusX: 60, focusY: 30
proportional: false

stops: [
Stop {offset: 0 color: Color.RED},
Stop {offset: 1 color: Color.WHITE}
]
} // radialGradient
} // circle
} // scene
}

Barra di scorrimento

Aggiungiamo ora una barra di scorrimento sotto il cerchio, importiamo la classe Slider e aggiungiamola ai nostri oggetti sulla scena. Nota che, dato che ora gli oggetti sulla scena sono due, modifichiamo il content per fare riferimento ad un array, e non ad un singolo oggetto:

...
import javafx.scene.control.Slider;

...
Stage {
...
scene: Scene {
...
content: [
Circle {
...
} // circle

Slider {
min: 0, max: 60, value : 0
translateX: 10, translateY: 120
}
] // content
} // scene
}

Nota che la posizione dell'oggetto slider nella scena é determinata dalle sue variabili translateX e translateY.

La relazione di binding

Per poter definire la relazione di binding all'interno del cerchio, l'oggetto slider deve essere precendentemente definito (e referenziabile). Per far ciò spostiamo la sua definizione prima della creazione dello stage. Fatto questo, possiamo definire il centro del cerchio in funzione del valore dello slider:

...
def slider = Slider {
min: 0, max: 60, value : 0
translateX: 10, translateY: 120
}

Stage {
...
scene: Scene {
...
content: [
Circle {
centerX: bind slider.value + 60, centerY: 60, radius: 50
...
}, // circle
slider
] // content
} // scene
}

Nota che, avendo fatto ricorso a classi del common profile, l'applicazione può essere eseguita sia in ambiente desktop, che mobile.

Segue il codice completo dello script:

package uiTutorial;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.shape.Circle;
import javafx.scene.paint.Color;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.control.Slider;

def slider = Slider {
min: 0, max: 60, value : 0
translateX: 10, translateY: 120
}

Stage {
title: "Data Binding"
scene: Scene {
width: 220, height: 170
content: [
Circle {
centerX: bind slider.value + 60, centerY: 60, radius: 50
stroke: Color.YELLOW
fill: RadialGradient {
centerX: 60, centerY : 60, radius: 50
focusX: 60, focusY: 30
proportional: false

stops: [
Stop {offset: 0 color: Color.RED},
Stop {offset: 1 color: Color.WHITE}
]
} // radialGradient
} // circle

slider
] // content
} // scene
}

Oggetti grafici

Quarta lezione del tutorial sun sulla creazione di applicazioni GUI con javaFX, qui si riprende il discorso fatto nella prima lezione, mostrando come combinare gli oggetti grafici forniti da javaFX per crearne di nuovi da usare nelle nostre applicazioni.

Iniziamo vedendo un esempio per il common profile, poi lo estenderemo per il desktop profile che permette di applicare effetti che lo rendono ancor più attraente.

Common Profile

Diciamo che vogliamo creare un lettore audio. L'esempio che trattiamo qui si limita a mostrare come creare il bottone "play", una freccia che, pigiata, lo farà partire.

Per crearlo, combineremo oggetti e effetti resi disponibili da javaFX e che abbiamo introdotto nella prima lezione di questo tutorial.

Prima di costruire un nuova componente, é una buona idea dare un'occhiata in giro per vedere cosa c'é già disponibile in giro. Con javaFX sono messi a disposizione una serie di utili componenti nel package standard javafx.scene.control.

In ogni caso, il nostro primo passo é quello di creare un nuovo file, chiamiamolo play.fx, ad esempio.

La finestra della applicazione

Ormai la cosa ci dovrebbe riuscire naturale, importiamo la classe Stage e ne dichiariamo una istanza, assegnando al su attributo title quello sarà il titolo la finestra principale della nostra applicazione.

import javafx.stage.Stage;

Stage {
title: "Play Button"
}

La scena

All'interno dello stage prepariamo la scena in cui verrà mostrato il nostro bottone. Aggiungiamo perciò un import per le classi Scene, Color e Group, dichiariamo un oggetto Scene all'interno del nostro Stage, definiamo il colore di sfondo che vogliamo usare e specifichiamo gli oggetti contenuti nella nostra scena inizializzando la sua proprietà content con un instanza della classe Group. Al momento non ci sono oggetti, perciò definiamo come content del nostro gruppo solo un'array vuoto:

...
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.Group;

Stage {
...

scene: Scene {
width: 250, height: 350
fill: Color.WHITE
content: Group {
content: []
} //Group
} //Scene
}

Nota che la definizione delle dimensione della scena non sarebbe richiesta se la nostra applicazione corresse solo su un mobile, dato che verrebbe assunto per default che usasse tutto lo spazio a disposizione. Nel nostro caso vogliamo che sia utilizzabile anche per desktop.

Lo sfondo del bottone

Vogliamo che il nostro bottone abbia un sfondo un poco elaborato. Lavoriamo con rettangoli, perciò aggiungiamo l'import per la classe Rectangle. Vogliamo usare sfumature nei colori e per far ciò importiamo le classi LinearGradient e Stop.
L'idea é quella di avere tre rettangoli sovrapposti, racchiusi da una cornice ad angoli smussati, in varie tonalità di blu sfumate.

La struttura del nostro script verrà perciò estesa in questo modo:

...
import javafx.scene.shape.Rectangle;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

Stage {
...

scene: Scene {
...
content: Group {
content: [
Rectangle {
...
}

Rectangle {
...
}

Rectangle {
...
}

Rectangle {
...
}
]
} //Group
} //Scene

Il primo rettangolo descriverà l'area in alto:

Rectangle {
x: 48, y: 46, width: 150, height: 59
stroke: Color.GREEN
fill: LinearGradient {
startX: 0.0, startY: 1.0, endX: 0.0, endY: 0.0,
proportional: true
stops: [
Stop { offset: 0.0, color: Color.web("#aaeeff") },
Stop { offset: 1.0, color: Color.web("#66aacc") }
]
} // fill
}

Si tratta di un rettangolo contornato da una sottile riga verde, sfumato usando due tonalità di blu.
Nota che il gradiente utilizzato per definire la sfumatura ha la proprietà proportional definita come true, in questo modo definiamo gli stop in formato relativo, decisamente più comodo.

Il secondo rettangolo definisce l'area centrale, é molto simile al primo, la differenza più grossa é che non ha un contorno:

Rectangle {
x: 48, y: 106, width: 150, height: 64
fill: LinearGradient {
startX: 0.0, startY: 1.0, endX: 0.0, endY: 0.0,
proportional: true
stops: [
Stop { offset: 0.0, color: Color.web("#99ddff") },
Stop { offset: 1.0, color: Color.web("#337799") }
]
} // fill
}

Il terzo rettangolo definisce l'area in basso:

Rectangle {
x: 48, y: 170, width: 150, height: 25
fill: LinearGradient {
startX: 0.0, startY: 1.0, endX: 0.0, endY: 0.0,
proportional: true
stops: [
Stop { offset: 0.0, color: Color.web("#337799") },
Stop { offset: 1.0, color: Color.web("#99ddff") }
]
} //fill
}

E infine usiamo un quarto rettangolo per incorniciare i tre di cui sopra:

Rectangle {
x: 48, y: 45, width: 150, height: 150
arcWidth: 15, arcHeight: 15,
stroke: Color.web("#337799")
strokeWidth: 5
fill: null
}

Notiamo che questo rettangolo é arrotondato (nota la definizione delle variabili arcWidth e arcHeight diverse da zero) ha un bordo piuttosto spesso (5 pixel) e, soprattutto, non ha nulla al suo interno (la variabile fill é settata a null). Dunque é solo una cornice, che lascia vedere quanto c'é sotto.

Il tasto Play

Abbiamo definito il fondale, ora vogliamo aggiungere un tasto che sarà composto da un cerchio con un triangolo al suo interno, che rappresenta una freccia verso destra. Anzi, per dare un'elegante effetto tridimensionale, di triangoli ne facciamo due, sovrapposti.

Avremo bisogno di altre due classi, Circle e Polygon, che importiamo. Poi aggiungeremo i nostri tre nuovi oggetti al gruppo sulla scena:

...
import javafx.scene.shape.Circle;
import javafx.scene.shape.Polygon;
...
Group {
content: [
...
Circle {
centerX: 122 centerY: 122 radius: 38
fill: LinearGradient {startX: 0.0, startY: 1.0, endX: 0.0, endY: 0.0,
proportional: true
stops: [
Stop {offset: 0.0 color: Color.web("#66aacc")},
Stop {offset: 1.0 color: Color.web("#99ddff")}
]
} // fill
stroke: Color.web("#66aacc")
strokeWidth: 2.0
} // circle

Polygon {
points: [142.0, 126.0, 113.0, 108.0, 111.0, 143.0]
fill: Color.web("#337799")
} // lower triangle

Polygon {
points: [142.0, 123.0, 110.0, 105.0, 110.0, 140.0]
fill: Color.web("#ffffff")
} // upper triangle
]

E questo é tutto per il nostro esempio in ambiente comune.

Desktop Profile

L'ambiente desktop permette di sbizzarrirsi un po' di più con gli effetti visuali.

Una cosa che possiamo fare, é ottenere l'effetto tridimensionale sul triangolo senza raddoppiare gli oggetti, ma aggiungendo al triangolo una ombreggiatura.

Effetto DropShadow

Importiamo la classe DropShadow e ridefiniamo il nostro triangolo usando un solo poligono definito come segue:

import javafx.scene.effect.DropShadow;
...
Polygon {
points: [142.0, 123.0, 110.0, 105.0, 110.0, 140.0]
fill: Color.web("#ffffff")
effect: DropShadow {
color: Color.web("#337799")
offsetX: 2, offsetY: 5
}
}


Effetto reflection

Come ultima cosa, aggiungiamo un riflesso alla nostra immagine, importiamo la classe Reflection, e aggiungiamo un effetto al nostro gruppo di oggetti sulla scena:

import javafx.scene.effect.Reflection;
Group {
content: [
...
]
effect: Reflection { fraction: 0.5 topOpacity: 0.5 topOffset: 0.5 }
} //Group

Come riferiment, lascio qui a seguire il codice completo per lo script di questa lezione:

package uiTutorial;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.Group;
import javafx.scene.shape.Rectangle;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Polygon;
import javafx.scene.effect.DropShadow; // for desktop enhancement
import javafx.scene.effect.Reflection; // for desktop enhancement

Stage {
title: "Play Button"

scene: Scene {
width: 250, height: 350
fill: Color.WHITE
content: Group {
content: [
Rectangle {
x: 48, y: 46, width: 150, height: 59
stroke: Color.GREEN
fill: LinearGradient {
startX: 0.0, startY: 1.0, endX: 0.0, endY: 0.0,
proportional: true
stops: [
Stop { offset: 0.0, color: Color.web("#aaeeff") },
Stop { offset: 1.0, color: Color.web("#66aacc") }
]
} // fill
} // top rectangle

Rectangle {
x: 48, y: 106, width: 150, height: 64
fill: LinearGradient {
startX: 0.0, startY: 1.0, endX: 0.0, endY: 0.0,
proportional: true
stops: [
Stop { offset: 0.0, color: Color.web("#99ddff") },
Stop { offset: 1.0, color: Color.web("#337799") }
]
} // fill
} // central rectangle

Rectangle {
x: 48, y: 170, width: 150, height: 25
fill: LinearGradient {
startX: 0.0, startY: 1.0, endX: 0.0, endY: 0.0,
proportional: true
stops: [
Stop { offset: 0.0, color: Color.web("#337799") },
Stop { offset: 1.0, color: Color.web("#99ddff") }
]
} //fill
} // bottom rectangle

Rectangle {
x: 48, y: 45, width: 150, height: 150
arcWidth: 15, arcHeight: 15,
stroke: Color.web("#337799")
strokeWidth: 5
fill: null
} // border rectangle

Circle {
centerX: 122 centerY: 122 radius: 38
fill: LinearGradient {startX: 0.0, startY: 1.0, endX: 0.0, endY: 0.0,
proportional: true
stops: [
Stop {offset: 0.0 color: Color.web("#66aacc")},
Stop {offset: 1.0 color: Color.web("#99ddff")}
]
} // fill
stroke: Color.web("#66aacc")
strokeWidth: 2.0
} // circle

/*
// common profile version for the triangle
Polygon {
points: [142.0, 126.0, 113.0, 108.0, 111.0, 143.0]
fill: Color.web("#337799")
} // lower triangle

Polygon {
points: [142.0, 123.0, 110.0, 105.0, 110.0, 140.0]
fill: Color.web("#ffffff")
} // upper triangle
*/
Polygon {
points: [142.0, 123.0, 110.0, 105.0, 110.0, 140.0]
fill: Color.web("#ffffff")
effect: DropShadow {
color: Color.web("#337799")
offsetX: 2, offsetY: 5
}
} // triangle as desktop enhancement
] // group content

// effect as desktop enhancement
effect: Reflection { fraction: 0.5 topOpacity: 0.5 topOffset: 0.5 }
} //Group
} //Scene
}