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

3juin/110

AIR Mobile – Reproduire l'effet "Pull down to refresh" sur une liste Flex

J'aime bien les "challenges". Ça me fait parfois passer une demi-journée sur un commentaire farfelu posté sur flex-tutorial mais quand j'y arrive, je suis fier comme un gardon. Cette fois, ce n'était pas un commentaire mais un article de notre ami Michaël Chaize paru sur son blog riagora:

Pull Down To Refresh sur riagora.com

Le pull down to refresh, c'est le système que l'on a sur les applications type Twitter ou Facebook sur lesquelles vous avez un flux d'informations continues. Si vous "tirez" la liste vers le bas, on va vous indiquer que si vous "tirez" encore, cela va déclencher un refresh du flux. Cela permet de remplacer le classique bouton "refresh" de manière élégante.

Voici le rendu sur une musique de Nine Inch Nails:

Vous pouvez trouver les sources de cette application sur le blog de Michaël au format FXP à importer dans Flash Builder.

Il est pas bien cet exemple?

L'exemple est cool mais quand on le teste et que l'on regarde le code de plus près, il y a quelques détails qui me chagrinent:

  • Lorsque l'on fait plusieurs pull successifs (y compris quand on est en état chargement), le rendu devient assez anarchique
  • Le comportement est basé sur des évènements souris écoutés en permanence, pas terrible pour les perfs
  • Il y a une "feinte du loup blanc" dans l'itemRenderer des tweets, dans lequel on rajoute du contenu à la volée pour indiquer le chargement
  • Un élément de donnée est ajouté en tête de liste qui sera ensuite rendu de manière spécifique par l'itemRenderer
  • A cause de cette feinte, Michaël a été obligé de supprimer la virtualisation des renderers, ceux-ci n'étaient donc pas réutilisés (pas terrible pour les performances encore une fois).

Voilà, cela ne parait pas être grand chose mais cela peut vous mettre dedans au moment de l'intégration dans votre application, surtout l'ajout dans la data et dans l'itemRenderer. Donc j'ai pris mon Flash Builder et en 2 heures, j'ai remanié cet exemple pour résoudre ces problèmes.

Je vais pas faire comme si je l'avais fait en claquant des doigts, l'étape la plus longue ayant été d'aller chercher quels composants propageaient quels évènements et de chercher ceux qui étaient les plus adéquats à mon utilisation. Une petite chasse dans les classes de Flex toujours appréciable :) .

Ma version du pull down to refresh

Ici, on ne va pas ajouter d'élément dans la donnée, ni utiliser un itemRenderer spécial. On laisse la liste tranquille et on va tricoter autour. Pour être plus cohérent avec Flex 4 et pour améliorer la lisibilité du code, j'ai utilisé les States.

Ils sont au nombre de 3:

  • normal
  • pulled
  • loading

On aurait même pu mettre un 4 état car l'état "pulled" contient lui-même 2 états, suivant s'il est assez "tiré" pour demander un refresh ou pas. Tout se passe dans la vue pour plus de simplicité. Si vous voulez réutiliser ce comportement, vous pouvez caler les 4 fonctions liées au pull to refresh et les 3 composants graphiques dans un nouveau composant MXML.

<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:twittersearchadobe="services.twittersearchadobe.*"
        title="Tweets about France" viewActivate="view1_viewActivateHandler(event)"
        currentState="normal" creationComplete="onCComplete(event)"
        backgroundAlpha="0" xmlns:mx="library://ns.adobe.com/flex/mx">
  <fx:Script>
    <![CDATA[
      import mx.events.FlexEvent;
      import mx.events.TouchInteractionEvent;
      import mx.rpc.events.ResultEvent;

      import spark.components.Scroller;
      import spark.events.ViewNavigatorEvent;
      /**
      * Seuil à partir duquel on va demander un chargement
      */
      private static const SCROLL_POSITION_TRIGGER_REFRESH:int = -80;
      /**
      * Mémoire sur la position du scroll pour éviter de faire trop de modifications on enter frames
      */
      private var _previousVScrollValue:Number = Number.NEGATIVE_INFINITY;
      /**
      * Flag pour éviter d'ajouter plusieurs fois les event listener
      */
      private var _enterFrameFlag:Boolean = false;
      /**
      * Position en Y du group pull to refresh pour qu'il suive le mouvement de la liste
      */
      [Bindable]
      private var _loadGroupY:Number = 0;

      /////////////////////////////////////////////////////////////////////////
      // TWITTER SERVICE
      protected function operation1(q:String):void
      {
        Operation1Result.token = twitterSearchAdobe.Operation1(q);
      }

      protected function view1_viewActivateHandler(event:ViewNavigatorEvent):void
      {
        operation1('france');
      }

      protected function twitterSearchAdobe_resultHandler(event:ResultEvent):void
      {
        tweets.removeAll();
        tweets = event.result.results;
        currentState = currentState == "loading" ? "normal" : currentState;
      }

      /////////////////////////////////////////////////////////////////////////
      // PULL TO REFRESH BEHAVIOUR

      protected function onCComplete(event:FlexEvent):void{
        var scroller:Scroller = list.scroller;
        scroller.addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_START, onChangeStart);
      }

      protected function onChangeStart(event:TouchInteractionEvent):void{
        if (_enterFrameFlag){
          return;
        }
        list.addEventListener(MouseEvent.MOUSE_UP, onMouseStop);
        list.addEventListener(MouseEvent.ROLL_OUT, onMouseStop);
        list.addEventListener(Event.ENTER_FRAME, onEnterFrame);
        _enterFrameFlag = true;
      }

      protected function onEnterFrame(event:Event):void{
        var vScroll:Number = list.scroller.verticalScrollBar.value;
        if (_previousVScrollValue == vScroll){
          return;
        }
        _previousVScrollValue = vScroll;
        if(vScroll < -20){
          currentState = "pulled";
          if(vScroll < SCROLL_POSITION_TRIGGER_REFRESH){
            if(arrowImage.rotation == 0)  {
              arrowImage.rotation = 180;
            }
            loadText.text = "Release to refesh...";

          }else{
            if(arrowImage.rotation == 180)  {
              arrowImage.rotation = 0;
            }
            loadText.text = "Pull down to refresh";
          }
          _loadGroupY = -vScroll - loadingGroup.height;
        }else{
          _loadGroupY = 0;
          currentState = "normal";
        }
      }

      protected function onMouseStop(event:MouseEvent):void{
        var vScroll:Number = list.scroller.verticalScrollBar.value;
        if (currentState == "pulled" && vScroll < SCROLL_POSITION_TRIGGER_REFRESH){
          operation1('france');
          currentState = "loading";
          loadGroup.includeInLayout = true;
        } else {
          currentState = "normal";
        }
        _loadGroupY = 0;
        if (_enterFrameFlag){
          list.removeEventListener(MouseEvent.MOUSE_UP, onMouseStop);
          list.removeEventListener(MouseEvent.ROLL_OUT, onMouseStop);
          list.removeEventListener(Event.ENTER_FRAME, onEnterFrame);
          _enterFrameFlag = false;
        }
      }

    ]]>
  </fx:Script>

  <fx:Declarations>
    <s:CallResponder id="Operation1Result"/>
    <twittersearchadobe:TwitterSearchAdobe id="twitterSearchAdobe" result="twitterSearchAdobe_resultHandler(event)"/>
    <s:ArrayCollection id="tweets"/>
    <s:Fade id='fadeIn' target="{loadingGroup}" duration="500" alphaFrom="0" alphaTo="1"/>
  </fx:Declarations>
  <s:states>
    <s:State name="normal" />
    <s:State name="loading" />
    <s:State name="pulled" />
  </s:states>
  <s:transitions>
    <s:Transition fromState="loading" toState="normal">
      <s:Parallel effectEnd="loadGroup.includeInLayout = false;" duration="500">
        <s:Fade id="fadeInLG" target="{loadGroup}" alphaFrom="1" alphaTo="0"/>
        <s:Resize target="{loadGroup}" heightTo="0">
          <s:easer>
            <s:Sine />
          </s:easer>
        </s:Resize>
      </s:Parallel>
    </s:Transition>
  </s:transitions>
    <s:VGroup width="100%" height="100%">
      <s:HGroup id="loadGroup" width="100%" height="50"
                visible.loading="true" visible.normal="false" visible.pulled="false"
                includeInLayout="false"
               verticalAlign="middle" horizontalAlign="center">
          <s:BusyIndicator width="30" height="30"/>
          <s:Label id="loadanim" color="0x444444" text="Loading new tweets..." />
      </s:HGroup>
      <s:List id="list" dataProvider="{tweets}"
              width="100%" height="100%"
              >
        <s:itemRenderer>
          <fx:Component>
            <s:IconItemRenderer labelField="text" messageField="from_user" iconField="profile_image_url" iconWidth="48" iconHeight="48" >
            </s:IconItemRenderer>
          </fx:Component>
        </s:itemRenderer>
      </s:List>
    </s:VGroup>
    <s:HGroup id="loadingGroup" horizontalAlign="center" verticalAlign="middle" width="100%" visible.pulled="true"
              visible.normal="false" visible.loading="false" includeInLayout.pulled="true"
              includeInLayout.normal="false" includeInLayout.loading="false" height="50"
              y="{_loadGroupY}"
              >
      <s:Image id="arrowImage"  source="@Embed('Arrow-double-up-48.png')"/>
      <s:Label text="Pull down to refresh"  id="loadText"/>
    </s:HGroup>
</s:View>

On retrouve les 3 grands composants:

  • loadGroup : Contient le texte "Chargement en cours…"
  • loadingGroup : Contient le texte "Pull to Refresh" ou "Release to Refresh"
  • list : La liste des Tweets

Ici, les évènements sont gérés de manières plus fine. On n'ajoute les event listener que lorsque l'utilisateur a commencé un scroll et on écoute Event.ENTER_FRAME au lieu de MOUSE_MOVE qui se produit de manière plus aléatoire sur mobile (trop souvent). Je n'ai pas vraiment réussi à effectué un parfait suivi en Y avec la liste qui descend, le loadingGroup est toujours un peu en retard mais je n'ai pas réussi à trouver pourquoi!

Pour le fun, j'ai aussi rajouté quelques petits effets visuels, histoire que ce soit moins brutal comme transition.

Voilà, l'effet est sensiblement le même, pas besoin donc que je vous fasse une vidéo ;) .

Sources de l'application au format FXP

Télécharger les sources au format FXP

Remplis sous: ActionScript, Adobe Air, Exemple || Taggé comme: Laisser un commentaire

Articles similaires

Commentaires (0) Trackbacks (0)

Aucun commentaire pour l'instant


Leave a comment

(required)

Aucun trackbacks pour l'instant