Understanding Custom Flex Component Development

By | June 6, 2008

I’ve been refining my custom Flex component development for a while but I still smile when I come across someone’s definition of the basics that makes what I’ve already learned even more understandable. I came across Mike Nimer’s post about slides from the Flex Camp in Chicago and after reading through them I figured I’d post a summary of the contents on my own blog just to have a reference post for my own use.

Understanding the Component Lifecycle

The component lifcycle is a series of methods called by the Flex Framework that every component experiences. These methods include:

  • The component’s constructor
  • The commitProperties() method
  • The createChildren() method
  • The measure() method
  • The updateDisplayList() method

Breaking it all down:

Constructor
Inside the component’s constructor you will setup the initial properties and do tasks which need to be done regardless if the component is ever shown on the screen. You will NOT do things like creating child components or positioning items because at this stage you do not have enough information to do this. The constructor is called whenever the component is created but the other lifecycle methods do not occur until a component is added to a container:

var blah:Button = new Button(); // invokes the constructor
container.addChild(blah);       // adds the component to a container,
                                // triggering the other lifecycle methods

createChildren()
This method creates any visual children of the component. It only creates them but does NOT size or position them because at this point your component does not know how much screen space it has. You cannot tell your children how big they can be when you do not know how much space you have to work with. The same logic applies to positioning them.

measure()
To determine how much screen space a component requires, it calls its measure() method. (NOTE: The measure() method is not called on components that you explicitly set the width or height on; this makes sense because if you explicitly set the size of the component, you obviously do not care what that component thinks its size should be; you are making the decision for that component so its measure() method results are irrelevant; thanks to Maulik for pointing this out). The measure method is responsible for setting four properties of the component:

  • measuredMinWidth
  • measuredMinHeight
  • measuredWidth
  • measuredHeight

These values form the basis of a ‘suggestion’ provided by the component to its parent about how big it would like to be. This is where the updateDisplayList() method comes in.
The measure method also involves the layout manager. The layout manager is responsible for, among other things, ensuring that the process of sizing your component is started. The layout manager always starts on the outer most component. To know how much space a component needs, you need to know how much space its children need.

updateDisplayList()
When updateDisplayList() is called on a given component, it is passed an unscaledWidth parameter and an unscaledHeight parameter. Basically, it is told ‘Regardless of what you requested, here is what you get’. While Flex containers never choose to size one of their children smaller than the minimum size, you can do whatever you want. You are free to ignore a component’s suggestion and make it any size you would like and when a Flex container realizes it cannot fit all of the components into the allotted space, it will add scrollbars. updateDisplayList() is also responsible for positioning its children. The method in which the Flex containers and components position their children is implicit in their type (ie. VBox-vertical, HBox-horizontal, DateField-Horizontal). For a custom component, the method in which it arranges its children can be based on an equation that is most suitable.

commitProperties()
The commitProperties() method is responsible for coordinating property modifications. Why would you want to coordinate properties? Because there are times when you need to wait to act until more than one property upon which you are dependent is set/ready. There are also times when you do not want to do something complex every single time a change occurs. Finally, commitProperties() method allows you to defer some of these things until the screen needs to be redrawn because commitProperties() (just like updateDisplayList()) is ‘scheduled’ and called by the Flex framework when needed.

How do you tell the framework that they are needed? By calling the invalidate methods:

  • invalidateDisplayList()
  • invalidateProperties()
  • invalidateSize()

These methods tell the framework to ensure the method is called at the next appropriate time. Notice that there is no invalidateChildren(). This is because you only create children once and you do not recreate them for every property change.

So where does this leave us? Well, basically you have to remember these point:

  • Constructor() – set things up but do not create children or position things
  • createChildren() – create the children but do not position or size them
  • measure() – determines the required screen space; here you set measuredMinWidth, measuredMinHeight, measuredWidth, measuredHeight (these suggest how big the component should be)
  • invalidateSize() – tells Flex to re-measure things
  • updateDisplayList() – receives unscaledWidth and unscaledHeight, you can use or ignore this, and also position children
  • invalidateDisplayList() – tells Flex to call updateDisplayList()
  • commitProperties() – allows you to coordinate property modifications
  • invalidateProperties() – tells Flex to call invalidateProperties()

To show an example of how this all fits together, I’ll take a look at the Carousel example that was mentioned in the slide show.

The component’s lifecycle is as follows:

1. The user changes the selectedIndex/selectedChild property and we in turn set some internal values and ask for a commitProperties call.

[Bindable("change")]
public function get selectedIndex():int
{
    return _selectedIndex;
}

public function set selectedIndex( value:int ):void
{
    if ( _selectedIndex == value )
    {
        return;
    }
    _selectedIndex = value;
    selectedIndexChanged = true;
    dispatchEvent( new Event( 'change' ) );
    this.invalidateProperties();
}

2. When commitProperties() is called we determine the difference between the newly selected item is and where it needs to be and we animate through the values from the currentPosition to the selectedIndex:

/** commitProperties starts the majority of the heavy lifting here. It is called by use when the selectedIndex or selectedChild
 *  changes. We create a new animation effect that moves us from the current selected index to the newly selectedIndex.
 *  Sometimes it is easier to move around the circle to the right, and sometimes to the left, to acheive this result. The
 *  commitProperties makes this determination and sets up the proper values and starts the effect playing.
 *  selectedChild changes
 **/
override protected function commitProperties():void
{
    super.commitProperties();

    if ( selectedIndexChanged )
    {
        selectedIndexChanged = false;

        if ( !animateProperty )
        {
            animateProperty = new AnimateProperty( this );
        }

        if ( !animateProperty.isPlaying )
        {
            //Current position can get bigger and bigger, let's ensure we stay real and realize that it is just a factor
            //of the number of children
            currentPosition %= numChildren;

            if ( currentPosition < 0 )
            {
                currentPosition = numChildren + currentPosition;
            }
        }

        //Determine if it is easier to move right or left around the circle to get to our new position
        var newPosition:Number;
        var moveVector:Number = selectedIndex-currentPosition;
        var moveScalar:Number = Math.abs( moveVector );

        if ( moveScalar > ( numChildren/2 ) )
        {
            if ( moveVector < 0 )
            {
                newPosition = numChildren + selectedIndex;
            }
            else
            {
                newPosition = ( selectedIndex - numChildren );
            }
        }
        else
        {
            newPosition = selectedIndex;
        }

        animateProperty.property = "currentPosition";
        animateProperty.fromValue = currentPosition;
        animateProperty.toValue = newPosition;
        //The duration of this effect is based on how far we must move. This way a one position move is not painfully slow
        animateProperty.duration = 700 * Math.abs( currentPosition-newPosition );
        animateProperty.easingFunction = Quadratic.easeInOut;
        animateProperty.suspendBackgroundProcessing = true;
        animateProperty.play();
    }
}

3. A the setting of the currentPosition property triggers an invalidateDisplayList() call:

/** When animating the change of position, current position contains the location (in radians) of the current position. This will likely be
 *  a numeric number between the integer selectedIndex values. So, when animating between selectedIndex 1 and 2, the currentPosition will
 *  move in tenths of a radian between those two values over a fixed amount of time. **/
[Bindable("currentPositionChanged")]
public function get currentPosition():Number
{
    return _currentPosition;
}

public function set currentPosition(value:Number):void
{
    _currentPosition = value;

    dispatchEvent( new Event( 'currentPositionChanged' ) );

    invalidateDisplayList();

    if ( currentPosition == selectedIndex )
    {
        dispatchEvent( new Event( 'animationComplete' ) );
    }
}

4. Finally, the updateDisplayList() method uses the currentPosition property to determine where each image should be moved and how it should be scaled:

/** updateDisplayList does the animation time heavy lifting. It determines the size of the circle that can be drawn.
 *  The padding between each child around the circle and then calls method to size, determine the position, scale and
 *  blur of each of the children. Those values are then applied. Note, updateDisplay list uses our orderedChildren
 *  array to ensure the original order of the children is preserved despite our manipulations of their order.
 **/
override protected function updateDisplayList( unscaledWidth:Number, unscaledHeight:Number ):void
{
    super.updateDisplayList( unscaledWidth, unscaledHeight );

    var centerOfCircle:Point = new Point( unscaledWidth/2, unscaledHeight/2 ); 

    //We resize the shadowContainer to the same size as our component.
    //This ensures that the x and y positions of the components moving around the carousel
    //track with the x and y of the shadows moving on this container
    shadowContainer.setActualSize( unscaledWidth, unscaledHeight );

    var maxChildWidth:Number = 0;
    var child:UIComponent;
    for ( var i:int=0; i
    {
        child = orderedChildren[ i ] as UIComponent;
        child.setActualSize( child.getExplicitOrMeasuredWidth(), child.getExplicitOrMeasuredHeight() );
        maxChildWidth = Math.max( maxChildWidth, child.width );
    }

    radius = ( unscaledWidth /2 ) - ( maxChildWidth / 2 ) ;

    /** In a circle, we have a total of 2pi radians. So, in this case, we take the number of children present
     * and divide up those sizes that everything spaces evenly **/
    var childPaddingInRadians:Number;
    var childDistanceInRadians:Number;
    childPaddingInRadians = (2*Math.PI)/(numChildren);

    for ( var j:int=0; j
    {
        child = orderedChildren[ j ] as UIComponent;

        childDistanceInRadians = (j*childPaddingInRadians) + radianOffset - ( currentPosition * childPaddingInRadians ) ;
        var newPoint:Point = getPointOnCircle( centerOfCircle, radius, childDistanceInRadians );

        //Set the appropriate scale of the children as the move backward in 3d space.
        var scale:Number =  getScale( centerOfCircle, radius, newPoint.y );
        child.scaleX = scale;
        child.scaleY = scale;

        //Reapply the blur to the child. The base amount is determined by a style
        var blur:Number = getBlur( centerOfCircle, radius, newPoint.y );
        ( blurEffectDictionary[ child ] as BlurFilter ).blurX = blur;
        ( blurEffectDictionary[ child ] as BlurFilter ).blurY = blur;
        ( blurEffectDictionary[ child ] as BlurFilter ).quality = 1;
        child.filters = [ blurEffectDictionary[ child ] ];

        childCenterMove( child, newPoint );

        //Position the hanging shadow below the child. The base distance is determined by a style
        //This only works as the shadowContainer is mapped to the same size as this component so the
        //x and y positions correspond 1 to 1
        var shadow:HangingShadow;
        shadow = ( shadowDictionary[ child ] as HangingShadow );
        shadow.setActualSize( child.width, getStyle("baseShadowHeight") );
        shadow.move( child.x, child.y + child.height + getShadowDistance( centerOfCircle, radius, newPoint.y ) );
        shadow.scaleY = scale;
    }
}

Thanks to folks over at DigitalPrimates for the code, slides and demo component

18 thoughts on “Understanding Custom Flex Component Development

  1. Chris Spence

    Hi Bill,
    I’m a bit of a Flex newbie and have a question that may not be easily answered. I’ve built a few custom components that load local, external images (based on data imported from an external xml file). When I place the components on the stage, and run/debug… it woks seamlessly on my local computer. Upon uploading it to my site, sometimes the components appear, sometimes they don’t.

    Seems like a loading problem to me. Incidentally, running the debug version (not the release version) on my site works fine. Assuming I haven’t done anything more than load my images and swfs with the image component, or the flash.display.loader Class… (haven’t placed any checks to confirm that the data loaded before the component was added to the stage) any thoughts on what a newbie might have done wrong?

    Thanks if you can help.

    Reply
  2. Bill Post author

    Chris,

    Without having seen the code, I would just have to make a few guesses:

    1. Check the path of the URL and make sure that it will work consistently on the local machine and the server. For example when I reference an external jpg into my Flex App with a URL like this ‘/images/myimage.jpg’, you have to keep in mind that on the server, it will search from the root of the webapp so you will have to place the images in the root of the webapp directory, not the root of the project.. In my case, with Tomcat, I had to put them in an images folder in the ROOT folder of the Tomcat webapps directory.

    2. Troubleshoot it through simplification. Try a new project with a panel containing a single image. Then deploy it on the server and make sure it works there. Once that works you should be able to duplicate that way of making it work over to your larger, more complex application. Following the ‘fresh start’ path like this usually exposes what you are doing wrong in short order.

    Based on your last statement, I think you are correct about the instantiation issue. It could have something to do with the fact that the component has not fully loaded the image when it is displayed. Maybe you component that contains the image should call invalidateDisplayList or updateDisplayList once the image has loaded. This example shows how to listen for the image to finish loading. Then you could dispatch the event afterward? Just guessing

    Hope that helps.

    Reply
  3. Troy

    This was REALLY helpful. I’ve been extremely frustrated with some code I’ve been working on and this solved all my problems.

    The world is now a better place 🙂

    I never new the order of precedence you so easily described:
    * The component’s constructor
    * The commitProperties() method
    * The createChildren() method
    * The measure() method
    * The updateDisplayList() method

    Thanks Bill!

    Reply
  4. John

    This is a great post. I’ve been working with a Datagrids updateDisplayList method trying to reposition the vertical scrollbar to the left side of a datagrid. This can be extremely helpful when the number of rows exceeds the width of the datagrid. Setting the following verticalScrollBar.x = 0; moves the scrollbar but does not move the header or reposition the other columns. Have you ever come across this? Any help would be great. Thanks!

    Reply
  5. Eugene Lee

    It’s awesome.
    I know I must get through custom component well if I want to be an expert of FLEX.
    And you’ve given me great help!

    Reply
  6. Maulik

    Great article! Thanks for taking time. However, something many articles forget to mention is that measure() will not be called when width and height are explicitly set. You should consider mentioning it.

    Reply
    1. Bill Post author

      Good point. I’ve been hit by that little issue a few times myself! I’ll make a note of it in the article.

      Reply
  7. Pingback: Custom Flex Component Development – Andrew's Memory Cache

  8. Pingback: VideoDisplay Overlay Steuerelemente - Flashforum

  9. satya

    Good article but I want to point out that commitProperties() method is called before meaure()..

    Reply
    1. Bill Post author

      That is correct. In the article I did not specifically indicate this, but commitProperties is indeed called before measure().

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *