Flex4 : skin et primitives

Dans le dernier article, nous avons fait un petit tour d’horizon du nouveau système de skin de Flex4. J’ai brièvement présenté EazeElement que j’utilise dans l’article. Nous allons maintenant rentrer un peu plus profondément dans les primitives, qui vous permettent de créer vos propres formes et autres éléments graphiques personnalisés.
Retour sur les primitives
Dans Flex4, une Skin est une agglomération d’éléments graphiques, définis avec un type de ligne, de fond, et des filtres. L’objet graphique final est une superposition de rectangles, de lignes, d’ellipses, d’images, de formes complexes.
Intéressons nous à celles qui utilisent directement l’API de dessin : Rect, Ellipse, Line, Path
Comment ça marche?
L’architecture des primitives utilisant l’API de dessin est la suivante :

Les classes de bases :
- GraphicElement : la base des primitives. Cette classe définit les tailles et positionnement de l’objet dans l’espace, avec ses contraintes de position. Les getters protégés drawX et drawY, utilisés par toutes les primitives, renvoient la position réelle en x et y de l’objet.
- StrokedElement : définit le Stroke (interface mx.graphics.IStroke), l’apparence de la ligne, de la primitive. La base des méthodes de dessin est présente : beginDraw, draw, endDraw. La méthode beginDraw applique le style du Stroke dans l’élément graphique Graphics de la primitive.
- FilledElement : définit le Fill (interface mx.graphics.IFill), l’apparence du fond, de la primitives. La méthode beginDraw prépare l’objet graphique en appliquant la ligne Stroke et le fond Fill. A ce niveau, la méthode draw n’effectue aucun tracé.
Les primitives :
- Line : (extension de StrokedElement) dessine une ligne, passant par 2 points (xFrom/yFrom et xTo/yTo). Si votre ligne est encadrée dans des contraintes top, bottom, left, right; la ligne dessinée sera une diagonale, respectant les contraintes top/left et bottom/right;
- Ellipse : (extension de FilledElement) dessine une ellipse définie par ses contraintes de positionnement;
- Rect : (extension de FilledElement) dessine un rectangle défini par ses contraintes de positionnement. Sa particularité est de pouvoir définir des rayons de courbure globaux (radiusX/radiusY), ou localisés aux angles du rectangle (topLeftRadiusX/topLeftRadiusY, bottomLeftRadiusX/bottomLeftRadiusY, …).
- Path : (extension de FilledElement) dessine une forme complexe, définie dans une propriété data. Cette source utilise la syntaxe SVG.
Digression : Définition et obtention d’un “data Path”
Le “data source” d’une primitive Path est définit par un code, identique à celui utilisé dans le format vectoriel SVG. Ce code ressemble a celui-ci :
M 8 4 C 8 4 15 4 15 4 L 15 0 L 27 8 L 15 15 L 15 11 L 8 11 L 8 11 L 8 11 L 0 11 C 0 11 0 11 0 11 C 0 7 3 4 8 4 Z
Il représente une flèche avec un bord arrondi, d’une largeur de 27 pixels, et une hauteur de 15 pixels.
Fonctionnement du code
c’est une succession de commandes (définies par des lettres), et de positions de points (les chiffres) en pixels utilisées par la commande.
Les différentes commandes sont :
- M : Move, déplacement de la ligne vers le point définit par le couple de chiffres suivant. Exemple : “M 8 4” déplace le point courant de la ligne à la position x=8 et y=4. C’est toujours la première commande du code.
- L : Line, création d’une ligne, avec comme point de destination le couple de chiffres suivant. Exemple “ L 15 11” crée la ligne depuis le point précédent, vers le point x=15 et y 11.
- H : Horizontal Line, identique à la commande L, à la différence que seul le point de destination x est nécessaire. Exemple : “H 15” construit une ligne horizontale depuis le point précédent, vers le point x=15.
- V : Vertical Line, identique à la commande H, en vertical.
- Q : Quadratic Bezier, création d’une ligne courbe, a partir du précédent point, à destination du point définit par le dernier couple x/y, et passant par le point de contrôle définit par le premier couple x/y. exemple : “Q 15 11 20 22” la ligne est créée entre le point précédent et le point x=20, y=22, en passant par le point de contrôle x=15, y=11;
- C : Cubic Bezier, identique à la commande Q, à la différence que la ligne passe par un point de contrôle supplémentaire. Exemple : “ C 0 7 3 4 8 4” la ligne est créée entre le point précédent et le point x=8 / y=4, en passant par un premier point de contrôle x=0, y=7, et un second x=3, y=4;
- Z : Close, définit la fermeture de la forme. C’est aussi le dernier code du data.
Obtenir un code à partir d’une forme
La meilleur méthode pour obtenir le code de votre forme vectorielle est de passer par Fireworks. Je vous recommande fortement d’utiliser ce logiciel pour vos créations d’interface, car très complet, performant, et beaucoup plus adapté à la création graphique web que Photoshop ou Illustrator.
Fireworks, depuis la CS4, dispose d’une commande d’exportation FXG. Vous pouvez la trouver sous “Commandes” > “Exporter au format FXG”.
Il vous suffit de sélectionner votre forme, (si rien n’est sélectionné, c’est la totalité du document, y compris les calques masqué, qui sera exporté), et de lancer la commande d’exportation.

Vous obtenez un document de ce type :
<?xml version="1.0" encoding="UTF-8"?>
<Graphic version="1.0"
xmlns="http://ns.adobe.com/fxg/2008"
xmlns:d="http://ns.adobe.com/fxg/2008/dt"
xmlns:fc="http://ns.adobe.com/flashcatalyst/2009"
viewHeight= "40" viewWidth= "150">
<Library>
<Definition name="Selection">
<Group d:userLabel="Symbol">
<Group d:userLabel="Group_0">
<Path winding="evenOdd"
data="M 8 4 C 8 4 15 4 15 4 L 15 0 L 27 8 L 15 15 L 15 11 L 8 11 L 8 11 L 8 11 L 0 11 C 0 11 0 11 0 11 C 0 7 3 4 8 4 Z "
blendMode="normal" alpha="1">
<fill>
<SolidColor color="#b8b3af"/>
</fill>
</Path>
</Group>
</Group>
</Definition>
</Library>
<Selection x="0" y="0" />
</Graphic>
Copiez le data du noeud <Path />, et utilisez le dans votre skin comme dans cette exemple :
<s:Path
top="0" left="0"
data="M 8 4 C 8 4 15 4 15 4 L 15 0 L 27 8 L 15 15 L 15 11 L 8 11 L 8 11 L 8 11 L 0 11 C 0 11 0 11 0 11 C 0 7 3 4 8 4 Z ">
<s:fill>
<s:LinearGradient rotation="90">
<s:GradientEntry color.selected="#FCD301" color="#F7F7F7" ratio="0" />
<s:GradientEntry color.selected="#F19000" color="#C2C6C7" ratio="1" />
</s:LinearGradient>
</s:fill>
<s:filters>
<s:DropShadowFilter color="#000000"
distance="1"
blurX="4" blurY="4" quality="3"
alpha="0.8" />
</s:filters>
</s:Path>
Et maintenant?
L’idée serait de pouvoir créer des primitives, spécialisées ou non, utilisables dans un contexte de composants très précis, comme la primitive EazeElement du dernier article; ou dans une perspective plus globale, type librairie… Allons-y
Un trou dans mon Rectangle
Il peut être intéressant dans certains cas d’avoir un “trou” dans un rectangle. Pour créer par exemple une bordure qui pourrait avoir un Stroke ET un Fill. C’est ce que j’ai fais ici.
Pour obtenir un rectangle “troué” avec l’api de dessin, c’est extrêmement simple : il suffit de dessiner 2 rectangles l’un dans l’autre, à la suite : les points de vecteurs faisant partie du même chemin, cela génère l’exclusion du rectangle 2 dans le rectangle 1.
![]() |
![]() |
Bon c’est une méthode rapide, mais qui est suffisante pour nos besoin… Nous ne sommes pas en train de coder un outil de Pathfinding !
Dessin de rectangle
Au lieu d’utiliser les méthodes de dessin de rectangle fournies par Flex, j’utilise celle proposée par Philippe Elsass. Performante, elle m’évite de recourir au méthode de base, qui lance des contrôles de positions évitable.
Pour plus d’information sur son fonctionnement, je vous invite à vous rendre sur cette explication.
La méthode utlisée dans la librairie, que j’expose ci-après, derrière ce lien.
La primitive ExclusionRect
Donc nous avons besoin de 2 rectangles. Le premier est celui qui correspond aux contraintes de positionnement de base de la primitive. J’utilise une petite méthode getOutterVectorPoint qui me renvoie le vecteur définissant les points de ce rectangle. Le second rectangle, celui définissant la zone à exclure, à lui besoin de contraintes spécifiques, que j’ai ajouté dans la classe (innerTop, innerRight, innerLeft, innerBottom). la méthode getInnerVectorPoint remplit la même fonction que getOutterVectorPoint. Dans l’override de la méthode draw, celle qui “dessine”, les 2 rectangles sont créés l’un après l‘autre. Cela donne :
/**
* Return the Vector of Point for the rectangle
*/
protected function getOutterVectorPoint() : Vector.<Point>
{
var vector : Vector.<Point> = new Vector.<Point>();
vector.push( new Point( drawX, drawY ) );
vector.push( new Point( drawX+width, drawY ) );
vector.push( new Point( drawX+width, drawY+height ) );
vector.push( new Point( drawX, drawY+height ) );
return vector;
}
/**
* Return the Vector of Point for the inner rectangle
*/
protected function getInnerVectorPoint() : Vector.<Point>
{
var vector : Vector.<Point> = new Vector.<Point>();
vector.push( new Point( drawX+innerLeft, drawY+innerTop ) );
vector.push( new Point( drawX+width-innerRight, drawY+innerTop ) );
vector.push( new Point( drawX+width-innerRight, drawY+height-innerBottom) );
vector.push( new Point( drawX+innerLeft, drawY+height-innerBottom ) );
return vector;
}
/**
* Draw the rectangle
* @see com.lafabrick.uigfx.utils.UigfxUtils
*/
override protected function draw(g:Graphics) : void
{
UigfxUtils.drawRoundPath( g, getOutterVectorPoint(), radius, true );
if( enableExclusion ) {
UigfxUtils.drawRoundPath( g, getInnerVectorPoint(), innerRadius, true );
}
}
Utilisation
L’utilisation est très proche de celle de Rect, avec les contraintes et radius intérieur :
<primitives:ExclusionRect id="excl"
radius="10"
innerRadius="30"
top="0" left="0" right="0" bottom="0"
innerTop="0" innerBottom="20" innerLeft="0" innerRight="20">
<primitives:fill>
<s:SolidColor color="#353535" />
</primitives:fill>
<primitives:filters>
<s:DropShadowFilter color="#000000" inner="false" distance="1"
blurX="4" blurY="4"
quality="3" alpha="0.8" />
</primitives:filters>
</primitives:ExclusionRect>
La classe complète de la primitive

Dessine moi une forme !
Une primitive qui me manque est celle permettant de dessiner une forme, à la manière de Path, mais en définissant un tableau de points. Quelque chose qui ressemble à ça :
<primitives:PointsPath
radius="60" width="100%" height="100%"
horizontalCenter="0" verticalCenter="0">
<primitives:points>
<mx:Point x="100" y="0" />
<mx:Point x="125" y="75" />
<mx:Point x="200" y="75" />
<mx:Point x="140" y="125" />
<mx:Point x="160" y="200" />
<mx:Point x="100" y="155" />
<mx:Point x="40" y="200" />
<mx:Point x="60" y="125" />
<mx:Point x="0" y="75" />
<mx:Point x="75" y="75" />
</primitives:points>
<primitives:fill>
<s:SolidColor color="#222222" />
</primitives:fill>
<primitives:filters>
<s:DropShadowFilter color="#000000" inner="true" distance="1"
blurX="4" blurY="4"
quality="3" alpha="0.8" />
</primitives:filters>
</primitives:PointsPath>
Bien, mais pas suffisant.
Dans ce premier essai, les points sont définis “statiquement”. Une utilisation parfaite pour moi d’une telle primitive serait de pouvoir définir des contraintes de placement sur les points (top, verticalCenter, bottom et left, horizontalCenter, right), par rapport à la primitive.
J’ai donc fait une classe ConstraintPoint.
ConstraintPoint : un point “contraint”
Cette classe toute simple, extension de Point, dispose de propriétés de placement, en getter/setter : top, left, bottom, right, horizontalCenter, verticalCenter. Une méthode getConstraintPoint() renvoie un point x/y, calculer par rapport à une cible GraphicElement, en tenant compte des contraintes de placement. J’ai préféré avoir ce type de fonctionnement, plutôt que d’avoir le couple x/y directement modifié lors du set des différentes propriétés de contraintes.
Une primitive se dessine par rapport à son positionnement global. GraphicElement fournit les propriétés protégés drawX et drawY, qui donne les positions x/y de la primitive de manière global, par rapport à l’application.
La méthode getConstraintPoint() doit donc renvoyer le point par rapport à l’application. GraphicElement fournit tout la mécanique de calcul nécessaire, notamment l’objet postLayoutTransformOffsets, qui contient la position globale de la primitive.
La méthode en question :
public function getConstraintPoint( graphicElement : GraphicElement ) : Point
{
var px : Number = 0;
var py : Number = 0;
var xOffset : Number;
var yOffset : Number;
if (graphicElement.displayObjectSharingMode ==
DisplayObjectSharingMode.OWNS_UNSHARED_OBJECT) {
xOffset = 0;
yOffset = 0;
}
else {
xOffset = graphicElement.postLayoutTransformOffsets ?
graphicElement.x + graphicElement.postLayoutTransformOffsets.x : graphicElement.x;
yOffset = graphicElement.postLayoutTransformOffsets ?
graphicElement.y + graphicElement.postLayoutTransformOffsets.y : graphicElement.y;
}
// Calcutates y position
if( !isNaN( top ) ) {
py = yOffset + top;
}
else if( !isNaN( bottom ) ) {
py = yOffset + graphicElement.height - bottom;
}
else if( !isNaN( verticalCenter ) ) {
py = ( yOffset + graphicElement.height ) / 2 + verticalCenter;
}
else {
py = yOffset + y;
}
// Calcutates x position
if( !isNaN( left ) ) {
px = xOffset + left;
}
else if( !isNaN( right ) ) {
px = xOffset + graphicElement.width - right;
}
else if( !isNaN( horizontalCenter ) ) {
px = ( xOffset + graphicElement.width ) / 2 + horizontalCenter;
}
else {
px = xOffset + x;
}
return new Point( px, py );
}
La classe complète ce trouve derrière ce lien.
Intéressons nous maintenant à la primitive.
Primitive PointsPath
J’utilise toujours la méthode de dessin drawRoundPath, contenu dans ma classe static UigfxUtils.
Ici, la classe étend FilledElement. Une propriété points, définit les différents points de type ConstraintPoint de la primitive. J’utilise ici un vecteur comme conteneur des points : Vector.<ConstraintPoint>. Le “set” de cette propriété appelle invalidateDisplayList(), afin de lancer le processus de dessin de la forme.
La méthode draw parcour le vecteur de points ConstraintPoint, appelle pour chacun sa méthode getConstraintPoint, et pousse le point dans un vecteur. C’est ce vecteur qui est utilisé par drawRoundPath.
Le code de cette mécanique :
public function get points():Vector.<ConstraintPoint>
{
return _points;
}
/**
* @private
*/
public function set points(value:Vector.<ConstraintPoint>):void
{
_points = value;
invalidateDisplayList();
}
/**
* Draw the path
* <p>Drawing method is static, in UiGfxUtils</p>
* @see com.lafabrick.uigfx.utils.UiGfxUtils
*/
override protected function draw(g:Graphics) : void
{
// replace points from local to global position
var vectorPoint : Vector.<Point> = new Vector.<Point>();
for each( var point : ConstraintPoint in points ) {
vectorPoint.push( point.getConstraintPoint( this ) );
}
// launch drawing method
UigfxUtils.drawRoundPath( g, vectorPoint, radius, true );
}
La classe PointsPath en entier
Utilisation
Reprenons l’exemple, avec cette primitive et les contraintes de placement :
<primitives:PointsPath
radius="60" width="100%" height="100%"
horizontalCenter="0" verticalCenter="0">
<primitives:points>
<geom:ConstraintPoint top="0" horizontalCenter="0" />
<geom:ConstraintPoint horizontalCenter="25" verticalCenter="-25"/>
<geom:ConstraintPoint right="0" verticalCenter="-25" />
<geom:ConstraintPoint horizontalCenter="40" verticalCenter="25" />
<geom:ConstraintPoint right="40" bottom="0" />
<geom:ConstraintPoint horizontalCenter="0" verticalCenter="40" />
<geom:ConstraintPoint left="40" bottom="0" />
<geom:ConstraintPoint horizontalCenter="-40" verticalCenter="25" />
<geom:ConstraintPoint left="0" verticalCenter="-25" />
<geom:ConstraintPoint horizontalCenter="-25" verticalCenter="-25" />
</primitives:points>
<primitives:fill>
<s:SolidColor color="#222222" />
</primitives:fill>
<primitives:filters>
<s:DropShadowFilter color="#000000" inner="true" distance="1"
blurX="4" blurY="4"
quality="3" alpha="0.8" />
</primitives:filters>
</primitives:PointsPath>
![]() |
![]() |
La forme respecte les contraintes de placement, en fonction de l’évolution de la taille de la primitive. Amazing ! ![]()
L’exemple complet (les sources au clic droit), ce cache sous ce lien.
Librairie Uigfx
Tout ce que je viens d’exposer fait partie d’une librairie nommée par Erick « uigfx ».
Cette petite librairie, sera bientôt disponible publiquement sur le svn lafabrick. Vous pouvez d’ores et déjà télécharger le projet (format .fxpl), dans la version proposée dans cette article. La documentation ( en anglais… pas parfait ! ) est disponible là.
[UPDATE]
Le projet uigfx a maintenant sa page sur google code. Pour plus d’information, la présentation de ce projet.
[/UPDATE]
Je vous laisse la place sur le fil de commentaire !
Tags: FlashBuilder, flex4, skinning, Tutoriels, UI Design





Français
--> 4 janvier 2010 ( 8:16 )
[...] Photoshop: Working with Spot Color Channels | ROCKY MOUNTAIN TRAINING blog 2 Likes La Fabrick interactive» Archive du blog » Flex4 : skin et primitives La Fabrick Interactive [blog], , laboratoire d'expérimentations interactives : elearning, [...]
--> 14 juillet 2010 ( 10:50 )
Bon ben plus qu’à avoir un outil de transformation de fichiers SVG créés avec inkscape en fichier FXG et Flex sera encore plus propre du w3c
Merci pour la présentation
Flexment votre