AIR Mobile – Skinner un TextInput de façon programmatique
Tutorial Flex écrit par Arnaud Thorel (@athorel)
Publiez vous aussi sur flex-tutorial!
Depuis la version 4.0 de Flex, nous pouvons développer des skins en utilisant la nouvelle structure de composants spark.
Spark propose un système basé sur des composants logiques et des représentations graphiques que l’on appelle Skin. Dans un premier temps la plupart des skins étaient des représentations MXML où venaient se superposer des groupes de primitives de dessins (border, background, shadow, …), mais avec l’avènement du développement mobile, les enjeux de rapidité d’exécutions et de poids des fichiers rentrent en jeu.
C’est ainsi qu’Adobe a mis en place un ensemble de skins pour mobile dont la chaîne d’héritage est nettement moins longue que pour les skins sparks de base.
Dans ce tutorial nous allons voir comment intégrer un titre pour un composant TextInput (comme sur iOS), pour obtenir le rendu suivant, cet exemple pourra servir de base afin d’ajouter d’autres informations dans notre composant (Icon, Aide).
Comprendre l’affichage actuel d’un composant TextInput
Le composant TextInput possède 4 états :
- Enable : Cet état est l’état éditable d’un TextInput
- Disable : Cet état est l’état non éditable d’un TextInput
- EnableWithPrompt : Cet état est l’état éditable d’un TextInput avec une valeur vide
- DisableWithPrompt: Cet état est l’état non éditable d’un TextInput avec une valeur vide
La première étape consiste à hériter de la classe spark.skins.mobile.TextInputSkin pour obtenir une notre classe PromptTextInputSkin, et ensuite analyser le comportement de TextInpuitSkin.
Tout de suite on se rend compte du coté manichéen du composant TextInput, la valeur vient effacer le prompt, ce qui se traduit dans le code de cette manière.
override protected function commitCurrentState():void
{
super.commitCurrentState();
alpha = currentState.indexOf("disabled") == -1 ? 1 : 0.5;
var showPrompt:Boolean = currentState.indexOf("WithPrompt") >= 0;
if (showPrompt && !promptDisplay)
{
promptDisplay = createPromptDisplay();
addChild(promptDisplay);
}
else if (!showPrompt && promptDisplay)
{
removeChild(promptDisplay);
promptDisplay = null;
}
invalidateDisplayList();
}
Lors d’un changement d’état la fonction commitCurrentState on retrouve les éléments suivants :
- On vérifie si nous sommes dans un état disabled, pour modifier l’alpha du composant.
- On vérifie si nous sommes dans un état WithPrompt, si c’est le cas et que le prompt n’est pas déjà affiché, il est créé et ajouté, sinon il est retiré si il était déjà présent.
- On conclut cette fonction par l’appel à invalidateDisplayList qui va permettre l’appel à updateDisplayList qui redessinera le composant
Redéfinir le comportement en fonction de l'état courant
Dans cette méthode on vient d’identifier la partie qui va nous poser problème, la première étape consiste donc à redéfinir cette méthode dans notre classe.
override protected function commitCurrentState():void
{
alpha = currentState.indexOf("disabled") == -1 ? 1 : 0.5;
invalidateDisplayList();
}
Nous avons ici de façon volontaire supprimé l’appel à la fonction super afin de ne pas avoir à exécuter le code qui ajoute ou supprime le promptDisplay. Il est généralement dangereux de faire ça car on coupe tout lien avec le code porté par l’héritage, mais dans notre cas on risque rien, car en remontant la chaîne d’héritage on se rend compte que la fonction commitCurrentState que nous venons de redéfinir est la première redéfinition d’une fonction vide.
Si on lance notre code maintenant nous n’obtenons pas encore le résultat voulu
Effectivement, nous avons retiré l’ajout du promptDisplay, nous allons donc gérer cette partie dans le constructeur de notre composant.
public class PromptTextInputSkin extends TextInputSkin
{
public function PromptTextInputSkin(){
super();
promptDisplay = createPromptDisplay();
this.addChild(promptDisplay);
}
Maintenant le prompt est affiché continuellement, mais le texte vient se superposer par-dessus.
Mise en place des différents composants
Afin de remédier à ce problème nous allons redéfinir une fonction indispensable des skins, il s’agit de la fonction layoutContents, cette fonction permet la mise en place des éléments de la skin.
// Define gap beetween prompt and text
[Style(name="gap", type="Number", format="Number", inherit="yes")]
override protected function layoutContents(unscaledWidth:Number, unscaledHeight:Number):void
{
super.layoutContents(unscaledWidth, unscaledHeight);
if(promptDisplay) {
var leftMarginText:int = promptDisplay.x + promptDisplay.textWidth + getStyle('gap');
var rightMarginText:int = getStyle('paddingRight');
var textDisplaySize:int = hostComponent.width - leftMarginText - rightMarginText;
//Replace la partie texte pour ne pas chevaucher le prompt
setElementPosition(textDisplay, leftMarginText , textDisplay.y);
//Redéfini la taille de la partie texte afin de prendre en compte les nouvelles marges
setElementSize(textDisplay, textDisplaySize, textDisplay.height);
}
}
Le gap est un Style ajouté, qui permettra dans notre CSS de définir l’espacement entre le prompt et le texte.
Afin de spécifier la marge qui doit être laissée à gauche du texte, nous calculons les éléments suivants : position x du prompt + taille du texte + le gap.
On récupère aussi la marge à laisser côté droit du texte, cela nous permettra de définir la nouvelle taille du texte.
La taille du texte est donc la taille du composant TextInput auquel on soustrait les marges de droites et de gauche.
On fait ensuite appel aux fonctions setElementPosition et setElementSize afin que le texte ne déborde pas du composant et ne sois plus superposé avec le prompt.
On se rapproche du résultat final, on remarque quand même quelques détails de style manquant :
- Le prompt n’a pas le format voulu (gras, noir)
- Le texte n’a pas l’alignement voulu.
Finir avec style
Pour remédier à ce problème nous utilisons donc la dernière méthode qui nous permet de jouer sur les propriétés du composant, il s’agit de la méthode commitProperties.
// Define color of the prompt text
[Style(name="promptColor", type="uint", format="Color", inherit="no")]
// Define font Weigth of the prompt text
[Style(name="promptFontWeight", type="String", format="String", inherit="no")]
override protected function commitProperties():void
{
super.commitProperties();
promptDisplay.setStyle('color', getStyle('promptColor'));
promptDisplay.setStyle('fontWeight', getStyle('promptFontWeight'));
promptDisplay.setStyle('textAlign', 'left');
}
On ajoute ici deux style pour la mise en forme du prompt, on force par défaut le promptDisplay à gauche sinon il est dépendant de la mise en forme textAlign et dans cet exemple nous n’avons pas prévu un affichage à droite du prompt.
On termine donc par une petite touche de CSS et le tour est joué
skins|PromptTextInputSkin {
gap : 10px;
promptColor : black;
promptFontWeight : bold;
textAlign : right;
}
Le résultat correspond à nos attentes, voici une base de travail pour ajouter une image comme dans cet exemple développé par nos amis de people in action inclus dans la lib e-skimo.
Flex 4 – Le lien "Données" entre Skin et Composant
Grâce à l’utilisation des States, notre composant a un nouvel aspect graphique. Cependant, le label du bouton est toujours « Bouton ». En effet, celui-ci est codé en dur dans notre fichier MyButtonSkin. Le label que l’on va donner au bouton n’est pas une propriété de la Skin, mais une propriété du composant Button. Pourtant, notre composant Label se trouve dans le Skin, et c’est lui qui doit recevoir ce texte.
C’est là qu’intervient le contrat de type "Données" entre le composant Button et la Skin. La Skin va ainsi indiquer sur quel composant hôte elle peut se greffer. Cela se fait par l’ajout d’une méta-information sur le composant : [HostComponent("chemin vers la classe concernée")].
Depuis la Skin, on pourra alors accéder à ce composant hôte par la propriété « hostComponent ». Pour lier la propriété « label » du composant Button et la propriété « text » du composant Label de la Skin, on va faire un simple DataBinding. Voici les éléments à rajouter dans MyButtonSkin.mxml :
<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark" alpha.disabled=".5">
<fx:Metadata>
[HostComponent("spark.components.Button")]
</fx:Metadata>
...
<s:Label text="{hostComponent.label}" color="0x131313"
textAlign="center"
verticalAlign="middle"
horizontalCenter="0" verticalCenter="1"
left="12" right="12" top="6" bottom="6"
/>
</s:Skin>
Vous pouvez maintenant préciser n’importe quel label, en donnant simplement une valeur à la propriété « label » de votre Button:

Votre composant se comporte maintenant comme vous le souhaitiez, exactement comme le Button Spark de base. Le composant Button de base est d’ailleurs fait de la même manière, avec de base, la classe spark.skins.spark.ButtonSkin.
Vous pouvez si le souhaitez aller voir comment est crée la Skin de base des Button Spark. Pour cela, il vous suffit d’ouvrir la classe ButtonSkin dans Flash Builder. Dans le menu de Flash Builder 4, allez dans Navigate > Open Type… puis tapez « ButtonSkin » dans le champ de recherche.
Flex 4 – Utilisation de States dans une Skin
Tout d’abord, on va voir l’aspect State. Un composant pouvant être dans plusieurs états différents, il est normal que la Skin suive, et propose plusieurs états. Tout composant héritant de SkinnableComponent a au moins un état, celui de base.
Un composant peut définir des états supplémentaires à l’aide de la balise MetaData [SkinStates]. Voici par exemple les States déclarés dans la classe Button :
- [SkinState("up")]
- [SkinState("over")]
- [SkinState("down")]
- [SkinState("disabled")]
Pour un bouton, on a quatre States : up, over, down, disabled. On va modifier notre fichier Skin pour avoir un rendu différent dans chaque State. Comme on l’a vu plus haut, on déclare dans une balise « s:states », la liste des States utilisés par le composant :
<s:states> <s:State name="up" /> <s:State name="over" /> <s:State name="down" /> <s:State name="disabled" /> </s:states>
Grâce à cette déclaration, la Skin va respecter sa partie du contrat Skin-Composant. Vous pouvez utiliser la nouvelle notation Flex 4 « pointée ». Ainsi, alpha.disabled=".5" indique que l’opacité sera de 0.5 dans l’état nommé « disabled ». Pour donner un effet graphique plus agréable, on va aussi rajouter un léger dégradé.
Voici donc la Skin modifiée pour pouvoir avoir un aspect différent suivant la Skin :
<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx" alpha.disabled=".5">
<s:states>
<s:State name="up" />
<s:State name="over" />
<s:State name="down" />
<s:State name="disabled" />
</s:states>
<s:Rect radiusX="4" radiusY="4" top="0" right="0" bottom="0"
left="0" includeIn="down">
<s:fill>
<s:SolidColor color="0"/>
</s:fill>
<s:filters>
<s:DropShadowFilter knockout="true" blurX="5" blurY="5"
alpha="0.32" distance="2" />
</s:filters>
</s:Rect>
<s:Rect id="rect" radiusX="4" radiusY="4" top="0"
right="0" bottom="0" left="0">
<s:fill>
<s:SolidColor color="0x0099FF" color.over="0x0066FF"
color.down="0x0000CC"/>
</s:fill>
<s:stroke>
<s:SolidColorStroke color="0x222222" weight="2"/>
</s:stroke>
</s:Rect>
<s:Rect radiusX="4" radiusY="4" top="2" right="2" left="2"
height="50%">
<s:fill>
<s:LinearGradient rotation="90">
<s:GradientEntry color="0xFFFFFF" alpha=".5"/>
<s:GradientEntry color="0xFFFFFF" alpha=".1"/>
</s:LinearGradient>
</s:fill>
</s:Rect>
<s:Label text="Bouton" color="0x222222"
textAlign="center" verticalAlign="middle"
horizontalCenter="0" verticalCenter="1"
left="12" right="12" top="6" bottom="6"
/>
</s:Skin>
Et voici le résultat, dans les différents états :

<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx" alpha.disabled=".5">
<s:states>
<s:State name="up" />
<s:State name="over" />
<s:State name="down" />
<s:State name="disabled" />
</s:states>
<s:Rect radiusX="4" radiusY="4" top="0" right="0" bottom="0"
left="0" includeIn="down">
<s:fill>
<s:SolidColor color="0"/>
</s:fill>
<s:filters>
<s:DropShadowFilter knockout="true" blurX="5" blurY="5"
alpha="0.32" distance="2" />
</s:filters>
</s:Rect>
<s:Rect id="rect" radiusX="4" radiusY="4" top="0"
right="0" bottom="0" left="0">
<s:fill>
<s:SolidColor color="0x0099FF" color.over="0x0066FF"
color.down="0x0000CC"/>
</s:fill>
<s:stroke>
<s:SolidColorStroke color="0×222222" weight="2"/>
</s:stroke>
</s:Rect>
<s:Rect radiusX="4" radiusY="4" top="2" right="2" left="2"
height="50%">
<s:fill>
<s:LinearGradient rotation="90">
<s:GradientEntry color="0xFFFFFF" alpha=".5"/>
<s:GradientEntry color="0xFFFFFF" alpha=".1"/>
</s:LinearGradient>
</s:fill>
</s:Rect>
<s:Label text="Bouton" color="0×222222"
textAlign="center" verticalAlign="middle"
horizontalCenter="0" verticalCenter="1"
left="12" right="12" top="6" bottom="6"
/>
</s:Skin>
Et voici le résultat, dans les différents états :















