Wednesday, June 18, 2008

A Reusable Flex Hover Details Window



Update: The source code for this is now hosted on googlecode.


An increasingly common feature in rich web interfaces is when hovering over some piece of information, a window pops up providing more details about that item. An example of this is in the Gmail interface. If you hover over a name in your chat list, you get a popup which displays more details about the person, such as their name, photo and email address. In this short tutorial, I will go over how to do something similar in Flex in a reusable way.

First we start with our simple demo Application.
<?xml version="1.0" encoding="utf-8"?>
  <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init()">
  <mx:Script>
  <![CDATA[
    import mx.collections.ArrayCollection;
    [Bindable] 
    private var dataProvider:ArrayCollection = 
                  new ArrayCollection( [ { name: "Leonard Nimoy", image: "nimoy.jpg" }, 
                                         { name: "William Shatner", image: "shatner.jpg" }, 
                                         { name: "Deforest Kelley", image: "kelley.jpg" },] );
  ]]>
  </mx:Script>
  <mx:Panel title="Famous Actors: Hover Over For More Details">
    <mx:List dataProvider="{dataProvider}" 
                labelField="name" 
                itemRenderer="PersonRenderer" 
                width="100%"/>
  </mx:Panel>
  </mx:Application>

In this case we are just displaying a set of names in a List component. We create a dataProvider for the list which is a set of persons where each person has a name and image property. We specify the labelField for the List as the name property of a person. Next we create a simple ItemRenderer called PersonRenderer which is used by the List component to display the items.
<mx:Label xmlns:mx="http://www.adobe.com/2006/mxml">
  <mx:Script>
  <![CDATA[
    import mx.binding.utils.BindingUtils;

    override protected function createChildren():void
    {
      super.createChildren();

      //Instance the ImageHover object and attach it to
      //the item renderer
      var imageHover:ImageHover = new ImageHover();
      imageHover.parentComponent = this;

      //Create a binding between the image property of the data object
      //and the source property of the ImageHover so it knows which image to display
      BindingUtils.bindProperty( imageHover, "source", this, ["data","image"] );

      //Create a binding between the name property of the data object
      //and the title property of the ImageHover so it knows which title to display
      BindingUtils.bindProperty( imageHover, "title", this, ["data","name"] );        
    }
  ]]>
  </mx:Script>
  </mx:Label>



This renderer is based on the Label component. Since it implements IDataRenderer we can use it almost 'as is' in the List. We override createChildren() where we attach our ImageHover component to the renderer. This component is what actually displays the details window when we hover over the renderer. There are only three properties we set on this component, the title and the source, which we bind to the name and image properties of our person, and the parentComponent which is the component that we want the hover detail window to appear over. Let's look at the ImageHover component to see how it works:
<HoverComponent xmlns="*" xmlns:mx="http://www.adobe.com/2006/mxml">
  <!-- Simple extension of the HoverComponent to display
    an image specified by the source property with a title -->

  <mx:Script>
  <![CDATA[
    [Bindable] public var source:String;
    [Bindable] public var title:String;
  ]]>
  </mx:Script>

  <mx:Panel paddingBottom="5"
               paddingLeft="5"
               paddingTop="5"
               paddingRight="5"
               backgroundAlpha="1"
               title="{title}">
    <mx:Image id="image" source="{source}"/>
  </mx:Panel>
  </HoverComponent>

This component is very simple and only consists of a Panel with a title and an Image component inside the Panel. The key thing is that it extends HoverComponent, which provides the hover capabilities. You can modify the ImageHover to do just about anything as long as it extends the HoverComponent. So, finally let's look at the HoverComponent class:
package
  {
    import flash.events.MouseEvent;
    import flash.events.TimerEvent;
    import flash.geom.Point;
    import flash.utils.Timer;

    import mx.containers.Canvas;
    import mx.core.UIComponent;
    import mx.managers.PopUpManager;

    public class HoverComponent extends Canvas
    {
      public var delay:Number = 1000;
      private var _parentComponent:UIComponent;

      private var timer:Timer;
      private var popped:Boolean = false;

      public function set parentComponent( parentComponent:UIComponent ):void
      {
        _parentComponent = parentComponent;
        if ( _parentComponent != null )
        {
          var thisComponent:UIComponent = this;

          //Detects mouse overs and starts a timer to display the hover
          parentComponent.addEventListener( MouseEvent.MOUSE_OVER, function( evt:MouseEvent ):void
          {
            //Initialize the timer to trigger one time after delay millis
            timer = new Timer( delay, 1 );

            //Wait for the timer to complete
            timer.addEventListener(TimerEvent.TIMER_COMPLETE, function( tevt:TimerEvent ):void
            {
              //move to a position relative to the mouse cursor
              thisComponent.move( evt.stageX + 20, evt.stageY + 20 );

              //Popup the hover component
              PopUpManager.addPopUp( thisComponent, parentComponent );

              //Set a flag so we know that a popup actually occurred
              popped = true;
            });

            //start the timer
            timer.start();
          });

          parentComponent.addEventListener( MouseEvent.MOUSE_OUT, function( evt:MouseEvent ):void
          {
            //If the timer exists we stop it
            if ( timer )
            {
              timer.stop();
              timer = null;
            }

            //If we popped up, remove the popup
            if ( popped )
            {
              PopUpManager.removePopUp( thisComponent );
              popped = false;
            }
          });
        }
      }

      public function get parentComponent():UIComponent
      {
        return _parentComponent;
      }  
    }
  }

The HoverComponent class extends the Canvas component. It adds a delay property, which is the amount of time in milliseconds that you need to hover over an item before the window displays. It also adds the parentComponent property which is the item that you want the hover to appear over. The default delay is 1 second. When you set the parentComponent, a mouseOver and mouseOut listener get added to the parentComponent. When you mouseOver, a timer gets started. When the timer completes, the hover component gets positioned and uses PopUpManager to display itself. When you mouseOut, the timer will get cancelled if it is not completed, and the hover window is removed, if it was displayed.



So with the HoverComponent class we have a simple and re-usable way to create custom hover detail windows. Just create your window by extending the HoverComponent and attach it to the component you want it to appear over like we did in the PersonRenderer.

Here is a demo and the source code.


Demo:
Unable to initialize flash content

7 comments:

Curious Bystander said...

Nice work. I have only some remarks about your code

a) No need put every item in dataProvider by hand, This, works too

dataProvider = new ArrayCollection(
[ { name: "Joe", image: "Joe.jpg}
, { name: "Ann", image: "And.jpg}
]);

b) There's no need to override commitProperties in your itemRenderer. Just put in the List component:

<mx:List ... labelField="name" dataProvider="{dataProvider}" ..

It will make the code shorter, which is usually a good thing, and easier on eyes. At least on my eyes ;)

Kelly Davis said...

Thanks for the comments. I modified the examples to simplify it.

Unknown said...

I was able to download your original component with all the source code before you made the changes, but now I get a zip file that only contains the flex project info--no source code.

Thanks!

Anonymous said...

Nice. I'm just learning Flex and have been looking for an easy to follow example of implementing custom renderers for lists.
The Source zip file does not seem to contain any action script code though, just the project and settings files.

Kelly Davis said...

Sorry about that. The source zip should be fixed now.

Palmero Tom said...

Nice but I wonder whether you didn't end up with a memory due to using a non-weak reference... I have noticed that this is even a problem with inline functions. Just specifying weakReference=true and using it with an inline function can cause it to be garbage collected before being called. It's my experience that you should specify weakReference=true and not use inline ('anonymous') functions...

Kelly Davis said...

RIA Flex, I did some profiling to see whether there was a memory leak issue. It appears that whether or not you use non-anonymous functions and weakReferences=true for the timer listener, the memory profile doesn't seem to change. There does seem to be a slight increase in memory usage either way as each popup window gets displayed, that does not go away upon garbage collection, but it doesn't appear to be Timer objects hanging around nor anything else in my code. Could be a framework issue, but not sure.