O'Reilly    
 Published on O'Reilly (http://oreilly.com/)
 See this if you're having trouble printing code examples


Layout: Chapter 3 - Flex 4 Cookbook

by Joshua Noble, Todd Anderson, Marco Casario, Garth Braithwaite
Flex 4 Cookbook

This excerpt is from Flex 4 Cookbook. This highly practical book contains hundreds of tested recipes for developing interactive Rich Internet Applications. You'll find everything from Flex basics to solutions for working with visual components and data access, as well as tips on application development, unit testing, and Adobe AIR. Each recipe features a discussion of how and why it works, and many of them offer sample code that you can put to use immediately.

buy button

Chapter 3. Layout

Visual elements of an application are sized and positioned within a parent container based on rules provided to and by a managing layout. The Flex Framework provides two sets of containers for layout: MX and Spark. The MX containers reside in the mx.containers package of the Flex Framework, while the Spark containers reside in the spark.components package. Though both container sets inherit from UIComponent, they differ in how they lay out and manage the children in their display lists.

Within a MX container (such as Box), the size and position of the children are managed by the container’s layout rules and constraints, which are internally defined and based on specified properties and styles. In contrast, the Spark set provides a level of abstraction between the container and the layout and allows you to define the layout separately from the skin and style. The separation of the layout from the container not only provides greater flexibility in terms of runtime modifications but also cuts down on the rendering cycle for a container, as the style properties of a container may not be directly related to the layout. A Spark layout manages the size and positioning of the target container’s child elements and is commonly referred to as the container’s layout delegate. Commonly used layout classes for Spark containers, such as VerticalLayout, can be found in the spark.layouts package of the Flex Framework and are extensions of the base LayoutBase class.

When you provide a layout delegate to a Spark container, the target property of the layout is attributed as the targeted container and considered to be a GroupBase-based element. The containers available in the Spark set, such as Group and DataGroup, are extensions of GroupBase and provide a set of methods and properties for accessing their child elements. This set is commonly referred to as the content API. Containers that handle visual elements directly, such as Group and SkinnableContainer, expose methods and attributes of the content API by implementing the IVisualElementContainer interface. Containers that handle data items that are presented based on item renderers, such as DataGroup and SkinnableDataContainer, provide the same methods and attributes directly on their extensions of GroupBase. The layout delegate of a container uses the content API to access and manage that container’s child elements.

Child elements accessed from the content API are attributed as implementations of the IVisualElement interface. This interface exposes implicit properties that allow you to access and modify common properties that relate to how the element is laid out and displayed in a container. IVisualElement is an extension of the ILayoutElement interface, which exposes constraint properties and accessor methods that layout delegates use to size and position children within a target container. With UIComponent implementing the IVisualElement interface, you can add elements from both the MX and Spark component sets to the display list of a Spark container and manage them using a layout delegate.

Position Children Linearly

Problem

You want to control the layout of children in a container, positioning them either horizontally or vertically.

Solution

Assign either HorizontalLayout or VerticalLayout to the layout property of the container, and set the desired alignment properties to the children along the axis of the specified layout.

Discussion

The HorizontalLayout and VerticalLayout classes are extensions of the spark.layout.LayoutBase class and lay out the child elements of a container in a horizontal or vertical sequence, respectively. Spark layouts handle only the size and position of child elements. Attributes related to dimension and positioning constraints are not available on Spark layouts; these are properties of the targeted Spark container.

You can define distances between child elements using the gap property of the HorizontalLayout and VerticalLayout classes. For example:

<s:Group>

    <s:layout>
        <s:VerticalLayout gap="10" />
    </s:layout>

    <s:TextInput text="hello world" />
    <s:Button label="click me" />

</s:Group>

The <s:Group> tag defines the parent Spark container, whose layout manager is specified as a VerticalLayout instance. This example lays out the child elements of the Group container vertically and distanced from each other by 10 pixels.

To position child elements relative to the container boundaries, assign values to the paddingLeft, paddingRight, paddingTop, and paddingBottom properties of the layout container, as shown here:

<s:Group>

    <s:layout>
        <s:HorizontalLayout gap="5"
                            paddingLeft="10" paddingRight="10"
                            paddingTop="10" paddingBottom="10" />
    </s:layout>

    <s:TextInput text="hello world" />
    <s:Button label="click me" />

</s:Group>

If you define a fixed or relative (using percent values) size for the container, the verticalAlign and horizontalAlign properties of HorizontalLayout and VerticalLayout, respectively, are used by the layout to position each of the container’s child elements with respect to each other and the container boundaries:

<s:Group width="300">

    <s:layout>
        <s:VerticalLayout horizontalAlign="center" />
    </s:layout>

    <s:TextInput text="hello world" />
    <s:Button label="click me" />

</s:Group>

Switch Layout Management at Runtime

Problem

You want to change the layout sequence of child elements at runtime.

Solution

Update the declared layout property of a Spark container at runtime in response to an event.

Discussion

The layout property of a Spark container defines the layout management delegate for child elements in the container’s display list. The default layout instance for a Spark container is spark.layouts.BasicLayout, which places children using absolute positioning. When the default layout is specified for a container, child elements are stacked upon each other based on their declared depths and the position of each element within the declared display list. You can instead supply sequenced-based layouts from the spark.layouts package or create custom layouts to manage the size and positioning of child elements.

The layout implementation for a Spark container can also be switched at runtime, as in the following example:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               creationComplete="handleCreationComplete();">

    <fx:Declarations>
        <s:VerticalLayout id="vLayout" gap="5" />
        <s:HorizontalLayout id="hLayout" gap="5" />
    </fx:Declarations>

    <fx:Script>
        <![CDATA[
            import spark.layouts.VerticalLayout;

            private function handleCreationComplete():void
            {
                layout = vLayout;
            }
            private function toggleLayout():void
            {
                layout = ( layout is VerticalLayout ) ? hLayout : vLayout;
            }
        ]]>
    </fx:Script>

    <s:TextInput text="hello world" />
    <s:Button label="click me" click="toggleLayout();" />

</s:Application>

In this example, the target container for the layout is the Application container. Two separate layout managers are declared in the <fx:Declarations> tag, and the designated layout is updated based on a click of the Button control, changing from VerticalLayout to HorizontalLayout.

The next example shows how to switch between these two layouts in a MX container:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx">

    <fx:Script>
        <![CDATA[
            import mx.containers.BoxDirection;

            private function toggleLayout():void
            {

                container.direction = 
                       ( container.direction == BoxDirection.VERTICAL )
                       ? BoxDirection.HORIZONTAL
                       : BoxDirection.VERTICAL;
            }
        ]]>
    </fx:Script>

    <mx:Box id="container" direction="vertical">

        <s:TextInput text="hello world" />
        <s:Button label="click me" click="toggleLayout();" />

    </mx:Box>

</s:Application>

With respect to layout management, the main difference between Spark and MX controls has to do with the separation of responsibilities for the parent container. Within the Spark architecture, you specify a layout delegate for a target container. This allows you to easily create multiple layout classes that manage the container’s child elements differently. Within the context of the MX architecture, any modifications to the layout of children within a container are confined to properties available on the container. Instead of easily changing layout delegates at runtime as you can do in Spark, one or more properties need to be updated, which invokes a re-rendering of the display.

This ability to switch layout implementations easily is a good example of the advantages of the separation of layout and containers within the Spark architecture of the Flex 4 SDK, and the runtime optimizations it enables.

Align and Size Children Within a Layout

Problem

You want to define the alignment of child elements within a container.

Solution

Use the verticalAlign and horizontalAlign properties of a sequenced-based layout.

Discussion

Using HorizontalLayout, VerticalLayout, and TileLayout, you can uniformly align the child elements of a container. To define the alignment along the x-axis, use the verticalAlign property of the HorizontalLayout class; along the y-axis, use the horizontalAlign property of the VerticalLayout class. TileLayout supports both the verticalAlign and horizontalAlign properties; it lays out the child elements of a container in rows and columns.

The available property values for horizontalAlign and verticalAlign are enumerated in the spark.layouts.HorizontalAlign and spark.layouts.VerticalAlign classes, respectively, and correspond to the axis on which child elements are added to the container.

The following example demonstrates dynamically changing the alignment of child elements along the y-axis:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx">

    <fx:Script>
        <![CDATA[
            import spark.layouts.HorizontalAlign;
            private function changeAlignment():void
            {
                vLayout.horizontalAlign = 
                       (vLayout.horizontalAlign == HorizontalAlign.LEFT) 
                        ? HorizontalAlign.RIGHT 
                        : HorizontalAlign.LEFT;
            }
        ]]>
    </fx:Script>

    <s:Panel title="Alignment Example" width="300">

        <s:layout>
            <s:VerticalLayout id="vLayout"
                              horizontalAlign="{HorizontalAlign.LEFT}" />
        </s:layout>

        <s:DropDownList />
        <s:HSlider />
        <s:Button label="button" click="changeAlignment();" />

    </s:Panel>

</s:Application>

When the s:Button control is clicked, the child elements are changed from being left-aligned to being right-aligned within a vertical layout of the s:Panel container.

To align children along the x-axis, specify HorizontalLayout as the layout property of the container and set the verticalAlign property value to any of the enumerated properties of the VerticalAlign class. To align the child elements in the center of a container along a specified axis, use HorizontalAlign.CENTER or VerticalAlign.MIDDLE as the property value for verticalAlign or horizontalAlign, respectively, as in the following example:

<s:Panel height="300">

    <s:layout>
        <s:HorizontalLayout id="hLayout" verticalAlign="{VerticalAlign.MIDDLE}" />
    </s:layout>

    <s:DropDownList />
    <s:HSlider />
    <s:Button label="button" click="changeAlignment();" />

</s:Panel>

Alignment properties can also be used to uniformly size all child elements within a layout. The two property values that are available on both HorizontalAlign and VerticalAlign are justify and contentJustify. Setting the justify property value for verticalAlign on a HorizontalLayout or TileLayout will size each child to the height of the target container. Setting the justify property value for horizontalAlign on a VerticalLayout or TileLayout will size each child to the width of the target container. Setting the contentJustify property value sizes children similarly, but sets the appropriate dimension of each child element based on the content height of the target container. The content height of a container is relative to the largest child, unless all children are smaller than the container (in which case it is relative to the height of the container). Both the justify and contentJustify property values uniformly set the corresponding size dimension on all child elements based on the specified layout axes and the target container; any width or height property values defined for the child elements are disregarded.

The following example switches between the center and justify values for the horizontalAlign property of a VerticalLayout to demonstrate how dimension properties of child elements are ignored when laying out children uniformly based on size:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx">

    <fx:Script>
        <![CDATA[
            import spark.layouts.HorizontalAlign;
            private function changeAlignment():void
            {
                vLayout.horizontalAlign = 
                        (vLayout.horizontalAlign == HorizontalAlign.JUSTIFY) 
                        ? HorizontalAlign.CENTER
                        : HorizontalAlign.JUSTIFY;
            }
        ]]>
    </fx:Script>

    <s:Panel width="300">

        <s:layout>
            <s:VerticalLayout id="vLayout"
                              horizontalAlign="{HorizontalAlign.JUSTIFY}" />
        </s:layout>

        <s:DropDownList width="200" />
        <s:HSlider />
        <s:Button label="button" click="changeAlignment();" />

    </s:Panel>

</s:Application>

Lay Out Children Using Rows and Columns

Problem

You want to display child elements in a sequence of rows and columns.

Solution

Assign TileLayout to the layout property of a Spark container to dynamically place its child elements in a grid.

Discussion

TileLayout adds children to the display list in both a horizontal and vertical fashion, positioning them in a series of rows and columns. It displays the child elements in a grid based on the dimensions of the target Spark container, as the following example demonstrates:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark">

    <fx:Declarations>
        <fx:String id="txt">
            Lorem ipsum dolor sit amet consectetur adipisicing elit.
        </fx:String>
    </fx:Declarations>

    <s:layout>
        <s:VerticalLayout />
    </s:layout>

    <s:HSlider id="slider" minimum="100" maximum="400" value="250" />

    <s:DataGroup width="{slider.value}"
                 itemRenderer="spark.skins.spark.DefaultItemRenderer">

        <s:layout>
            <s:TileLayout />
        </s:layout>

        <s:dataProvider>
            <s:ArrayCollection source="{txt.split(' ')}" />
        </s:dataProvider>

    </s:DataGroup>

</s:Application>

In this example, the width of the parent container for the layout delegate is updated in response to a change to the value property of the HSlider control. As the width dimension changes, columns are added or removed and child elements of the target DataGroup container are repositioned accordingly.

By default, the sequence in which child elements are added to the layout is based on rows. Children are added along the horizontal axis in columns until the boundary of the container is reached, at which point a new row is created to continue adding child elements. If desired, you can change this sequence rule using the orientation property of TileLayout, which takes a value of either rows or columns. The following example changes the default layout sequence from rows to columns, adding each child vertically in a row until the lower boundary of the container is reached, at which point a new column is created to take the next child element:

<s:DataGroup width="{slider.value}"
             itemRenderer="spark.skins.spark.DefaultItemRenderer">

    <s:layout>
        <s:TileLayout orientation="rows" />
    </s:layout>

    <s:dataProvider>
        <s:ArrayCollection source="{txt.split(' ')}" />
    </s:dataProvider>

</s:DataGroup>

You can restrict the number of rows and columns to be used in the display by specifying values for the requestedRowCount and requestedColumnCount properties, respectively, of a TileLayout. The default value for these properties is −1, which specifies that there is no limit to the number of children that can be added to the display in a row or column. By modifying the default values, you control how many child elements can be added to a row/column (rather than allowing this to be determined by the specified dimensions of the target container).

When you specify a nondefault value for the requestedRowCount or requestedColumnCount property of a TileLayout, the target container is measured as children are added to the display. If a width and height have not been assigned to the target container directly, the dimensions of the container are determined by the placement and size of the child elements laid out in rows and columns by the TileLayout, as in the following example:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark">

    <fx:Declarations>
        <fx:String id="txt">
            Lorem ipsum dolor sit amet consectetur adipisicing elit.
        </fx:String>
    </fx:Declarations>

    <s:layout>
        <s:VerticalLayout />
    </s:layout>

    <s:Group>

        <s:Scroller>
            <s:DataGroup width="100%"
                         itemRenderer="spark.skins.spark.DefaultItemRenderer">

                <s:layout>
                    <s:TileLayout requestedRowCount="2" requestedColumnCount="3"
                                  clipAndEnableScrolling="true" />
                </s:layout>

                <s:dataProvider>
                    <s:ArrayCollection source="{txt.split(' ')}" />
                </s:dataProvider>

            </s:DataGroup>
        </s:Scroller>

        <s:Rect width="100%" height="100%">
            <s:stroke>
                <s:SolidColorStroke />
            </s:stroke>
        </s:Rect>

    </s:Group>

</s:Application>

In this example, the TileLayout target container and a Rect graphic element are wrapped in a Group to show how the Group is resized to reflect the child element positioning provided by the layout.

Because the TileLayout disregards the target container’s dimensions when strictly positioning child elements in a grid based on the requestedRowCount and requestedColumnCount property values, unless scrolling is enabled children may be visible outside of the calculated row and column sizes. Consequently, in this example the target DataGroup container is wrapped in a Scroller component and the clipAndEnableScrolling property is set to true on the TileLayout.

Size Children Uniformly

Problem

You want all children within a container to be the same size.

Solution

To restrict the size of the child elements within a target container, use the columnHeight and rowHeight properties of HorizontalLayout and VerticalLayout, respectively. To dynamically size and position all children of a target container based on the dimensions of a single child element, use the typicalLayoutElement property.

Discussion

By default, the variableRowHeight and variableColumnHeight properties of HorizontalLayout and VerticalLayout, respectively, are set to a value of true. This default setting ensures that all child elements are displayed based on their individually measured dimensions. This can be beneficial when presenting elements that vary in size, but the rendering costs may prove to be a performance burden at runtime. To speed up rendering time, Spark layouts have properties for setting static values to ensure that all child elements are sized uniformly.

The following example sets the rowHeight and variableRowHeight property values to constrain the height of child elements in a target container using a vertical layout:

<s:Group>

    <s:layout>
        <s:VerticalLayout variableRowHeight="false" rowHeight="50" />
    </s:layout>

    <s:Button id="btn1" label="(1) button" />
    <s:Button id="txt2" label="(2) button" height="10" />
    <s:Button id="txt3" label="(3) button" height="30" />
    <s:Button id="txt4" label="(4) button" />

</s:Group>

In this example, the height property value assigned to any declared Button control is disregarded and all the children are set to the same height as they are positioned vertically without respect to the variable measure calculated by properties of each child.

To apply size constraints in a horizontal layout, use the columnWidth and variableColumnWidth properties, as in the following example:

<s:Group>

    <s:layout>
        <s:HorizontalLayout variableColumnWidth="false" columnWidth="80" />
    </s:layout>

    <s:Button id="btn1" label="(1) button" />
    <s:Button id="btn2" label="(2) button" height="50" />
    <s:Button id="btn3" label="(3) button" height="30" />
    <s:Button id="btn4" label="(4) button" />

</s:Group>

The previous two examples show how to specify static values for the dimensions of all child elements of a target container with regard to the specified layout control. Alternatively, child dimensions and subsequent positions can be determined by supplying an ILayoutElement instance as the value for a layout control’s typicalLayoutElement property. In this case, the target container’s children are sized and positioned based on the width or height, respectively, of the supplied target instance.

The following example supplies a child target to be used in sizing and positioning all children of a target container with a vertical layout:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark">

    <s:Group>

        <s:layout>
            <s:VerticalLayout variableRowHeight="false"
                              typicalLayoutElement="{btn3}" />
        </s:layout>

        <s:Button id="btn1" label="(1) button" />
        <s:Button id="btn2" label="(2) button" height="50" />
        <s:Button id="btn3" label="(3) button" height="30" />
        <s:Button id="btn4" label="(4) button" />

    </s:Group>

</s:Application>

In this example, each Button control is rendered at the height attributed to the assigned typicalLayoutElement; any previously assigned height property values are disregarded.

Lazily Create and Recycle Children

Problem

You want to improve runtime performance by creating and rendering children of a container as needed.

Solution

Use the useVirtualLayout property of a HorizontalLayout, VerticalLayout, or TileLayout whose target container is a DataGroup.

Discussion

Virtualization improves runtime performance by creating and recycling item renderers and rendering children only as they come into the visible content area of a display container. Layout classes that extend spark.layouts.LayoutBase, such as VerticalLayout, expose the useVirtualLayout property. Attributed as a Boolean value, useVirtualLayout is used to determine whether to recycle item renderers in a data element container that supports virtualization, such as DataGroup. When employing virtualization, access to data elements using methods of the content API (such as getElementAt()) is limited to elements visible within the content area of the container.

The following demonstrates enabling the use of virtualization on the layout delegate of a DataGroup container:

<fx:Declarations>
    <fx:String id="txt">
        Lorem ipsum dolor sit amet consectetur adipisicing elit.
    </fx:String>
</fx:Declarations>

<s:Scroller>

    <s:DataGroup width="150" height="120"
                 itemRenderer="spark.skins.spark.DefaultItemRenderer">

        <s:layout>
            <s:VerticalLayout useVirtualLayout="true" />
        </s:layout>

        <s:dataProvider>
            <mx:ArrayCollection source="{txt.split(' ')}" />
        </s:dataProvider>

    </s:DataGroup>

</s:Scroller>

In this example, the size of the DataGroup container is restricted to show approximately four child elements at a time based on the specified item renderer and supplied data. As new child elements are scrolled into view, previously created item renderer instances that have been scrolled out of view are reused to render the new data.

When a layout delegate is provided to a container, the target property of the layout is attributed to the target container of type spark.components.subclasses.GroupBase. When creating and reusing elements, the layout delegate uses methods of the content API exposed by a GroupBase that supports virtualization. At the time of this writing, DataGroup is the only container in the Flex 4 SDK that supports virtualization. You can, however, create custom containers that create, recycle, and validate child elements accordingly by extending GroupBase and overriding the getVirtualElementAt() method.

To demonstrate how virtual and nonvirtual children are treated within a GroupBase, the following example loops through the child elements of a container and traces out the virtual elements:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx"
               creationComplete="handleCreationComplete()">

    <fx:Library>
        <fx:Definition name="CustomRect">
            <s:Rect width="100">
                <s:fill>
                    <mx:SolidColor color="#000000" />
                </s:fill>
            </s:Rect>
        </fx:Definition>
    </fx:Library>

    <fx:Script>
        <![CDATA[
            import spark.components.supportClasses.GroupBase;
            private function handleCreationComplete():void
            {
                scroller.verticalScrollBar.addEventListener(Event.CHANGE,
                        inspectVirtualChildren);
            }

            private function inspectVirtualChildren( evt:Event ):void
            {
                var target:GroupBase = vLayout.target;
                for( var i:int = 0; i < target.numElements; i++ )
                {
                    trace( target.getElementAt( i ) );
                }
            }
        ]]>
    </fx:Script>

    <s:Scroller id="scroller">

        <s:DataGroup width="150" height="120"
                     itemRenderer="spark.skins.spark.DefaultComplexItemRenderer"
                     creationComplete="inspectVirtualChildren(event);">

            <s:layout>
                <s:VerticalLayout id="vLayout" useVirtualLayout="true" />
            </s:layout>

            <s:dataProvider>
                <mx:ArrayCollection>
                    <fx:CustomRect height="20" />
                    <s:Button height="100" />
                    <s:DropDownList height="40" />
                    <fx:CustomRect height="60" />
                </mx:ArrayCollection>
            </s:dataProvider>

        </s:DataGroup>

    </s:Scroller>

</s:Application>

As the scroll position of the targeted DataGroup container changes, any child elements accessed using the getElementAt() method that are not in view are attributed as null. When you create a custom layout that respects virtualization of data elements, use the getVirtualElementAt() method of the GroupBase target with the getScrollRect() method to anticipate which elements will be visible in the content area of the container.

The DefaultComplexItemRenderer is used as the item renderer for the DataGroup container to show that virtualization also works with child elements of differing sizes.

When working with a relatively small data set, such as in this example, using virtualization to improve rendering performance may seem trivial. The true power of using lazy creation and recycling children through virtualization really becomes evident when using large data sets, such as a group of records returned from a service.

Create a Custom Layout

Problem

You want to create a custom layout for a container to use to display its child elements.

Solution

Create a custom layout by extending the com.layouts.supportClasses.LayoutBase class and override the updateDisplayList() method to position and size the children accordingly.

Discussion

What if a project comes along in which the desired layout is not available from the layout classes provided by the Flex 4 SDK? You can easily create and apply a custom layout for a container by extending the LayoutBase class, thanks to the separation of responsibilities in the Spark component architecture. Child elements of a targeted container are positioned and sized within the updateDisplayList() method of a LayoutBase subclass, such as HorizontalLayout or VerticalLayout. By overriding the updateDisplayList() method in a custom layout, you can manipulate the display list of a targeted GroupBase-based container.

Each child of a GroupBase container is attributed as an ILayoutElement instance. Methods are available on the ILayoutElement interface that relate to how the child element is drawn on screen with regard to size and position. To create a custom layout for a targeted container’s child elements, loop through those children and apply any desired transformations:

package com.oreilly.f4cb
{
    import mx.core.ILayoutElement;

    import spark.components.supportClasses.GroupBase;
    import spark.layouts.supportClasses.LayoutBase;

    public class CustomLayout extends LayoutBase
    {

        override public function updateDisplayList(width:Number, height:Number)
                : void
        {
            super.updateDisplayList( width, height );

            var layoutTarget:GroupBase = target;
            var w:Number = width / 2;
            var h:Number = height / 2;
            var angle:Number = 360 / layoutTarget.numElements;
            var radius:Number = w;
            var radians:Number;
            var xpos:Number;
            var ypos:Number;
            var element:ILayoutElement;

            for( var i:int = 0; i < layoutTarget.numElements; i++ )
            {
                element = layoutTarget.getElementAt( i );
                radians = ( ( angle * -i ) + 180 ) * ( Math.PI / 180 );
                xpos = w + ( Math.sin(radians) * radius );
                ypos = h + ( Math.cos(radians) * radius );

                element.setLayoutBoundsSize( NaN, NaN );
                element.setLayoutBoundsPosition( xpos, ypos );
            }
        }
    }
}

The CustomLayout class in this example displays the child elements uniformly radiating from the center of a target container in a clockwise manner. As the child elements of the container are accessed within the for loop, each ILayoutElement instance is accessed using the getElementAt() method of the content API and is given a new position based on the additive angle using the setLayoutBoundsPosition() method. The size of each ILayoutElement is determined by a call to the setLayoutBoundsSize() method. Supplying a value of NaN for the width or height argument ensures that the size of the element is determined by the rendered content of the element itself.

A custom layout is set on a container by using the layout property of the container:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx"
               xmlns:f4cb="com.oreilly.f4cb.*">

    <fx:Declarations>
        <fx:String id="txt">
            Lorem ipsum dolor sit amet consectetur adipisicing elit.
        </fx:String>
    </fx:Declarations>

    <s:layout>
        <s:VerticalLayout horizontalAlign="center" />
    </s:layout>

    <s:HSlider id="slider" minimum="200" maximum="400" value="300" />

    <s:DataGroup width="{slider.value}" height="{slider.value}"
                 itemRenderer="spark.skins.spark.DefaultItemRenderer">

        <s:layout>
            <f4cb:CustomLayout />
        </s:layout>

        <s:dataProvider>
            <mx:ArrayCollection source="{txt.split(' ')}" />
        </s:dataProvider>

    </s:DataGroup>

</s:Application>

When the updateDisplayList() method of the layout is invoked, as happens in response to a change in the container size or the addition of an element on the content layer, the children are laid out using the rules specified in the CustomLayout class. Generally speaking, it is good practice to also override the measure() method when creating a custom layout class extending LayoutBase. The measure() method handles resizing the target container accordingly based on its child elements and constraint properties. However, because the custom layout created in this example positions children based on the bounds set upon the target container, this is not necessary.

Measure and Alter the Container Size

Problem

You want to set the size of a target container based on the dimensions of the child elements in the layout.

Solution

Create a LayoutBase-based custom layout and override the measure() method to access the desired bounds for the container based on the dimensions of its child elements.

Discussion

When explicit dimensions are not specified for the target container, control over its size is handed over to the layout. When width and height values are applied to a container, the explicitWidth and explicitHeight properties (respectively) are updated, and their values determine whether to invoke the layout’s measure() method. If values for these properties are not set (equated as a value of NaN), the container’s layout delegate determines the target dimensions and updates the container’s measuredWidth and measuredHeight property values accordingly.

To alter how the dimensions of a target container are determined, create a custom layout and override the measure() method:

package com.oreilly.f4cb
{
    import mx.core.IVisualElement;

    import spark.components.supportClasses.GroupBase;
    import spark.layouts.VerticalLayout;

    public class CustomLayout extends VerticalLayout
    {
        override public function measure() : void
        {
            var layoutTarget:GroupBase = target;
            var count:int = layoutTarget.numElements;
            var w:Number = 0;
            var h:Number = 0;
            var element:IVisualElement;
            for( var i:int = 0; i < count; i++ )
            {
                element = layoutTarget.getElementAt( i );
                w = Math.max( w, element.getPreferredBoundsWidth() );
                h += element.getPreferredBoundsHeight();
            }

            var gap:Number = gap * (count - 1 );
            layoutTarget.measuredWidth = w + paddingLeft + paddingRight;
            layoutTarget.measuredHeight = h + paddingTop + paddingBottom + gap;
        }
    }
}

The CustomLayout class in this example sets the measuredWidth and measuredHeight properties of the target container based on the largest width and the cumulative height of all child elements, taking into account the padding and gap values of the layout.

When the measure() method is overridden in a custom layout, it is important to set any desired measure properties of the target container. These properties include measuredWidth, measuredHeight, measuredMinWidth, and measuredMinHeight. These properties correspond to the size of the target container and are used when updating the display during a pass in invalidation of the container.

To apply a custom layout to a container, use the layout property:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx"
               xmlns:f4cb="com.oreilly.f4cb.*">

    <fx:Script>
        <![CDATA[
            import mx.graphics.SolidColor;
            import spark.primitives.Rect;
            private function addRect():void
            {
                var rect:Rect = new Rect();
                rect.width = Math.random() * 300;
                rect.height = Math.random() * 30;
                rect.fill = new SolidColor( 0xCCFFCC );
                group.addElement( rect );
            }
        ]]>
    </fx:Script>

    <s:layout>
        <s:VerticalLayout horizontalAlign="center" paddingTop="30" />
    </s:layout>

    <s:Group>
        <!-- Content group -->
        <s:Group id="group">
            <s:layout>
                <f4cb:CustomLayout paddingTop="5" paddingBottom="5"
                                   paddingLeft="5" paddingRight="5"
                                   horizontalAlign="center" />
            </s:layout>
        </s:Group>
        <!-- Simple border -->
        <s:Rect width="100%" height="100%">
            <s:stroke>
                <mx:SolidColorStroke color="#000000" />
            </s:stroke>
        </s:Rect>
    </s:Group>

    <s:Button label="add rect" click="addRect();" />

</s:Application>

When the s:Button control is clicked, a new, randomly sized s:Rect instance is added to the nested <s:Group> container that contains the custom layout. As each new child element is added to the container, the measure() method of the CustomLayout is invoked and the target container is resized. The updated size of the nested container is then reflected in the Rect border applied to the outer container. Although this example demonstrates resizing a Group container based on visual child elements, the same technique can be applied to a DataGroup container whose children are item renderers representing data items.

Layouts that support virtualization from the Flex 4 SDK invoke the private methods measureVirtual() or measureReal(), depending on whether the useVirtualLayout property is set to true or false, respectively, on the layout. Because the custom layout from our example does not use virtualization, child elements of the target container are accessed using the getElementAt() method of GroupBase. If virtualization is used, child elements are accessed using the getVirtualElementAt() method of GroupBase.

Dynamically Change the Child Depth in the Layout

Problem

You want to change the depth of children in a layout programmatically at runtime.

Solution

Use the depth property of child elements that implement mx.core.IVisualElement.

Discussion

You can access child elements of a GroupBase-based container via methods of the content API such as getElementAt(). When you work with the content API, each element is attributed as an implementation of ILayoutElement, which exposes attributes pertaining to constraints and methods used in determining the size and position of the element within the layout of a target container. The IVisualElement interface is an extension of ILayoutElement and is implemented by UIComponent and GraphicElement. Implementations of IVisualElement expose explicit properties for size and position, as well as attributes related to the owner and parent of the element. Visual elements from the Spark and MX component sets are extensions of UIComponent. This means elements from both architectures can be added to a Spark container, which attributes children as implementations of ILayoutElement.

To use the depth property of an element within a container, access the child using the getElementAt() method of the content API and cast the element as an IVisualElement:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx">

    <fx:Script>
        <![CDATA[
            import mx.core.IVisualElement;
            import spark.components.supportClasses.GroupBase;

            private var currentIndex:int = 0;
            private function swapDepths():void
            {
                var layoutTarget:GroupBase = bLayout.target;
                var element:IVisualElement;
                for( var i:int = 0; i < layoutTarget.numElements; i++ )
                {
                    element = layoutTarget.getElementAt( i ) as IVisualElement;
                    if( i == currentIndex )
                    {
                        element.depth = layoutTarget.numElements - 1;
                    }
                    else if( i > currentIndex )
                    {
                        element.depth = i - 1;
                    }
                }
                if( ++currentIndex > layoutTarget.numElements - 1 )
                    currentIndex = 0;
            }
        ]]>
    </fx:Script>

    <s:layout>
        <s:VerticalLayout />
    </s:layout>

    <s:Group>

        <s:layout>
            <s:BasicLayout id="bLayout" />
        </s:layout>

        <s:Button x="0" label="(1) button" />
        <s:Button x="30" label="(2) button" />
        <s:Rect x="60" width="100" height="30">
            <s:fill>
                <mx:SolidColor color="0x000000" />
            </s:fill>
        </s:Rect>
        <s:Button x="90" label="(3) button" />

    </s:Group>

    <s:Button label="swapDepths" click="swapDepths();" />

</s:Application>

In this example, the child element at the lowest depth is brought to the top of the display stack within the container when a Button control is clicked. Initially, the children of the Group container are provided depth values relative to the positions at which they are declared, with the first declared s:Button control having a depth value of 0 and the last declared s:Button control having a depth value of 3. Upon each click of the Button control, the IVisualElement residing on the lowest layer is brought to the highest layer by giving that element a depth property value of the highest child index within the container. To keep the highest depth property value within the range of the number of children, the depth values of all the other child elements are decremented.

Use Matrix3D to Apply Transformations Within a Layout

Problem

You want to apply 2D and 3D transformations to child elements of a layout.

Solution

To apply transformations that affect all children within a layout, set the individual transformation properties (such as rotationX, scaleX, and transformX) that are directly available on instances of UIComponent and GraphicElement, or supply a Matrix3D object to the layoutMatrix3D property of UIComponent-based children.

Discussion

The Flex 4 SDK offers several approaches for applying transformations to child elements of a layout. Before you apply transformations, however, you must consider how (or whether) those transformations should affect all the other child elements of the same layout. For example, setting the transformation properties for rotation, scale, and translation that are available on UIComponent-based and GraphicElement-based child elements will also affect the size and position of all other children within your layout:

<s:Button rotationZ="90" scaleY="0.5" transformX="50" />

On instances of UIComponent and GraphicElement, the rotation, scale, and transform attributes each expose properties that relate to axes in a 3D coordinate space. These properties are also bindable, so direct reapplication is not necessary when their values are modified at runtime.

Alternatively, you can use the layoutMatrix3D property to apply transformations to UIComponent-based elements; again, all children within the layout will be affected. UIComponent-based elements include visual elements from both the MX and Spark component sets. Using the layoutMatrix3D property, you can supply a flash.geom.Matrix3D object that applies all the 2D and 3D transformations to a visual element. Keep in mind, however, that the layoutMatrix3D property is write-only, so any changes you make to the Matrix3D object to which it is set will not be automatically applied to the target element. You will need to reassign the object in order to apply modifications.

The following example shows how to apply transformations within a layout using the transformation properties and the layoutMatrix3D property, which subsequently affects other children in the layout of a target container:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx">

    <fx:Script>
        <![CDATA[
            import mx.core.UIComponent;
            [Bindable] private var rot:Number = 90;
            private function rotate():void
            {
                var matrix:Matrix3D = button.getLayoutMatrix3D();
                matrix.appendRotation( 90, Vector3D.Z_AXIS );
                button.layoutMatrix3D = matrix;

                rot += 90;
            }
        ]]>
    </fx:Script>

    <s:Group>

        <s:Group width="120">

            <s:layout>
                <s:VerticalLayout paddingLeft="5" paddingRight="5"
                                  paddingTop="5" paddingBottom="5" />
            </s:layout>

            <s:Button id="button" label="push over" click="rotate();" />
            <s:Rect id="rect" rotationZ="{rot}"
                    width="110" height="50">
                <s:fill>
                    <s:SolidColor color="#000000" />
                </s:fill>
            </s:Rect>

        </s:Group>

        <s:Rect width="100%" height="100%">
            <s:stroke>
                <mx:SolidColorStroke color="#000000" />
            </s:stroke>
        </s:Rect>

    </s:Group>

</s:Application>

When the click event is received from the s:Button control, the rotate() method is invoked. Within the rotate() method, the bindable rot property is updated and the rotation value along the z-axis is updated on the GraphicElement-based Rect element. Likewise, rotation along the z-axis is updated and reapplied to the layoutMatrix3D property of the Button control. As mentioned earlier, the layoutMatrix3D property is write-only and prevents any modifications to the Matrix3D object applied from being bound to an element. As such, the Matrix3D object can be retrieved using the getLayoutMatrix3D() method and transformations can be prepended or appended using methods available on the Matrix3D class and reapplied directly to an element.

Use TransformOffsets to Apply Transformations Within a Layout

Problem

You want to apply 2D and 3D transformations to child elements of a layout without affecting other children within the layout.

Solution

Supply a TransformOffsets object to the postLayoutTransformOffsets property of instances of UIComponent and GraphicElement, or use the transformAround() method of UIComponent-based children.

Discussion

Along with the Matrix3D object, which affects all children within a layout, TransformOffsets can be used to apply transformations to specific children. When you apply transformations to a UIComponent or GraphicElement instance by supplying a TransformOffsets object as the value of the postLayoutTransformOffsets property, the layout does not automatically update the positioning and size of the other child elements within the target container. Like the Matrix3D object, TransformOffsets is a matrix of 2D and 3D values, but it differs in that it exposes those values as read/write properties. The TransformOffsets object supports event dispatching, allowing updates to properties to be applied to a child element without reapplication through binding.

To apply transformations using a TransformationOffsets object, set the postLayoutTransformOffsets property of the target element:

<s:Button label="above">
    <s:offsets>
        <mx:TransformOffsets rotationZ="90" />
    </s:offsets>
</s:Button>
<s:Button label="below" />

In contrast to how other child elements are affected when applying transformations to an element of a layout using the layoutMatrix3D property, when the s:Button control in this example is rotated 90 degrees along the z-axis, the position of the second declared s:Button control in the layout is not updated in response to the transformation.

When applying transformations to child elements, it is important to keep in mind how you want the application of the transformation to impact the layout, as this will affect whether you choose to use the layoutMatrix3D or postLayoutTransformOffsets property. If your transformations need to be applied over time and you do not want them to affect the other child elements of the layout, the postLayoutTransformOffsets property can be used in conjunction with an AnimateTransform-based effect:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx">

    <fx:Declarations>
        <s:Rotate3D id="rotator1" autoCenterTransform="true" target="{btn1}"
                    angleXFrom="0" angleXTo="360" />
        <s:Rotate3D id="rotator2" autoCenterTransform="true" target="{btn2}"
                    angleYFrom="360" angleYTo="0" />
    </fx:Declarations>

    <s:Group width="300" height="300">

        <s:Group width="100%" height="100%">

            <s:layout>
                <s:VerticalLayout paddingLeft="5" paddingRight="5"
                                  paddingTop="5" paddingBottom="5"
                                  horizontalAlign="center" />
            </s:layout>

            <s:Button id="btn1" label="(0) button" click="{rotator1.play();}" />
            <s:Button id="btn2" label="(1) button" click="{rotator2.play();}" />

        </s:Group>

        <s:Rect width="100%" height="100%">
            <s:stroke>
                <mx:SolidColorStroke color="#000000" />
            </s:stroke>
        </s:Rect>

    </s:Group>

</s:Application>

In this example, two s:Rotate3D effects are declared to apply transformations to targeted child elements over a period of time. By default, AnimateTransform-based effects apply transformations using the postLayoutTransformOffsets property of a target element, so updates to the transformation values do not affect the size and position of other child elements of the layout. This is a good strategy to use when some visual indication is needed to notify the user of an action, and you do not want to cause any unnecessary confusion by affecting the position and size of other children. If the desired effect is to apply transformations to other child elements of a layout while an animation is active, you can change the value of the applyChangesPostLayout property of the AnimateTransform class from the default of true to false.

As an alternative to using the transformation properties for rotation, scale, and translation or the layoutMatrix3D property, transformations can be applied to UIComponent-based elements using the transformAround() method. The transformAround() method has arguments for applying transformations to an element that will affect the position and size of other children within the layout, and arguments for applying transformations post-layout without affecting the other child elements.

The following example uses the transformAround() method to apply rotations around the z-axis to two elements, one that affects the layout of other children and one that does not:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx">

    <fx:Script>
        <![CDATA[
            import mx.core.UIComponent;

            private var newAngle:Number = 0;
            private function pushOver( evt:MouseEvent ):void
            {
                var angle:Number = btn1.rotationZ + 90;
                btn1.transformAround( new Vector3D( btn1.width, btn1.height / 2 ),
                                      null, new Vector3D( 0, 0, angle ) );
            }

            private function pushAround( evt:MouseEvent ):void
            {
                newAngle += 90;
                btn2.transformAround( new Vector3D( btn2.width / 2,
                                      btn2.height / 2 ),
                                      null, null, null,
                                      null, new Vector3D( 0, 0, newAngle ) );
            }
        ]]>
    </fx:Script>

    <s:Group>

        <s:Group id="group" width="120">

            <s:layout>
                <s:VerticalLayout paddingLeft="5" paddingRight="5"
                                  paddingTop="5" paddingBottom="5" />
            </s:layout>

            <s:Button id="btn1" label="push over" click="pushOver(event);" />
            <s:Button id="btn2" label="push around" click="pushAround(event);" />

        </s:Group>

        <s:Rect width="100%" height="100%">
            <s:stroke>
                <mx:SolidColorStroke color="#000000" />
            </s:stroke>
        </s:Rect>

    </s:Group>

</s:Application>

Each parameter of the transformAround() method takes a Vector3D object and all are optional aside from the first argument, which pertains to the center point for transformations. In this example, the first s:Button declared in the markup rotates in response to a click event and affects the position of the second s:Button declared. As the rotation for the first Button element is set using a Vector3D object on the rotation parameter of transformAround(), the rotationZ property of the element is updated. Within the pushAround() method, a post-layout transformation is applied to the second Button by setting the Vector3D object to the postLayoutRotation argument of transformAround(). When post-layout transformations are applied, the explicit transformation properties of the element (such as rotationZ) are not updated, and as a consequence the layout of the other children is not affected.

Though transformations can play a powerful part in notifying users of actions to be taken or that have been taken, you must consider how other children of a layout will be affected in response to those transformations.

Create a Custom 3D Layout

Problem

You want to create a custom layout that applies 3D transformations to all children of a target container.

Solution

Create a custom layout by extending com.layouts.supportClasses.LayoutBase and override the updateDisplayList() method to apply transformations to child elements accordingly.

Discussion

Along with the layout classes available in the Flex 4 SDK, you can assign custom layouts that extend LayoutBase to containers using the layout property. When the display of a target container is changed, the updateDisplayList() method of the container is invoked, which in turn invokes the updateDisplayList() method of the layout delegate. By overriding updateDisplayList() within a custom layout, you can apply transformations such as rotation, scaling, and translation to the child elements of a GroupBase-based container.

Each child element of a GroupBase container is attributed as an ILayoutElement instance, which has methods to apply 3D transformations, such as setLayoutMatrix3D() and transformAround(). To apply transformations using the utility methods of an ILayoutElement instance, access the element using the content API, as in the following example:

package com.oreilly.f4cb
{
    import flash.geom.Vector3D;

    import mx.core.IVisualElement;
    import mx.core.UIComponent;

    import spark.components.supportClasses.GroupBase;
    import spark.layouts.supportClasses.LayoutBase;

    public class Custom3DLayout extends LayoutBase
    {
        private var _focalLength:Number = 500;
        private var _scrollPosition:Number = 0;

        override public function updateDisplayList(width:Number,
                                                   height:Number) : void
        {
            super.updateDisplayList( width, height );

            var layoutTarget:GroupBase = target;
            var w:Number = width / 2;
            var h:Number = height / 2;
            var angle:Number = 360 / layoutTarget.numElements;
            var radius:Number = w;
            var radians:Number;
            var scale:Number
            var dist:Number;
            var xpos:Number = w;
            var ypos:Number;
            var element:IVisualElement;

            for( var i:int = 0; i < layoutTarget.numElements; i++ )
            {
                element = layoutTarget.getElementAt( i ) as IVisualElement;
                radians = ( ( angle * i ) + _scrollPosition ) * ( Math.PI / 180 );
                dist = w + ( Math.sin(radians) * radius );
                scale = _focalLength / ( _focalLength + dist );
                ypos = h + ( Math.cos(radians) * radius ) * scale;

                element.depth = scale;
                element.setLayoutBoundsSize( NaN, NaN );
                element.transformAround( new Vector3D((element.width / 2),
                                         (element.height / 2) ),
                                         null, null, null,
                                         new Vector3D( scale, scale, 0 ),
                                         null,
                                         new Vector3D( xpos, ypos, 0 ));
            }
        }

        public function get scrollPosition():Number
        {
            return _scrollPosition;
        }
        public function set scrollPosition( value:Number ):void
        {
            _scrollPosition = value;
            target.invalidateDisplayList();
        }
    }
}

The Custom3DLayout class in this example displays the child elements of a target container within a vertical carousel with 3D perspective. As the child elements of the container are accessed within the for loop, each instance is cast as an IVisualElement implementation (an extension of ILayoutElement) in order to set the appropriate depth value based on the derived distance from the viewer. As well, the explicit width and height properties are used to properly apply 3D translations using the transformAround() method. The first argument of the transformAround() method is a Vector3D object representing the center around which to apply transformations to the element. The following three optional arguments are Vector3D objects representing transformations that can be applied to an element that affect other children of the same layout, which in this example are attributed as a value of null. The last three arguments (also optional) are Vector3D objects representing transformations to be applied post-layout. Post-layout scale and translation transformations applied to an element do not affect the position and size of other children.

A scrollPosition property has been added to Custom3DLayout to allow for scrolling through the carousel of elements. As the scrollPosition value changes, the invalidateDisplayList() method of the target container is invoked, which in turn invokes the updateDisplayList() method of the custom layout delegate.

The following example applies the Custom3DLayout to a DataGroup container:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx"
               xmlns:f4cb="com.oreilly.f4cb.*">

    <s:layout>
        <s:HorizontalLayout paddingTop="10" paddingLeft="10" />
    </s:layout>

    <s:Group width="300" height="300">

        <s:DataGroup width="100%" height="100%"
                     itemRenderer="spark.skins.spark.DefaultComplexItemRenderer">

            <s:layout>
                <f4cb:Custom3DLayout scrollPosition="{slider.value}" />
            </s:layout>

            <s:dataProvider>
                <s:ArrayCollection>
                    <s:Button label="Lorem" />
                    <s:Button label="ipsum" />
                    <s:Button label="dolar" />
                    <s:Button label="sit" />
                    <s:Button label="amet" />
                    <s:Button label="consectetur" />
                </s:ArrayCollection>
            </s:dataProvider>

        </s:DataGroup>

        <s:Rect width="100%" height="100%">
            <s:stroke>
                <s:SolidColorStroke color="#000000" />
            </s:stroke>
        </s:Rect>

    </s:Group>

    <s:VSlider id="slider" height="300" liveDragging="true"
               minimum="0" maximum="360" />

</s:Application>

As the value of the s:VSlider control changes, the scrollPosition of the custom layout delegate is modified and the transformations on child elements are updated to show a smooth scrolling carousel with perspective.

Programmatically Scroll Within a Layout

Problem

You want to programmatically set the scroll position within a container.

Solution

Use the horizontalScrollPosition and verticalScrollPosition properties of a LayoutBase-based layout.

Discussion

When a fixed size is applied to a container and the clipAndEnableScrolling property is set to a value of true, the rendering of child elements is confined to the dimensions of the container. If the position of a child element is determined as being outside of the parent container’s bounds, the layout does not display that child within the container. Containers that implement the IViewport interface—as GroupBase-based containers do—can be wrapped in a Scroller component, and scroll bars will automatically be displayed based on the contentWidth and contentHeight of the viewport.

Because, unlike MX containers, Spark containers do not inherently support adding scroll bars to their display, programmatically scrolling the content of a viewport is supported by updating the horizontalScrollPosition and verticalScrollPosition properties of a layout. In fact, that is how a container internally determines its scroll position: by requesting the scroll values of its layout.

As shown in the following example, a container viewport can be scrolled programmatically by using another value-based component:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx">

    <fx:Declarations>
        <fx:String id="txt">
            Lorem ipsum dolor sit amet consectetur adipisicing elit.
        </fx:String>
    </fx:Declarations>

    <s:layout>
        <s:VerticalLayout horizontalAlign="center" />
    </s:layout>

    <s:HSlider id="slider"
               minimum="0"
               maximum="{group.contentHeight - group.height}"
               liveDragging="true" />

    <s:DataGroup id="group" width="100" height="100"
        clipAndEnableScrolling="true"
        itemRenderer="spark.skins.spark.DefaultItemRenderer">

        <s:layout>
            <s:VerticalLayout id="vLayout"
                              verticalScrollPosition="{slider.value}" />
        </s:layout>

        <s:dataProvider>
            <mx:ArrayCollection source="{txt.split(' ')}" />
        </s:dataProvider>

    </s:DataGroup>

</s:Application>

The maximum scroll value for the s:HSlider control is determined by subtracting the value of the container’s height property from its contentHeight property value. The contentHeight property is an attribute of the IViewport interface, which all GroupBase-based containers implement. The verticalScrollPosition of the container’s layout delegate is bound to the value of the HSlider control, in turn updating the rendered view within the viewport of the container. As the value increases, child elements that previously resided below the viewport are rendered in the layout. As the value decreases, child elements that previously resided above the viewport are rendered.

Because the scroll position in the previous example is updated prior to the rendering of child elements, the layout can employ virtualization easily. However, determining the scroll position of a virtualized layout based on the size of child elements involves accessing the virtual child elements of a container directly.

The following example demonstrates how to programmatically use virtualized elemental scrolling:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx">

    <fx:Library>
        <fx:Definition name="MyRect">
            <s:Rect width="100">
                <s:fill>
                    <mx:SolidColor color="#000000" />
                </s:fill>
            </s:Rect>
        </fx:Definition>
    </fx:Library>

    <fx:Script>
        <![CDATA[
            import mx.core.IVisualElement;
            import spark.core.NavigationUnit;

            private var elementHeight:Vector.<Number> = new Vector.<Number>();
            private var currentIndex:int;
            private function handleScroll( unit:uint ):void
            {
                currentIndex = (unit == NavigationUnit.UP)
                               ? currentIndex - 1
                               : currentIndex + 1;
                currentIndex = Math.max( 0, Math.min( currentIndex,
                               group.numElements - 1 ) );
                var element:IVisualElement;
                var ypos:Number = 0;
                for( var i:int = 0; i < currentIndex; i++ )
                {
                    element = group.getVirtualElementAt( i );
                    if( element != null )
                    {
                        elementHeight[i] = element.getPreferredBoundsHeight();
                    }
                    ypos += elementHeight[i];
                }
                ypos += vLayout.paddingTop;
                ypos += vLayout.gap * currentIndex;
                vLayout.verticalScrollPosition = ypos;
            }
        ]]>
    </fx:Script>

    <s:layout>
        <s:VerticalLayout horizontalAlign="center" />
    </s:layout>

    <s:DataGroup id="group" width="100" height="100"
                 clipAndEnableScrolling="true"
                 itemRenderer="spark.skins.spark.DefaultComplexItemRenderer">

        <s:layout>
            <s:VerticalLayout id="vLayout" useVirtualLayout="true" />
        </s:layout>

        <s:dataProvider>
            <mx:ArrayCollection>
                <fx:MyRect height="30" />
                <s:DropDownList height="20" />
                <fx:MyRect height="50" />
                <s:Button height="80" />
                <fx:MyRect height="40" />
            </mx:ArrayCollection>
        </s:dataProvider>

    </s:DataGroup>

    <s:Button label="up" click="handleScroll(NavigationUnit.UP)" />
    <s:Button label="down" click="handleScroll(NavigationUnit.DOWN)" />

</s:Application>

The elemental index on which to base the vertical scroll position of the container viewport is determined by a click event dispatched from either of the two declared s:Button controls. As the currentIndex value is updated, the position is determined by the stored height values of child elements retrieved from the getVirtualElementAt() method of the GroupBase target container.

See Also

the section called “Enable Scrolling in a Container”

Determine the Visibility of Elements in a Sequence-Based Layout

Problem

You want to determine the visibility of an element within a container with a sequence-based layout delegate and possibly scroll the element into view.

Solution

Use the fractionOfElementInView() method of a sequence-based layout such as VerticalLayout or HorizontalLayout to determine the visibility percentage value of an element within the container’s viewport and set the container’s scroll position based on the corresponding coordinate offset value returned from the getScrollPositionDeltaToElement() method of a LayoutBase-based layout.

Discussion

Sequence-based layouts available in the Flex 4 SDK, such as VerticalLayout and HorizontalLayout, have convenience properties and methods for determining the visibility of an element within the viewport of its parent container. The fractionOfElementInView() method returns a percentage value related to the visibility of an element within a range of 0 to 1. A value of 0 means the element is not present in the view, while a value of 1 means the element is completely in view. The argument value for the fractionOfElementInView() method is the elemental index of an element in the container’s display list. This index is used, along with the firstIndexInView and lastIndexInView convenience properties, to determine the visibility of the element; you can also use it to determine whether to update a container’s scroll position and if so, by how much.

If it is determined that the element needs to be scrolled into view, the scroll position of the container viewport can be updated programmatically using the verticalScrollPosition and horizontalScrollPosition properties, as in the following example:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx">

    <fx:Script>
        <![CDATA[
            import spark.layouts.VerticalLayout;
            import mx.core.IVisualElement;

            private function scrollTo( index:int ):void
            {
                var amt:Number = (group.layout as
                                  VerticalLayout).fractionOfElementInView(index);
                if( amt < 1.0 )
                {
                    var pt:Point = group.layout.getScrollPositionDeltaToElement(
                                   index );
                    if( pt != null ) group.verticalScrollPosition += pt.y;
                    // else already in view
                }
                // else already fully in view
            }

            private function getRandomInRange( min:int, max:int ):int
            {
                return ( Math.floor( Math.random() * (max - min + 1) ) + min );
            }

            private function handleClick( evt:MouseEvent ):void
            {
                var item:IVisualElement = evt.target as IVisualElement;
                var index:Number = group.getElementIndex( item );
                scrollTo( index );
            }

            private function handleRandomScroll():void
            {
                var index:int = getRandomInRange( 0, 9 );
                scrollTo( index );
                scrollToField.text = (index+1).toString();
            }
        ]]>
    </fx:Script>

    <s:layout>
        <s:VerticalLayout />
    </s:layout>

    <s:Scroller width="200" height="100">
        <s:VGroup id="group" horizontalAlign="justify">
            <s:Button label="button (1)" click="handleClick(event)" />
            <s:Button label="button (2)" click="handleClick(event)" />
            <s:Button label="button (3)" click="handleClick(event)" />
            <s:Button label="button (4)" click="handleClick(event)" />
            <s:Button label="button (5)" click="handleClick(event)" />
            <s:Button label="button (6)" click="handleClick(event)" />
            <s:Button label="button (7)" click="handleClick(event)" />
            <s:Button label="button (8)" click="handleClick(event)" />
            <s:Button label="button (9)" click="handleClick(event)" />
            <s:Button label="button (10)" click="handleClick(event)" />
        </s:VGroup>
    </s:Scroller>

    <s:HGroup verticalAlign="middle">
        <s:Button label="random scroll to:" click="handleRandomScroll()" />
        <s:Label id="scrollToField" text="1" />
    </s:HGroup>

</s:Application>

Each element in the layout of the <s:VGroup> container is assigned a handler for a click event, which determines the elemental index of that item using the getElementIndex() method of the content API. This example also provides the ability to randomly scroll to an element within the container using the handleRandomScroll() method, which (similar to the handleClick() event handler) hands an elemental index to the scrollTo() method to determine the visibility percentage of that element within the container viewport. If the element is not fully in view, it is scrolled into view using the getScrollPositionDeltaToElement() method of a LayoutBase-based layout. This method returns a Point object with position values that indicate the element’s offset from the container viewport (i.e., how far to scroll to make it completely visible). If the return value is null, either the elemental index lies outside of the display list or the element at that index is already fully in view.

The display list indexes of the elements visible in the container viewport can also be determined using the firstIndexInView and lastIndexInView convenience properties of a sequence-based layout, as in the following snippet:

private function scrollTo( index:int ):void
{
    var amt:Number = ( group.layout as VerticalLayout ).fractionOfElementInView(
                     index );
    if( amt < 1.0 )
    {
        var pt:Point = group.layout.getScrollPositionDeltaToElement( index );
        if( pt != null ) group.verticalScrollPosition += pt.y;
        // else already in view
    }
    // else already fully in view
    trace( "firstIndex: " + ( group.layout as VerticalLayout ).firstIndexInView );
    trace( "lastIndex: " + ( group.layout as VerticalLayout ).lastIndexInView );
}

Upon a change to the scroll position of the layout delegate assigned to a container, the read-only firstIndexInView and lastIndexInView properties of a sequence-based layout are updated and a bindable indexInViewChanged event is dispatched.

If you enjoyed this excerpt, buy a copy of Flex 4 Cookbook.

Copyright © 2009 O'Reilly Media, Inc.