This is part three of a look into custom layouts in Flex 4. Be sure to read part 1 and part 2 first.

A dynamic custom layout.

So far we have created a very simple layout. In fact the layout won’t always work in every situation because it doesn’t override all the required methods for a layout. Now we will look at updating the layout to include the other required method and adding some new features.

Implementing measure()

Our layout from part two was missing the implementation of the measure() method. This method is used to set the measured width, measured height, measured min width and measured mid height. Flex will uses this information if you haven’t constrained the element already by explicitly setting it’s height and width. Previously we had our group in a scroller and the updateDisplayList() method was taking care of updating the content size for the scroller.

All our method needs to do is set these size properties, based on the size of the content we are displaying.

override public function measure():void
{
	var measuredWidth:Number = 0;
	var measuredHeight:Number = 0;
 
	// TODO - Update measuredWidth and measuredHeight to be the correct size of the content
 
	var currentGroup:GroupBase = target;
	currentGroup.measuredWidth = measuredWidth;
	currentGroup.measuredHeight = measuredHeight;
	currentGroup.measuredMinWidth = measuredWidth;
	currentGroup.measuredMinHeight = measuredHeight;
}

For our example we will set the min width and height to the same as the normal width and height, as we want all of our content to be displayed.

Our next part of the implementation is to update our variables to represent the correct size of our layout. To do this we will loop though all the elements adding the additional size they will add to our values. This is very similar to how we lay them out in the updateDisplayList() method.

override public function measure():void
{
	var measuredWidth:Number = 0;
	var measuredHeight:Number = 0;
 
	var currentLayoutElement:ILayoutElement;
	var currentGroup:GroupBase = target;
	for (var i:int = 0; i < currentGroup.numElements; i++)
	{
		currentLayoutElement = currentGroup.getElementAt(i);
		if(!currentLayoutElement)
			continue; // Skip this loop if there isn't an element
 
		measuredWidth += 10; // Add the aditional width an extra element will add
 
	}
 
	if(currentLayoutElement) // add the width/height of the last element (shown in full)
	{
		measuredWidth += currentLayoutElement.getPreferredBoundsWidth();
		measuredHeight += currentLayoutElement.getPreferredBoundsHeight()
	}
 
	var currentGroup:GroupBase = target;
	currentGroup.measuredWidth = measuredWidth;
	currentGroup.measuredHeight = measuredHeight;
	currentGroup.measuredMinWidth = measuredWidth;
	currentGroup.measuredMinHeight = measuredHeight;
}

Now our element will size it’s self correctly when asked to by the layoutManager.

Dynamic layout properties

The next step is to add more features. The size of the overlap gap is great, but what if we want to change that value. Currently we would have to update both of the methods that use it. It would be much better to have this value as a property so we can just change it in one place. Even better though, would be to make it public, so another object can change it at runtime, and while we are there why not add the ability to add a vertical overlap too.

private var _horizontalOverlap:int = 10;
 
private var _verticalOverlap:int = 0;
 
/**
 * The amount of each element that is shown before it's overlapped by the next horizontally
 */
public function get horizontalOverlap():int
{
	return _horizontalOverlap;
}
 
/**
 * @private
 */
public function set horizontalOverlap(value:int):void
{
	_horizontalOverlap = value;
	if(target)
	{
		// Invalidate the size and display to trigger a re-draw
		target.invalidateSize();
		target.invalidateDisplayList();
	}
}
 
/**
 * The amount of each element that is shown before it's overlapped by the next vertically
 */
public function get verticalOverlap():int
{
	return _verticalOverlap;
}
 
/**
 * @private
 */
public function set verticalOverlap(value:int):void
{
	_verticalOverlap = value;
	if(target)
	{
		// Invalidate the size and display to trigger a re-draw
		target.invalidateSize();
		target.invalidateDisplayList();
	}
}

We use accessors here, so that when the properties are changed via the setter we can invalidate our layout, which will cause it to be redrawn in the next render. This means that any changes to these values will be seen by the user!

All we need to do now, is use these variables in our methods, rather than the hard coded values. First lets update the measure() method.

override public function measure():void
{
	var measuredWidth:Number = 0;
	var measuredHeight:Number = 0;
 
	var currentLayoutElement:ILayoutElement;
	var currentGroup:GroupBase = target;
	for (var i:int = 0; i < currentGroup.numElements; i++)
	{
		currentLayoutElement = currentGroup.getElementAt(i);
		if(!currentLayoutElement)
			continue; // Skip this loop if there isn't an element
 
		measuredWidth += _horizontalOverlap; // Add the aditional width an extra element will add
		measuredHeight  += _verticalOverlap; // Add the aditional height an extra element will add
	}
 
	if(currentLayoutElement) // add the width/height of the last element (shown in full)
	{
		measuredWidth += currentLayoutElement.getPreferredBoundsWidth();
		measuredHeight += currentLayoutElement.getPreferredBoundsHeight()
	}
 
	// Set the sizes and min sizes
	currentGroup.measuredWidth = measuredWidth;
	currentGroup.measuredHeight = measuredHeight;
	currentGroup.measuredMinWidth = measuredWidth;
	currentGroup.measuredMinHeight = measuredHeight; 	
}

We also need to change the updateDisplayList() method in the same way.

override public function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
 {
     var currentGroup:GroupBase = target; // The current group whos content we are layout out
     var x:Number = 0; // The x position which we will increase to move each element along
		var y:Number = 0; // The y position which we will increase to move each element down
     var currentLayoutElement:ILayoutElement; // The current layout target (in our demo this is one of the images)
 
     // Iterate through all the elements in the target so we can position it
     for (var i:int = 0; i < currentGroup.numElements; i++)
     {
         currentLayoutElement = currentGroup.getElementAt(i);
			if(!currentLayoutElement)
				continue; // Skip this loop if there isn't an element
 
			currentLayoutElement.setLayoutBoundsSize(NaN, NaN); // Causes the layout to size to it's default size
         	currentLayoutElement.setLayoutBoundsPosition(x, y);
         	x += _horizontalOverlap;
			y += _verticalOverlap;
     }
 
     // Set the content size, so it can be scrolled
     // This is the current x value plus the width of the last element. There is only one row, so the height is the same as an element height
     currentGroup.setContentSize(x + currentLayoutElement.getPreferredBoundsWidth(), y + currentLayoutElement.getPreferredBoundsHeight());
 }

Demo

Now we have quite a nice demo, showing how the measure() method is used, and also how the you can update the layout at runtime.

You can download the example code here, and view the demo in a new window here.

This entry was posted on Monday, November 22nd, 2010 at 3:38 pm and is filed under ActionScript, AIR, Development, Flex. You can leave a comment and follow any responses to this entry through the RSS 2.0 feed.

One Comment Leave a comment

  1. Patrick 31 March 2011 at 4:49 pm #

    Thank you very much for this information!

Leave a Reply