Apache Adobe Flex TutorialTutoriaux Adobe Flex & AIR en Français

24mar/099

Flex Item Renderer – Modifier la taille d'un item avec des Transition

Dans l'article précédent, j'ai montré comment utiliser des States dans un itemRenderer pour modifier son apparence. Comme pour tous les composants qui héritent UIComponent, vous pouvez utiliser des Transition entre vos State. On va ici s'intéresser à la manière par laquelle on pourrait avoir des item qui s'étendent (dont la hauteur est modifiée).

La question de l'expansion d'un item devient intéressante quand on sait que la liste doit être scrollée. Imaginez cette situation: vous avez une liste d'items ayant la même hauteur. Maintenant, vous étendez l'item 2. Jusque là tout va bien, l'item 2 est plus grand que les autres items visibles. Et c'est là le problème: les items visibles. Maintenant vous faîtes un scroll dans la liste. Souvenez-vous que les itemRenderers sont recyclés. Donc quand l'item 2 est hors de vue, son itemRenderer sera bougé en bas de la liste. Vous devez donc mettre à zéro sa taille. OK, jusque là cela fonctionne. Maintenant faire un scroll vers le haut pour ré-afficher l'item 2. Vous voudriez qu'il soit dans l'état étendu. Comme l'itemRenderer pourrait-il le savoir? Si vous avez lu les articles précédents, vous savez que cette information vient soit de la data, soit d'une source externe.

Créer un itemRenderer resizable pour cela est assez complexe et pas forcement utile. Il y a une meilleure manière de faire cela en utilisant une VBox et un Repeater. Seulement, le problème avec le Repeater est que chaque child sera crée. Si vous avez 1000 enregistrement et que vous utilisez un Repeater, vous aurez 1000 instances de votre itemRenderer.

Pour cet exemple, on va écrire un itemRenderer, que l'on va utiliser en tant qu'enfant d'une VBox. Les éléments de cette liste sont simples: le nom et l'auteur du livre. Mais cliquer sur l'itemRenderer va le faire s'étendre. Ceci se fait en 2 étapes:

  • L'itemRenderer a une état qui inclut les informations supplémentaires
  • L'itemRenderer utiliser une transition Resize pour donner une fluidité dans la contraction/expand de l'itemRenderer


L'état de base de l'itemRenderer est plutôt simple:

<mx:HBox width="100%">
	<mx:Label text="{data.author}" fontWeight="bold"/>
	<mx:Text  text="{data.title}" width="100%" fontSize="12" selectable="false"/>
</mx:HBox>

L'état étendu ("ExpandedState") ajouter des éléments qui vont agrandir l'itemRenderer.

Attention, pour une utilisation dans un composant mx:List, n'oubliez pas de fixer la propriété  variableRowHeight à true

<mx:states>
	<mx:State name="ExpandedState">
		  <mx:AddChild position="lastChild">
				<mx:HBox width="100%">
					  <mx:Image source="{data.image}"/>
					  <mx:Spacer width="100%"/>
					  <mx:Label text="{data.price}"/>
					  <mx:Button label="Buy"/>
				</mx:HBox>
		  </mx:AddChild>
	</mx:State>
</mx:states>

Pour changer la taille de manière fluide, on ajoute simplement une transition:

<mx:transitions>
	<mx:Transition fromState="*" toState="*">
		  <mx:Resize target="{this}" />
	</mx:Transition>
</mx:transitions>

Les tags mx:states et mx:transitions doivent être placés à la racine de votre itemRenderer.

Les transitions sont appliquées quelque soit le basculement de state, car les propriétés fromState et toState sont des jokers (*). Maintenant, il ne vous reste plus qu'a prendre en charge le clic sur l'itemRenderer (en ajouter un évènement click sur le tag root) pour changer l'état:

private function expandItem() : void{
	if( currentState == "ExpandedState" ){
		currentState = "";
	}else{
		currentState = "ExpandedState";
	}
}

Le résultat (catastrophique)

Flex Source Code Download: Télécharger le code source complet de l'application

This movie requires Flash Player 11

Testez par vous mêmes, ouvrez plusieurs items et faites un scroll. Les items vont se mélanger, certains items vont se fermer/ouvrir magiquement. Bref, pas du tout le comportement escompté.

Alors comment faire ?

Vous devez vous en douter, d'autres se sont posé la question et sont arrivés à leur fin. Parmi eux, en voici un qui fonctionne bien, crée par FlashGuru. Pour cela, ils stockent l'état dans un objet au niveau du composant list et si fient ensuite à l'internal_uid (identifiant unique) pour savoir si l'item est étendu ou pas au niveau du set data(). C'est un peu feinté mais terriblement bien joué. Voici l'itemRenderer en question:

<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas
    xmlns:mx="http://www.adobe.com/2006/mxml"
    width="100%"
    height="{ (this.owner as EList).rowHeight }"
    doubleClickEnabled="true"
    doubleClick="this.onDoubleClick()"
    horizontalScrollPolicy="off"
    verticalScrollPolicy="off"
    mouseChildren="false">

    <mx:Resize id="expand" target="{this}" heightTo="80" duration="150"/>
    <mx:Resize id="contract" target="{this}" duration="150"/>

    <mx:Script>
        <![CDATA[
            import mx.utils.ObjectUtil;
            import mx.controls.Alert;

            private var rendererList:Object;

            private function onDoubleClick( e:MouseEvent = null):void{

                if (rendererList.expanded){
                    contract.heightTo = (this.owner as EList).rowHeight;
                    contract.end();
                    contract.play();
                }else{
                    expand.end();
                    expand.play();
                }
                rendererList.expanded = !rendererList.expanded;
            }

            [Bindable]
            override public function get data():Object{
                return super.data;
            }

            override public function set data( value:Object ):void{
                if (value){
                    super.data = value;

                    var list:EList = this.owner as EList;

                    if(list.rendererData[super.data.mx_internal_uid] == null){
                        rendererList = new Object();
                        rendererList.expand = false;
                        list.rendererData[super.data.mx_internal_uid] = rendererList;
                    }else{
                        rendererList = list.rendererData[super.data.mx_internal_uid];
                    }
                    this.height = rendererList.expanded?expand.heightTo:(this.owner as EList).rowHeight;
                }

            }

        ]]>
    </mx:Script>

    <mx:VBox
        height="100%"
        width="100%"
        horizontalAlign="left"
        verticalAlign="top">

        <mx:HBox>
            <mx:Image source="{data.thumb}" width="27" height="27"/>
            <mx:Canvas verticalScrollPolicy="off">
                <mx:Text text="{data.username}" selectable="false" fontSize="13" fontWeight="bold" textAlign="left" x="0" y="0"/>
                <mx:Text text="{data.location}" selectable="false" fontSize="11" textAlign="left" x="0" y="18"/>
            </mx:Canvas>
            <mx:Spacer width="100%"/>
        </mx:HBox>

        <mx:Text text="{data.profile}" width="100%" selectable="false"/>
    </mx:VBox>

</mx:Canvas>

Et voila l'application exemple:

Flex Source Code Download: Télécharger le code source complet de l'application

This movie requires Flash Player 11

Articles similaires

Commentaires (9) Trackbacks (0)
  1. Bonjour et merci pour ce tutoriel !

    J'avoue ne pas avoir tout compris (je suis encore un novice parmis tant d'autre ^^), cependant j'ai réussi a implémenter cette technique sans soucis, seul petit truc j'aurais voulu faire un contract de tous les éléments de ma list lorsque je clique sur un élément déjà contracté je ne suis peut-être pas très clair, voici un petit exemple :

    lorsque j'ouvre mon appli chaque article est contracté de base, lorsque je clique sur un article il s'étend, si je clique sur un autre article celui d'avant reste étendu et celui sur lequel j'ai cliqué s'étend a son tour par conséquent je perd en gain de place sur ma page, j'aurais donc voulu savoir si j'avais la possibilité de contracté tous mes articles lorsque je clique sur un article en particulier pour pouvoir ensuite étendre celui-ci.

    Encore merci pour ton tuto, et merci d'avance pour vos réponses !

  2. Salut,
    A mon avis, il faut que tu essaie de réinitialiser la variable "expanded" de l'objet "rendererList" qui est crée dynamiquement oar les renderers.
    genre si ton dataProvider s'appelle "dp", un petit:
    Tu peux boucler sur les items de ton dataProvider et récupérer l'itemRenderer (avec la méthode indexToItemRenderer:
    http://livedocs.adobe.com/flex/201/langref/mx/controls/listClasses/ListBase.html#indexToItemRenderer%28%29
    for each (var o:Object in dp){
    // récupérer le renderer
    if (renderer.rendererList =! null){
    renderer.rendererList.expanded = false;
    }
    }
    ou alors essayer d'accéder à l'effet contract et de faire un end() puis un play() dessus.

    Voilà, bonne chance
    Fabien

  3. Coucou Fabien,

    Merci de t'être donné la peine de répondre !
    Désolé de ne pas avoir pu te répondre plutôt

    Merci pour le conseil !!!

  4. Hello c'est encore moi, bon j'avoue que je coince toujours autant...
    Voici un peu de code :

    Actionscript:
    1. private function onClick( e:MouseEvent = null):void
    2.  {
    3.    var i:int = 0;          
    4.    
    5.    for each (var o:Object in this.parentDocument.list.dataProvider)
    6.    {
    7.       trace(i++);         
    8.       if (rendererList.expanded)
    9.       {
    10.         contract.heightTo = (this.owner as EList).rowHeight;
    11.         contract.end();
    12.         contract.play();
    13.       }
    14.       else
    15.       {
    16.         expand.end();
    17.         expand.play();
    18.       }
    19.       rendererList.expanded = !rendererList.expanded;              
    20.    }
    21. }

    Donc cette fonction est situé dans mon composant ItemRenderer, j'obtiens bien le nombre d'éléments que je devrais avoir seul problème la méthode indexToItemRenderer() n'a pas l'air d'être utilisable (l'autocompletion ne la trouve pas) ou alors je m'y prend mal.... ce qui est fort possible ^^'

    Il faudrait que je fasse un truc dans ce genre : rendererList = indexToItemRenderer(i) ?

    ou peut-etre tout simplement un : indexToItemRenderer(i) ?

    Enfin quoi qu'il en soit je n'ai pas accés a cette methode flex me dit qu'elle ne semble pas definis (je suis sous flex 3).

    Merci d'avance !!!

  5. La méthode indexToItemRenderer se trouve sur la classe ListBase donc toutes les classes qui héritent de ListBase ont cette méthode, comme List par exemple

    Fabien

  6. Merci pour ta réponse finalement j'ai fait autrement je ne m'en sortais pas!

  7. Bonjour,

    tout d'abord merci pour tous ces tutoriels, je débute en Flex et je trouve souvent les réponses à mes questions dans vos pages.

    J'ai le même problème que décrit ci-dessus mais sur un DataGrid dont une des colonne est gérée par un ItemRenderer. A chaque fois que je fais un scroll, les images de ma colonne sont modifiées/disparaissent.... Bref, cela fait n'importe quoi !

    J'ai des soucis pour transposer votre exemple sur le mien.

    J'ai récupéré la fonction "override set data"
    mais je ne sais pas quoi faire de cette ligne "var list:EList = this.owner as EList;" dois je la laisser telle quelle ?

    De même pour cette ligne "list.rendererData[super.data.mx_internal_uid] = rendererList;
    "

    J'ai fais plusieurs essai mais aucun m'a permis de régler mon problème.

    Merci d'avance pour votre aide !

    Cordialement,

  8. Salut Thibault,
    j'aimerai bien jeter un coup d'oeil à ton itemRenderer si possible. Pour les images, tu peux utiliser un composant qui gère le cache, comme cela, Flex ne va pas retélécharger les images à chaque apparition d'un item:
    http://thanksmister.com/index.php/archive/flex-imagecache-a-cheap-way-to-cache-images/
    http://www.quietlyscheming.com/blog/2007/01/23/some-thoughts-on-doubt-on-flex-as-the-best-option-orhow-i-made-my-flex-images-stop-dancing/
    Cela devrait te permettre un meilleur fonctionnement. Si tu postes du code dans le commentaire, n'oublie pas les tags (voir en bas de page) et si ca ne marche pas, envoie le moi par mail (voir en bas de page aussi)

    Fabien

  9. Bonjour et merci pour la réponse.

    Voilà mon itemRenderer tel qu'il ai sans tenir compte de ce tuto :

    MXML:
    1. lastAsk) {
    2.                         lastAsk = state.state_id;
    3.                     }
    4.                 }
    5.                
    6.                 switch (lastAsk) {
    7.                     case 35 :
    8.                         nextState = 40;
    9.                         nextStateString = "BD reçue";
    10.                         this._addImage(String(nextState), nextStateString);
    11.                         break;
    12.                     case 60 :
    13.                         nextState = 70;
    14.                         nextStateString = "Chronique mise en forme";
    15.                         this._addImage(String(nextState), nextStateString);
    16.                         break;
    17.                 }
    18.             }
    19.            
    20.             private function _addImage(numeroImage:String, nameNextStep:String):void {
    21.                 var imageToAdd:Image = new Image;
    22.                 imageToAdd.source = "img/etape_"+numeroImage+".png";
    23.                 imageToAdd.height = 18;
    24.                 imageToAdd.toolTip = nameNextStep;
    25.                 imageToAdd.addEventListener(MouseEvent.CLICK, this.validateNextStep);
    26.                 imagesBox.addChild(imageToAdd);
    27.             }
    28.            
    29.             // Réception des informations du serveur suite
    30.             // à l'envoi de l'ensemble des données du formulaire
    31.             private function resultHandler(event:Event):void {
    32.                
    33.                 // TODO: Généraliser l'objet d'envoi du Json au serveur
    34.                 // Variable de l'objet recu du serveur
    35.                 var messageResult:Object;
    36.                 messageResult = JSON.decode(String(service.lastResult));
    37.                
    38.                 // Vérification du retour du serveur et traitement de l'info
    39.                 if (! messageResult.errors) {
    40.                     infoServerValidated();
    41.                     this.parent.parent.parent.parent.dispatchEvent(new Event(Event.CHANGE));
    42.                     Alert.show("Données mises à jour.");
    43.                 } else {
    44.                     Alert.show("Erreur, vos données n'ont pas pu être mises à jour.\nVeuillez contacter l'administrateur si l'erreur se reproduie");
    45.                 }
    46.             }
    47.            
    48.             // Modification du curseur           
    49.             private function setCursor(cursorType:String):void {
    50.                 if (cursorType == "normal") {
    51.                     CursorManager.removeAllCursors();
    52.                 } else if (cursorType == "busy") {
    53.                     CursorManager.removeAllCursors();
    54.                     CursorManager.setBusyCursor();
    55.                 }
    56.             }
    57.            
    58.             // Une fois les données envoyées, réveil du formulaire
    59.             private function infoServerValidated():void {
    60.                 setCursor("normal");
    61.             }
    62.            
    63.             private function validateNextStep(e:Event):void {
    64.                 var textAlert:String;
    65.                
    66.                 textAlert = "BD : " + data.title + "\n\n";
    67.                 textAlert += "Etes vous sûr de vouloir passer cette chronique"
    68.                 textAlert += "à l'étape : " + nextStateString;
    69.                
    70.                 Alert.yesLabel = "Oui";
    71.                 Alert.noLabel = "Non";
    72.                
    73.                 Alert.show(
    74.                     textAlert,
    75.                     "Validation d'étape",
    76.                     (Alert.YES | Alert.NO),
    77.                     Application.application.panel,
    78.                     nextStep);
    79.             }
    80.            
    81.             private function nextStep(event:CloseEvent):void {
    82.                 if (event.detail == Alert.YES) {
    83.                     service.request["1[state]"] = nextState;
    84.                     service.request["1[ask]"] = data.states[0].ask_id;
    85.                     service.send();
    86.                 }
    87.             }
    88.         ]]&gt;
    89.    
    90.    
    91.     <!-- Création d'un service pour l'envoi du formulaire -->

    Le principe est que je récupère un état d'un catalogue et en fonction de cet état j'affiche des images pour chaque état qu'il a traversé....

    Mon gros problème est que si je scroll sur mon DataGrid, j'ai toutes mes images qui sont affichés bizarement.

    Si le code est trop compliqué, je veux bien essayer de le présenter plus simplement.

    Je suis aussi preneur de toutes les remarques sur la façon de l'écrire.

    Merci d'avance,


Leave a comment

(required)

Aucun trackbacks pour l'instant