La couche View
Les composants visuels

Les composants visuels doivent être réutilisables de projet à projet. Ils sont donc agnostiques au projet, et ne doivent contenir aucune référence au framework. On doit littéralement pouvoir les importer dans un projet vide, et les lancer seuls – ils sont autonomes.
Pour ce projet nous aurons deux composants visuels :
- DiaporamaDisplay : c’est un UILoader en plus léger et plus classe. On lui passe une URL et il charge cette image puis l’affiche. Et c’est tout.
package view.components
{
import model.vo.Entry;
import controller.events.DiaporamaEvent;
import com.gskinner.motion.GTween;
import com.gskinner.motion.easing.Quadratic;
import flash.display.Sprite;
import flash.events.Event;
import flash.display.Loader;
import flash.display.Bitmap;
import flash.net.URLRequest;
import flash.display.LoaderInfo;
/**
* composant visuel basique,
* un afficheur d'image simple avec une transition animée
*/
public class DiaporamaDisplay extends Sprite
{
private var _front : Sprite;
private var _back : Sprite;
private var _backContent : Bitmap;
private var _frontContent : Bitmap;
private var _current : Sprite;
public function DiaporamaDisplay()
{
_back = new Sprite();
_front = new Sprite();
addChild(_back);
addChild(_front);
}
public function refresh( entry : Entry ) : void
{
var l : Loader = new Loader();
l.contentLoaderInfo.addEventListener( Event.COMPLETE, onLoadComplete );
l.load( new URLRequest(entry.file) );
}
private function onLoadComplete( e : Event ) : void
{
_backContent = (e.target as LoaderInfo).loader.content as Bitmap;
_back.alpha = 0;
_backContent.addEventListener( Event.ADDED_TO_STAGE, onAdded );
_back.addChild(_backContent);
}
private function onAdded(e:Event):void
{
removeEventListener(Event.ADDED_TO_STAGE, onAdded);
new GTween( _back, .5, { alpha: 1 }, { onComplete: onFadeComplete, ease : Quadratic.easeOut } );
new GTween( _front, .5, { alpha: 0 }, { ease : Quadratic.easeIn } );
}
private function onFadeComplete( g : GTween ) : void
{
var frontBuffer : Sprite = _front;
_front = _back;
_back = frontBuffer;
dispatchEvent( new DiaporamaEvent( DiaporamaEvent.LOAD_COMPLETE ) );
}
}
}
- DiaporamaControls : constitué d’une flêche vers la droite (btNext) et d’une flèche vers la gauche (btPrevious). il n’a que 2 méthodes, enable() et disable(), pour freezer les boutons, par exemple le temps d’un chargement.
package view.components
{
import controller.events.DiaporamaEvent;
import com.gskinner.motion.GTween;
import flash.display.MovieClip;
import flash.events.MouseEvent;
/**
* composant visuel basique, 2 flêches que l'on peut "geler"
* par exemple le temps d'un chargement
*/
public class DiaporamaControls extends DiaporamaControls_skin
{
public function DiaporamaControls()
{
disable();
}
/**
* rend actif les flêches
*/
public function enable() : void
{
for each( var button : MovieClip in [ btPrevious, btNext ] ) {
if ( button.alpha != 1 )
new GTween( button, .5, { alpha : 1 } );
button.addEventListener( MouseEvent.CLICK, onClick );
button.buttonMode = true;
}
}
/**
* rend inactif les flêches
*/
public function disable() : void
{
for each( var button : MovieClip in [ btPrevious, btNext ] ) {
if ( button.alpha != .5 )
new GTween( button, .5, { alpha : 0 } );
button.removeEventListener( MouseEvent.CLICK, onClick );
button.buttonMode = false;
}
}
/**
* émission d'un Event simple au click
*
* @param e
*/
private function onClick( e : MouseEvent ) : void
{
dispatchEvent( new DiaporamaEvent(
{
btNext : DiaporamaEvent.NEXT,
btPrevious : DiaporamaEvent.PREVIOUS
}[e.currentTarget.name]
) );
}
}
}
Nous avons bien 2 composants agnostiques et autonomes, sans aucune référence à un quelconque objet issu de l’application. Pour être parfait, nous aurions dû utiliser un Event séparé par composant, et faire un .swc par composant + événement attitré.
Les Mediators
Le rôle des Mediators est :
- d’écouter l’application et agir sur leur composant,
- d’écouter leur composant et d’en informer l’application.
Commençons par DisplayControlsMediator, qui va s’occuper de DisplayControls:
package view.mediators
{
import controller.events.DiaporamaEvent;
import view.components.DiaporamaControls;
import org.robotlegs.mvcs.Mediator;
/**
* Agit sur diaporamaDisplay en fonction des événements reçus
* du composant ou de l'application
*
*/
public class DiaporamaControlsMediator extends Mediator
{
[Inject]
public var controls : DiaporamaControls;
override public function onRegister() : void
{
// événements issus de l'application
eventMap.mapListener( eventDispatcher, DiaporamaEvent.LOAD_COMPLETE, onLoadComplete );
// événements issus du composant
controls.addEventListener( DiaporamaEvent.NEXT, onClick );
controls.addEventListener( DiaporamaEvent.PREVIOUS, onClick );
}
private function onClick( e : DiaporamaEvent ) : void
{
controls.disable();
dispatch(e);
}
private function onLoadComplete( e : DiaporamaEvent ) : void
{
controls.enable();
}
}
}
On constate que son rôle est d’appeler les méthodes enable() et disable() au bon moment, et forwarder son événement de clic, tout simplement.
Occupons-nous de DiaporamaDisplayMediator, qui s’occupe de DiaporamaDisplay:
package view.mediators
{
import controller.events.DiaporamaEvent;
import view.components.DiaporamaDisplay;
import org.robotlegs.mvcs.Mediator;
/**
* Agit sur diaporamaDisplay en fonction des événements reçus
* du composant ou de l'application
*
*/
public class DiaporamaDisplayMediator extends Mediator
{
[Inject]
public var display : DiaporamaDisplay;
override public function onRegister() : void
{
// événements issus de l'application
eventMap.mapListener( eventDispatcher, DiaporamaEvent.REFRESH, onRefresh );
// événements issus du composant
display.addEventListener( DiaporamaEvent.LOAD_COMPLETE, onLoadComplete );
}
private function onRefresh( e : DiaporamaEvent ) : void
{
display.refresh(e.entry);
}
private function onLoadComplete( e : DiaporamaEvent ) : void
{
dispatch(e);
}
}
}
On constate que son rôle est d’appeler la méthode refresh() au bon moment, et forwarder son événement de fin de chargement.
On remarque un DiaporamaEvent.REFRESH, qui n’est pas mappé sur une Command dans DiaporamaContext. En effet, seul ce Mediator est concerné par cet événement, il n’est pas obligatoire d’utiliser une Command.
L’utilité et le rôle d’un Mediator est souvent difficile à cerner pour un débutant. A la relecture de l’integralité du code que nous avons tapé pour l’instant, nous constatons que “c’est là que se font les addEventListener sur la Vue, et les appels de méthodes”. C’est aussi simple que ça.
Note : on voit que l’injecteur est utilisé pour injecter le composant. C’est une très mauvaise idée d’utiliser cet injecteur pour injecter directement dans le Mediator un Model. Cela nuit au découplage en couches. Si un Mediator a besoin d’une donnée, cette donnée doit lui parvenir via un Event reçu, et c’est tout.
Super article, c’est nickel.
Cependant, je n’ai pas trouvé la classe DiaporamaControls_skin …. Dommage
DiaporamaControls_skin dépend du design, donc j’en ai laissé l’implémentation au lecteur. Typiquement, c’est juste un MovieClip contenant 2 boutons, respectivement nommés btPrevious et btNext.
Tu peux dessiner ce MovieClip dans l’IDE Flash, lui attribuer le nom d’export DiaporamaControls_skin, publier en .SWC, et ajouter ce .SWC à ton projet. C’est la méthode qui a été employé pour la rédaction de l’article.
Bonjour,
et merci pour ce tuto…
J’ai repéré un petit bug
A la 2eme image chargée, l’ancienne ne disparait pas. Ce qui fait que les images se superposent.
En effet dans la classe DiaporamaDisplay, tu ne te sers pas de _frontContent.
Donc dans la classe « onFadeComplete », je fais ceci :
if (_frontContent !=null) _front.removeChild(_frontContent);
Et là c’est bon. Je ne sais pas si c’est la bonne methode mais bon, ca marche…
Si tu vois qqchose de mieux, je suis preneur.
Et tout cas merci à toi.
cyrille
C’est rigoureusement exact et merci de le souligner
J’oubliais :
Dans la function onLoadComplete, toujours dans DiaporamaDisplay, j’ai egalement fait ceci :
_frontContent = _backContent;
Merci.
cyrille