8 March 2010
This article assumes knowledge of the Flex 3 Framework.
Intermediate
Spark, the new component and skinning architecture in Flex 4, brings a lot to the RIA table and addresses a number of pain points in skinning, CSS, components, states, animation, text, graphics tags, layouts, and more.
In this article I'm going to focus on Spark layouts. I will provide an overview of the major architectural differences between Spark and Flex 3 layouts. After a brief discussion of usage differences I'll step through creating a custom Spark layout—a simple flow layout class.
Throughout this article the term MX refers to layouts, components, and the general architecture of the Flex 3 framework. The term Spark refers to layouts, components, and the general architecture introduced with Flex 4.
As with most other features of the new Spark architecture, the new layouts are in fact based on the already powerful MX layout system. Flex 3 developers will feel at home with the general execution flow, APIs, and layout logic. However, there are also a number of additions and changes that are driven by the Spark goal of more modular design.
For the developer working in MXML, not much has changed. Properties such as width, height, minWidth, explicitWidth, and percentWidth as well as their syntax and semantics have not changed. Well-known styles such as left, right, top, horizontalCenter, and baseline are supported.
The component developer will find familiar elements as well. The core LayoutManager class and component lifecycle have stayed the same. The three familiar methods for component developers—commitProperties(), measure(), and updateDisplayList()—are still called by the LayoutManager in the same order and the invalidation rules have not changed.
Components still have their default size calculated when their measure() method is called. Also, Components still have their children sized and arranged when their updateDisplayList() method is called.
Perhaps the most obvious layout difference is that in Spark the layouts have been separated from the containers! When a Spark container measure() or updateDisplayList() method is called, the task of measurement and child arrangement is promptly delegated to a Spark layout instance. This separation comes along with a number of other related novelties:
The changes in the Spark layout are geared towards making the whole system more modular, powerful, and extensible. Here is a list of the new features and enhancements that Spark brings as a direct result of the aforementioned changes:
x, y, z, rotation, scale, and more without affecting the layout. Since now all the stock Spark layouts have built-in 2D transform support, this new feature comes quite handy in lots of cases. Consider a hover over effect for example, where moving the mouse over a thumbnail in a horizontal list can scale it up a little or flip it in 3D, without actually pushing the siblings to the right.width, height, measuredWidth, and measuredHeight are now always consistently pre-transform. This eliminates confusing rules like "measuredWidth is unscaled in measure(), but scaled in updateDisplayList()".Because layouts and containers are separated in Spark, there are a few notable changes that you need to take into account when working with the layouts. Take a moment to examine the following table of MX layout container classes and their corresponding combination of Spark layout and container:
| MX Containers | Corresponding combination of Spark Layout and Container |
|---|---|
| Canvas | Group with BasicLayout (no advanced constraints) |
| HBox | Group with HorizontalLayout (or the HGroup class) |
| VBox | Group with VerticalLayout (or the VGroup class) |
| Tile | Group with TileLayout |
| List | List with VerticalLayout |
| TileList | List with TileLayout |
Note that Spark provides convenient classes for commonly used combinations—the HGroup and VGroup classes are simple Group classes that have HorizontalLayout and VerticalLayout by default.
To specify the layout for a container, set its layout property to an instance of a Spark layout:
<s:List id="list">
<s:layout>
<s:HorizontalLayout/>
</s:layout>
...
</s:List>
All layout properties are accessible through that layout class instance. Here's an example that shows how to configure the gap and the vertical alignment:
<s:List id="list">
<s:layout>
<s:HorizontalLayout gap="0" verticalAlign="justify"/>
</s:layout>
...
</s:List>
This new MXML syntax has another benefit. All the layout-related properties for the container are naturally separated into the layout class. It's very convenient to go through all of the properties for a specific layout and use code hinting to figure out what's supported by that layout.
The Spark containers Group and DataGroup are light-weight basic building block classes. Even though they support clipping and scrolling, they don't put up scrollbars automatically like MX does. Spark exposes lower-level APIs that allow for manual hook-up of scrollbars to Group and DataGroup–clipAndEnableScrolling, horizontalScrollPosition, verticalScrollPosition, contentWidth, and contentHeight. But there's also a higher-level component that facilitates this process. Wrapping any Group or DataGroup in a Scroller will enable clipping and scrolling, and the Scroller will take care of hooking up and displaying scrollbars when necessary:
<s:Scroller width="200">
<s:Group>
<s:layout>
<s:HorizontalLayout gap="0" verticalAlign="justify"/>
</s:layout>
<s:Button label="one"/>
<s:Button label="two"/>
<s:Button label="three"/>
<s:Button label="four"/>
<s:Button label="five"/>
</s:Group>
</s:Scroller>
In fact, that's exactly how scrolling is implemented for the rest of the Spark containers. Their skins contain a contentGroup or dataGroup part that is placed inside a Scroller. Take a look at the default List skin (the file spark/skins/spark/ListSkin.mxml comes with Flex 4):
...
<!--- The Scroller component to add scroll bars to the list. -->
<s:Scroller left="0" top="0" right="0" bottom="0" id="scroller" minViewportInset="1" focusEnabled="false">
<!--- The container for the data items. -->
<s:DataGroup id="dataGroup" itemRenderer="spark.skins.spark.DefaultItemRenderer">
<s:layout>
<s:VerticalLayout gap="0" horizontalAlign="contentJustify" />
</s:layout>
</s:DataGroup>
</s:Scroller>
...
Of course, all Spark scrolling APIs, scrollbars, layouts, and the Scroller class naturally support per-pixel scrolling.
A major goal of the Spark layouts is to simplify the process of creating custom layouts. Even though a new layout requires the creation of a separate class, you can get it to a functional state with relatively few lines of code. After that you can choose to gradually add more functionality to meet your specific requirements. To explore this, I'm going to create a simple FlowLayout class in which all the elements of a container are arranged horizontally, wrapping to the next row when the container width limit is reached. See Figure 1 and Figure 2 for a container with text elements, arranged by a FlowLayout at different width settings.
The minimal requirements for a functional Spark layout are deriving from the base class LayoutBase and implementing the updateDisplayList() method. Here's what an empty layout skeleton looks like:
public class FlowLayout extends
LayoutBase
{
override public function updateDisplayList(containerWidth:Number, containerHeight:Number):void
{
// TODO: iterate over the elements of the container,
// resize and position them.
}
}
When a layout is assigned to a container, the layout's target property is updated with that container. Looping over the target's elements is straightforward. Data containers, like DataGroup and List, use item renderer instances for each data item being displayed. The creation, recycling and destruction of item renderers is provided by the container. These containers are also referred to as virtualized since item renderers are created and reused for only a portion of all the data items. Typically, layouts that support virtualization use the scroll position and size to calculate which data items fall within the viewport and request elements only for the visible data items. When the layout requests an element, the data container will create and return the item renderer for the corresponding data item. For this example however, the FlowLayout requests an element for each data item as layout virtualization is beyond the scope of this article. If the container is virtualized, I call the getVirtualElementAt() method, otherwise I call getElementAt():
var layoutTarget:GroupBase = target;
var count:int = layoutTarget.numElements;
for (var i:int = 0; i < count; i++)
{
var element:ILayoutElement = useVirtualLayout ?
layoutTarget.getVirtualElementAt(i) :
layoutTarget.getElementAt(i);
}
Resizing and positioning the elements is also straightforward using the ILayoutElement APIs. The interface provides several useful methods to query the minimum, maximum, and preferred sizes as well as get and set element bounds, both pre- and post-transform. There are also methods for manipulating the 2D/3D transformation matrices. A detailed review of these is beyond the scope of this article, but here's a quick comparison with the APIs that the MX layout containers use:
| MX APIs on UIComponent (pre-transform only) | Spark APIs through ILayoutElement (pre- or post-transform) |
|---|---|
| getExplicitOrMeasuredWidth() | getPreferredBoundsWidth() |
| setActualSize() | setLayoutBoundsSize() |
| get x, get y | getLayoutBoundsX(), getLayoutBoundsY() |
| move() | setLayoutBoundsPosition() |
The following ILayoutElement APIs are used in the implementation of the updatedisplayList() method:
setLayoutBoundsSize(width, height). Quick tip: pass in NaN to set the element to its preferred size.getLayoutBoundsWidth() and getLayoutBoundsHeight().setLayoutBoundsPosition(x, y); the coordinates are relative to the container's origin.Here is what the updateDisplayList() code looks like (the code is also in the sample file FlowLayout1.as ):
for (var i:int = 0; i < count; i++)
{
// get the current element, we're going to work with the
// ILayoutElement interface
var element:ILayoutElement = useVirtualLayout ?
layoutTarget.getVirtualElementAt(i) :
layoutTarget.getElementAt(i);
element.setLayoutBoundsSize(NaN, NaN);
// Find out the element's dimensions sizes.
// We do this after the element has been already resized
// to its preferred size.
var elementWidth:Number = element.getLayoutBoundsWidth();
var elementHeight:Number = element.getLayoutBoundsHeight();
// Would the element fit on this line, or should we move
// to the next line?
if (x + elementWidth > containerWidth)
{
// Start from the left side
x = 0;
// Move down by elementHeight, we're assuming all
// elements are of equal height
y += elementHeight;
}
// Position the element
element.setLayoutBoundsPosition(x, y);
// Update the current position, add a gap of 10
x += elementWidth + 10;
}
Now that the FlowLayout is functional, I can configure the container using the familiar MXML syntax (this code is also in the sample FlowLayoutTest.mxml file):
<s:List id="list1" width="{widthSlider.value}" height="112"
dataProvider="{new ArrayCollection(
'The quick fox jumped over the lazy dog'.split(' '))}">
<!-- Configure the layout to be the FlowLayout -->
<s:layout>
<my:FlowLayout/>
</s:layout>
</s:List>
The FlowLayout works pretty well, but scrolling doesn't seem to function. I want the FlowLayout to be scrollable. In theory this means that I need to complete the following steps:
horizontalScrollPosition and verticalScrollPosition properties (these properties are implemented by the LayoutBase class).Having a Scroller in the default List skin automatically addresses steps 1 and 2. In practice, all I really have to do is calculate the scrollbar ranges. This is basically the total width and height of the scrollable area of the container—the so called content size. Calculating the content size is easy enough. I find the maximum extents of all the elements while resizing and arranging them in the updateDisplayList() method. At the very end of that method I set the updated content size using the following code (see the sample FlowLayout2.as file):
layoutTarget.setContentSize(maxWidth, maxHeight);
The common way to configure Spark layouts is through properties they expose. Adding properties to a custom layout is just like adding properties to any Flex class. It's a good idea to check the target property for null because in certain scenarios, for example during initialization, a property setter may get called before the target has been initialized. Also make sure that when the property value changes, the target container size and/or display list are invalidated appropriately. This ensures that the LayoutManager will call the container back to recalculate the layout. Here's what the horizontalGap property setter of FlowLayout looks like (this code is also in the sample FlowLayout3.as file):
public function set
horizontalGap(value:Number):void
{
_horizontalGap = value;
var layoutTarget:GroupBase = target;
if (layoutTarget)
layoutTarget.invalidateDisplayList();
}
When invoked by the LayoutManager, the container will delegate to the measure() and updateDisplayList() methods of the layout. In this example, I'm going include the newly added horizontalGap property in the position calculation for the next element. The code, near the bottom of the updateDisplayList() loop, looks like this:
// Update the current position, add the gap
x += elementWidth + _horizontalGap;
In Flex, measurement determines the default size of a component. The default size is used when the component doesn't have an explicit size defined. For example, a MX Canvas that contains some text, but doesn't have its width or height explicitly specified, will measure its default size to be just big enough to display all the text.
In Spark, whenever a container needs to be measured, the measure() method of its layout is called. Typically a layout loops over the elements and calculates the ideal area to fit all the elements in. The layout sets the default size through the container's properties measuredWidth, measuredHeight, measuredMinWidth, and measuredMinHeight. Later the container may be resized to its default size depending on its particular settings as well as its parent's settings and layout. At the end, the layout's updateDisplayList() method will be called with the final size of the container. There are a few notable things to keep in mind:
measure() method is not always called. For example, in cases where the container already has explicit sizes, the measurement will be optimized away.For the FlowLayout example, I implemented a very simplistic measurement. The measure() method calculates the default size for all the elements arranged horizontally in a single row. The code looks like this (it is also in the attached FlowLayout4.as file):
override public function measure():void
{
var totalWidth:Number = 0;
var totalHeight:Number = 0;
// loop through the elements
var layoutTarget:GroupBase = target;
var count:int = layoutTarget.numElements;
for (var i:int = 0; i < count; i++)
{
// get the current element, we're going to work with the
// ILayoutElement interface
var element:ILayoutElement = useVirtualLayout ? layoutTarget.getVirtualElementAt(i) :
layoutTarget.getElementAt(i);
// In virtualization scenarios, the element returned could
// still be null. Look at the typical element instead.
if (!element)
element = typicalLayoutElement;
// Find the preferred sizes
var elementWidth:Number = element.getPreferredBoundsWidth();
var elementHeight:Number = element.getPreferredBoundsHeight();
totalWidth += elementWidth;
totalHeight = Math.max(totalHeight, elementHeight);
}
if (count > 0)
totalWidth += (count - 1) * _horizontalGap;
layoutTarget.measuredWidth = totalWidth;
layoutTarget.measuredHeight = totalHeight;
// Since we really can't fit the content in space any
// smaller than this, set the measured minimum size
// to be the same as the measured size.
// If the container is clipping and scrolling, it will
// ignore these limits and will still be able to
// shrink below them.
layoutTarget.measuredMinWidth = totalWidth;
layoutTarget.measuredMinHeight = totalHeight;
}
Since the horizontalGap property is taken into account during measurement, changing the value should invalidate the target container size:
public function set horizontalGap(value:Number):void
{
_horizontalGap = value;
// We must invalidate the layout
var layoutTarget:GroupBase = target;
if (layoutTarget)
{
layoutTarget.invalidateSize();
layoutTarget.invalidateDisplayList();
}
}
A final note on measurement—it's a good idea to test layout measurement in bare-bone containers like Group as there are no default container sizes. Using any skinned container, like List or Panel, brings the skin's default sizes into the picture and makes testing more difficult. The sample file FlowLayoutTestMeasure.mxml demonstrates the FlowLayout measure logic in the context of the Group container.
As part of Flex 4, the Spark architecture has a lot of evolutionary changes in the layout system that bring numerous new and exciting possibilities. This article merely scratches the surface of Spark layouts.
For more information on the stock Spark layout rules, syntax, and usage examples, the online documentation is a good place to start.
For more information on the layout APIs, take a look at LayoutBase, ILayoutElement,IViewport, Scroller, GroupBase, DataGroup and the related classes in the Flex 4 Language Reference. Also make sure to check out Hans Muller's blog as he has excellent insights on scrolling, virtualization, layouts, and much more. You can also find custom layout topics and examples on my blog.

This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License