Layout: Chapter 3 - Flex 4 Cookbook
by Joshua Noble, Todd Anderson, Marco Casario, Garth BraithwaiteThis 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.
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.
You want to control the layout of children in a container, positioning them either horizontally or vertically.
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.
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>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.
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>Assign TileLayout to the
layout property of a Spark container
to dynamically place its child elements in a grid.
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.
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.
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.
You want to improve runtime performance by creating and rendering children of a container as needed.
Use the useVirtualLayout
property of a HorizontalLayout,
VerticalLayout, or
TileLayout
whose target container is a DataGroup.
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 by extending the com.layouts.supportClasses.LayoutBase class
and override the updateDisplayList()
method to position and size the children accordingly.
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.
You want to set the size of a target container based on the dimensions of the child elements in the layout.
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.
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.
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.
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.
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.
You want to apply 2D and 3D transformations to child elements of a layout without affecting other children within the layout.
Supply a TransformOffsets
object to the postLayoutTransformOffsets property of instances of UIComponent and GraphicElement, or use the transformAround() method of UIComponent-based children.
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
object as the value of the TransformOffsetspostLayoutTransformOffsets 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.
You want to create a custom layout that applies 3D transformations to all children of a target container.
Create a custom layout by extending com.layouts.supportClasses.LayoutBase and
override the updateDisplayList()
method to apply transformations to child elements accordingly.
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.
Use the horizontalScrollPosition and verticalScrollPosition properties of a
LayoutBase-based layout.
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.
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.
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.
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.
