WPF offre un gran supporto alle applicazioni che vogliono utilizzare animazioni. Tramite poche righe di Xaml possiamo dire agevolmente che vogliamo animare la proprietà di riempimento di un oggetto da un colore ad un altro in un certo periodo di tempo infinite volte utilizzando perfino delle funzioni matematiche per variare non linearmente l'animazione.
Esempio di animazione di un colore in Xaml
Non è però previsto un modo per aggiungere animazioni in maniera sincronizzata a quelle già esecuzione.
Se iniziamo un'animazione che sfuma dal bianco al rosso un ellisse e, supponiamo a metà dell'animazione, vogliamo iniziare un'altra animazione su un altro oggetto ma allo stesso tempo vogliamo tenere allineate temporalmente le due animazioni come possiamo fare?
Altri esempi di utilizzo possono essere led che lampeggiano continuamente in sincronia nella nostra animazione al cambiamento di determinate proprietà o eventi.
La tecnica più semplice se non ci preoccupa il fatto di dover fermare a metà le animazioni in esecuzione, è fermare le animazioni in corso e riiniziarle con la nuova.
Eseguire queste operazioni può significare scrivere tanto codice ogni volta ma vediamo come creare un TriggerAction che ci permette di incapsulare questa funzionalità e renderla riutilizzabile e fruibile da Xaml.
In sostanza animare la proprietà Fill di un ellisse diventerà facile come scrivere:
Dove il namespace "i" si riferisce al namespace Interactive aggiunto da Expression Blend ma che verrà molto probabilmente incluso ufficialmente nel framework nella prossima versione.
Sono stati utilizzati gli Interactive Trigger di Expression Blend e quindi è necessario aggiungere al progetto la Reference a System.Windows.Interactive e copiare la relativa .dll assieme all'exe (avviene in automatico dopo aver aggiunto la Reference) perchè a differenza di quelli inclusi nella versione attuale del Framework permettono una maggiore estendibilità ed aprono quindi nuovi scenari come quello descritto in questo articolo.
Ancora meglio possiamo addirittura fare drag-and-drop da Expression Blend del nostro TriggerAction dagli Asset di tipo Behaviors sul nostro oggetto e settare poco più di un paio di proprietà nella scheda Miscellaneous.
Vediamo ora come creare la nostra classe che chiameremo SyncAnimationAction.
Innanzitutto dovremo derivare SyncAnimationAction da System.Windows.Interactivity.TriggerAction<DependencyObject> per poter essere richiamata senza scrivere codice da ulteriori Interaction Trigger.
I Trigger possono scatenare l'animazione sincronizzata quando cambia una proprietà (PropertyChangedTrigger), allo scatenarsi di un evento (EventTrigger), alla pressione di un pulsante (KeyTrigger), etc..
Aggiungiamo quindi alla nostra classe 4 Proprietà che potranno essere impostate dallo Xaml come visto prima:
Proprietà
|
Tipo
|
Descrizione
|
Animation
|
AnimationTimeline
|
L'animazione da eseguire
|
AnimatedObject
|
IAnimatable
|
L'oggetto su cui eseguire l'animazione
|
AnimatedProperty
|
DependencyProperty
|
La proprietà dell'oggetto da animare
|
AnimationStopped
|
bool
|
Flag che indica se l'animazione è da terminare
|
Le prime 3 proprietà (o anche AnimationStopped se si ha la necessità) dovranno essere DependencyProperty per supportare il Binding tramite XAML.
Effettuiamo poi l'override del metodo Invoke che verrà scatenato dai Trigger ed aggiungiamo poi tramite una classe "contenitore" AppliedAnimation i nostri valori ad una variabile List<AppliedAnimation> statica chiamata _animations.
Quando verrà chiamato Invoke la nostra classe avrà già popolate le proprietà (AnimatedObject, AnimatedProperty, Animation e AnimationStopped) da Xaml quindi dovremo solamente controllare che questi dati siano validi per poterli aggiungere alla lista.
La lista servirà per tenere dei riferimenti alle nostre animazioni e ci permetterà di fermare/riavviare tutte le animazioni nella lista all'aggiunta di una nuova animazione.
All'aggiunta di una nuova animazione scorreremo tutta la lista delle animazioni fermandole e facendole ripartire.
Per gestire le animazioni non ci serviremo della classe Storyboard in quanto trova la sua maggiore utilità nel codice XAML ma semplicemente utilizzeremo il metodo BeginAnimation passando rispettivamente prima il secondo parametro nullo e poi con l'animazione da eseguire.
Eviteremo quindi l'overhead dovuto ad instanziare un ulteriore StoryBoard.
Spostiamo dunque il codice di sincronizzazione vero e proprio in un metodo statico ReSync per poterlo chiamare eventualmente anche da codice C# (o VB.NET).
Come facciamo però a rimuovere un'animazione?
Qui entra in gioco la quarta proprietà AnimationStopped, semplicemente nel metodo Invoke dovremo controllare se l'animazione è già presenta in lista e in tal caso fermarla ed eliminarla.
Memory Leak?
Mantenendo un riferimento agli oggetti IAnimatable nella lista tramite un AppliedAnimation potremmo causare un memory leak, è quindi consigliabile utilizzare la classe WeakReference al posto del tipo IAnimatable nella lista perché così il Garbage Collector potrà cancellare gli oggetti se sono rimasti solamente nella nostra lista.
E se aggiungo molte animazioni?
Se si aggiungono tante animazioni, ad ogni animazione aggiunta vengono riavviate le precedenti e questo potrebbe causare qualche problema prestazionale se le animazioni sono complesse e se sono in gran numero.
Esempio: se aggiungo 100 animazioni avrò 100 ReSync che complessivamente riavvieranno 100+99+98+....+1 animazioni e questo potrebbe non essere desiderato, è quindi necessario inserire una proprietà che permetta di "saltare" il metodo ReSync fino a quando tutte le animazioni non saranno state aggiunte. In questo modo si potrà avere un solo ReSync alla fine dell'aggiunta in blocco delle animazioni.
Nel codice allegato è presente l'implementazione comprensiva di WeakReference