Containers: Chapter 2 - 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.
The Flex Framework provides two sets of containers: MX and Spark. The
introduction of the Spark architecture to the Flex 4 SDK offers a level of
abstraction between containers and layouts not available in the MX
architecture. An MX container internally manages the size and position of
its children based on specified properties and styles. To modify the layout
rules of a MX container, you often create a subclass of a similar container
or the base mx.core.Container class and
override methods such as updateDisplayList() and measure(). In contrast, Spark containers are
separated from layout management and allow for a developer to specify
layouts available in the spark.layouts
package or create custom LayoutBase-based
layouts to manage the size and position of child elements. The separation of
responsibilities for Spark containers provides enhanced runtime performance
in rendering because layout is handled through delegation.
Included in the Spark layout container architecture are two base
classes, GroupBase and SkinnableContainerBase. Both handle visual
elements, but only the latter provides skinning capabilities for the
container. The spark.components.Group and
spark.components.DataGroup classes are
extensions of GroupBase; both are
non-skinnable containers, but they differ in how child elements are declared
and represented on their display lists. A Group container holds children that are
implementations of IVisualElement and
that are added either through MXML markup or through methods of the content
API. A DataGroup container uses item
rendering for visual elements represented as data items, which are provided
as IList implementations. DataGroup also supports virtualization through a
layout delegate, which can reduce the rendering time of its visual elements
at runtime. spark.components.SkinnableContainer (of which
Application is a subclass) and spark.components.SkinnableDataContainer are
equivalent to the GroupBase-based
containers yet support skinning for the visual makeup of the container
itself.
Layout containers—those that handle the size and position of their
child elements—have equal parity between the MX and Spark container sets.
Though using Spark layout containers is recommended because of their
improved performance and level of abstraction, there are no Spark
equivalents of the MX navigational containers (such as Accordion and ViewStack). If application requirements dictate
the use of navigational containers, the Flex Framework allows for
intermixing of MX and Spark containers in the same application. Note,
however, that a Spark container cannot be added directly as a child to a MX
navigator container. To add a Spark layout container to a MX navigator
container, you must wrap the Spark container in a spark.components.NavigatorContent container
instance.
Assign a layout from the spark.layouts package to the layout property of the target container and
apply constraints on the layout and container as necessary.
Spark layout containers, such as spark.components.Group and spark.components.DataGroup, delegate the
sizing and positioning of their child elements to LayoutBase-based layouts. As child elements
are added to a container, whether declaratively in MXML or
programmatically, the management of the display list is handed over to
the layout, which renders child elements accordingly. Common layouts,
such as those for positioning children vertically or horizontally, are
provided in the Flex 4 SDK in the spark.layouts package. Due to the separation
of responsibilities between Spark containers and layouts, custom
LayoutBase-based layouts can also
easily be applied to containers.
The default layout of the base Spark containers is spark.layouts.BasicLayout. When the default
BasicLayout is applied to a
container, child elements are displayed based on their individual
properties, without regard to the size and position of other
children:
<s:Group width="300" height="300">
<s:Button label="button (1)" x="10" y="10" />
<s:Button label="button (2)" x="10" y="40" />
</s:Group>Using BasicLayout affords
developers fine-grained control over the layout of child elements by enabling them to specify each
element’s size and position. Additional constraint properties can be
applied to the container as well, to uniformly offset child positions.
The child elements of the s:Group
container in the following example are displayed exactly as in the
previous example, yet the top,
left, right, and bottom constraint properties are
used:
<s:Group width="300" height="300"
top="10" left="10" right="10" bottom="10">
<s:Button label="button (1)" />
<s:Button label="button (2)" y="30" />
</s:Group>The HorizontalLayout, VerticalLayout, and TileLayout classes are available to display
children sequentially and can be applied using the layout property:
<s:Group width="300" height="300"
top="10" left="10" right="10" bottom="10">
<s:layout>
<s:VerticalLayout gap="10" />
</s:layout>
<s:Button label="button (1)" />
<s:Button label="button (2)" />
</s:Group>Similar to using constraint properties on a container, distances
between the border of the container and the child elements can be
specified using the paddingTop,
paddingLeft,
paddingRight, and paddingBottom properties of the sequential
layout classes of the SDK:
<s:Group width="300" height="300">
<s:layout>
<s:VerticalLayout gap="10" paddingTop="10" paddingLeft="10"
paddingRight="10" paddingBottom="10" />
</s:layout>
<s:Button label="button (1)" />
<s:Button label="button (2)" />
</s:Group>Some convenience classes are available in the Flex 4 SDK to
declare containers with predefined layouts. For example, the children of
spark.components.HGroup, spark.components.VGroup, and spark.components.TileGroup containers are laid
out sequentially in a predetermined manner. You declare these containers
just as you would any other, but the layout property is attributed as
read-only:
<s:VGroup width="300" height="300" gap="10"
paddingTop="10" paddingLeft="10"
paddingRight="10" paddingBottom="10">
<s:Button label="button (1)" />
<s:Button label="button (2)" />
</s:VGroup>There are many different ways to achieve a desired layout. For instance, all the examples in this recipe will display the same way, although they differ in approach. It is important to remember that containers and layout within the Spark architecture are decoupled, affording you more freedom as to how child elements are visually presented.
Use the addElement(), addElementAt(), removeElement(), and removeElementAt() methods of an IVisualElementContainer
implementation.
A layout assigned to a Spark container attributes its children as
instances of IVisualElement when
managing their size and position. Visual components from both the Spark
and MX packages are implementations of mx.core.IVisualElement, so you can add MX
controls that do not have a Spark equivalent to a Spark layout
container. GraphicElement-based
elements also implement IVisualElement and afford you a rich set of
visual elements to display within a container.
Along with declaring child elements in markup, you can
programmatically add children to a
container using the addElement() and
addElementAt() methods of an IVisualElementContainer
implementation. The addElement()
method adds a child element to the content layer at an elemental index
one higher than any previously assigned. Using addElementAt(), you can set the exact
elemental location within the display list. When you add an element
using the content API’s add methods, the element’s position depends on
the layout delegate specified for the container. The spark.components.Group and spark.components.SkinnableContainer (of which
the Spark Application container is a
subclass) containers are implementations of IVisualElementContainer.
To add a child at the next available elemental index within the
display, use the addElement() method as shown here:
<s:Button label="add" click="{myContent.addElement( new Button() );}" />
<s:Group id="myContent">
<s:layout>
<s:VerticalLayout />
</s:layout>
</s:Group>The click event of the s:Button control triggers the addition of a
new Button child element to the
targeted container. Depending on the layout applied to a container, the
addition and removal of child elements may or may not affect the
position of other child elements. In this example, as each new child
element is added, it is offset vertically from the last child element.
If BasicLayout were assigned to the
container, each additional child element would be presented at a higher
z-order within the display.
Using the addElementAt()
method, you can set the desired position for the new child within the
display list:
<s:Button label="add" click="{myContent.addElementAt( new Button(), 0 );}" />
<s:Group id="myContent">
<s:layout>
<s:VerticalLayout />
</s:layout>
</s:Group>In this example, as each new Button is added to the container, the control
is placed at the elemental index 0. Because VerticalLayout is assigned to the container,
each new child added will be placed at the top of the content display,
pushing the y-positions of any other child elements
downward.
To remove a child element at a specific index, use the removeElementAt() method:
myContent.removeElementAt(0);
Children can also be removed using the removeElement() method, which takes the
reference name of a child element as its argument:
myContent.removeElement(myElement);
If the reference name is not available, you can use the getElementAt() method of the content API and
specify the index of the child element you wish to remove. The following
example uses the getElementAt()
method to remove the first element from the container’s display
list:
myContent.removeElement( myContent.getElementAt( 0 ) );
The Group and SkinnableContainer layout container classes
also support the removal of all child elements using the removeAllElements() method.
As children are added to and removed from a container, the
numChildren property of the container
is updated. This can help in effectively using the methods of the
content API exposed by implementations of IVisualElementContainer.
The following example demonstrates the possible ways to programmatically add child elements to and remove them from a 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.IVisualElement;
import spark.components.Button;
private function getNewElement():IVisualElement
{
var btn:spark.components.Button = new spark.components.Button();
btn.label = "button " + myContent.numElements;
return btn;
}
private function addFirstElement():void
{
myContent.addElementAt( getNewElement(), 0 );
}
private function addLastElement():void
{
myContent.addElement( getNewElement() );
}
private function removeFirstElement():void
{
if( myContent.numElements > 0 )
myContent.removeElement( myContent.getElementAt( 0 ) );
}
private function removeLastElement():void
{
if( myContent.numElements > 0 )
myContent.removeElementAt( myContent.numElements - 1 );
}
]]>
</fx:Script>
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:Button label="addFirst" click="addFirstElement();" />
<s:Button label="addLast" click="addLastElement();" />
<s:Button label="removeFirst" click="removeFirstElement()" />
<s:Button label="removeLast" click="removeLastElement()" />
<s:Button label="removeAll" click="myContent.removeAllElements()" />
<s:Group id="myContent">
<s:layout>
<s:VerticalLayout />
</s:layout>
</s:Group>
</s:Application>You want to dynamically reorder the index positions of child elements within a container at runtime.
Use the setElementIndex()
method to change the elemental index of an individual child element, or
use the swapElements() and swapElementsAt() methods to transpose the
index positions of two children in an IVisualElementContainer implementation, such
as a Group or SkinnableContainer. The elemental index
corresponds to the order in which a child element is rendered in a
layout.
As child elements are added to a container, whether
programmatically or declaratively in MXML, references to those elements
are stored within an Array. The
container’s layout delegate uses that Array to display children. A child element can
be accessed from this display list using its elemental index, and the
order in which the children are displayed can be manipulated by changing
these indexes.
In the following example, a child element at the lowest index
within the display list is promoted to the
highest index using the setElementIndex() method of an
IVisualElementContainer
implementation, consequentially updating the layout of the other
children in the HorizontalLayout:
<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[
private function reorder():void
{
myContent.setElementIndex( myContent.getElementAt(0),
myContent.numElements - 1 );
}
]]>
</fx:Script>
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:Group id="myContent">
<s:layout>
<s:HorizontalLayout />
</s:layout>
<s:Button id="btn1" label="button 1" />
<s:DropDownList />
<s:Button id="btn2" label="button 2" />
</s:Group>
<s:Button label="reorder" click="reorder();" />
</s:Application>The reference to the first element within the display list of the
s:Group is accessed using the
getElementAt() method of the content
API. The child element is moved from the front of the list to the end,
and the order in which all child elements are rendered is updated on the
layout.
The rendering order can also be manipulated at runtime using the
swapElements() method, which takes
two references to IVisualElement
implementations:
myContent.swapElements( btn1, btn2 );
The swapElementsAt() method
swaps the indexes of two elements of a visual container based on the
specified index positions:
myContent.swapElements( 0, 2 );
Although a layout will render children sequentially from the list,
do not confuse the elemental index of a child with its depth property when considering display. The
depth property of an IVisualElement implementation represents the
layer position of the child within the layout. Its value is not tied to
the length of the child display list, and multiple children can share
the same depth. Manipulating the depths therefore differs from
manipulating the indexes of children within the display list, in that
assigning an index that has already been assigned or is out of the
current range (from 0 to the highest assigned index + 1) will throw a
runtime exception.
The elemental index and the depth property of a child element may not
visually affect the layout of children when using sequential layouts,
such as HorizontalLayout and VerticalLayout, but it’s important to
understand the concepts of rendering order and layer depth when using
BasicLayout or a custom layout. To
help you understand how the depth
property of an element affects the layout, the following example
displays the first declared s:Button
control on a layer above the second declared s:Button:
<s:Group id="myContent">
<s:Button depth="100" label="button 1" />
<s:Button label="button 2" />
</s:Group>Although the elemental index of the second s:Button control in the display list is
greater than that of the first, the first s:Button is rendered before the second and
placed on a higher layer within the default BasicLayout due to the assigned depth property value. The default value of the
depth property is 0.
You want to supply to a container an array of data items to be visually represented using item renderers.
Use the DataGroup container,
and set the dataProvider property to
an IList implementation and the
itemRenderer property to the
qualified class name of an IDataRenderer implementation.
The DataGroup layout container
utilizes item rendering for visual elements represented as data items.
Unlike with the Group container,
which handles visual elements declared directly in MXML or through
methods of the content API, an IList
implementation (mx.collections.ArrayCollection or mx.collections.XMLListCollection, for example)
is supplied to a DataGroup container,
and an item renderer handles the visual representation of each data item
in the collection. Included in the Flex 4 SDK are two convenient item
renderer classes that can be used to easily present data items visually:
spark.skins.DefaultItemRenderer
presents data textually, while spark.skins.DefaultComplexItemRenderer renders
data items that are implementations of IVisualElement, such as the components from
the Spark and MX sets.
In the following example, an array of textual data is supplied to
a DataGroup and rendered using the
DefaultItemRenderer:
<fx:Declarations>
<fx:String id="txt">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</fx:String>
</fx:Declarations>
<s:DataGroup itemRenderer="spark.skins.spark.DefaultItemRenderer">
<s:layout>
<s:HorizontalLayout />
</s:layout>
<s:dataProvider>
<s:ArrayCollection source="{txt.split(' ')}" />
</s:dataProvider>
</s:DataGroup>The data items in the dataProvider of the DataGroup are positioned horizontally from
left to right and rendered using the supplied itemRenderer. The value of the
itemRenderer
property, a qualified class name, is used internally by the DataGroup container to create a new instance
of the specified class for each data item in the collection. If the
class is an IDataRenderer
implementation, the data property of
the implementation is updated with the item within the collection at the
time of instantiation.
The DefaultComplexItemRenderer
class can also be used to easily render IVisualElement data within a DataGroup container:
<fx:Declarations>
<fx:String id="txt">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</fx:String>
</fx:Declarations>
<s:DataGroup itemRenderer="spark.skins.spark.DefaultComplexItemRenderer">
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:dataProvider>
<s:ArrayCollection>
<s:Label text="Using DefaultComplexItemRenderer." />
<s:Button label="button 1" />
<s:DropDownList dataProvider="{new ArrayCollection(txt.split(' '))}" />
<s:CheckBox selected="true" />
<mx:Button label="button 2" />
</s:ArrayCollection>
</s:dataProvider>
</s:DataGroup>When the itemRenderer property
is set to the DefaultComplexItemRenderer class, the DataGroup internally determines each data item
to be an IVisualElement
implementation and renders the items directly on the display.
Unlike from a Group container,
child elements cannot be accessed directly from a DataGroup container. Although the child
elements of both a Group and a
DataGroup are IVisualElement implementations, the Group class exposes a content API through its
implementation of the IVisualElementContainer interface that enables
you to dynamically add, remove, and set the indexes of elements directly
in the container. The display list of a DataGroup can be altered using the IList instance set as the dataProvider property value for the container.
Because the dataProvider property
supports binding, the collection can be affected directly at runtime to
update the display of visual elements dynamically.
As item renderers are added to and removed from the display list
of a DataGroup container, RendererExistenceEvent objects are
dispatched. The properties of a RendererExistenceEvent instance
correspond to the item renderer instance, the data supplied to the item renderer, and the
elemental index within the display list at which it resides.
The following example demonstrates how to dynamically change the
display list of a DataGroup container
and listen for the addition and removal of item renderer
instances:
<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.events.RendererExistenceEvent;
import mx.collections.ArrayCollection;
private function itemAdded( evt:RendererExistenceEvent ):void
{
trace( "Item Added: " + evt.index + " : " + evt.data +
" : " + evt.renderer );
}
private function itemRemoved( evt:RendererExistenceEvent ):void
{
trace( "Item Removed: " + evt.index + " : " + evt.data +
" : " + evt.renderer );
}
private function addItem():void
{
if( collection.length > 0 )
myContent.dataProvider.addItem( collection.removeItemAt(0) );
}
private function removeItem():void
{
if( myContent.dataProvider.length > 0 )
{
var item:Object = myContent.dataProvider.removeItemAt(
myContent.dataProvider.length - 1 );
collection.addItem( item );
}
}
]]>
</fx:Script>
<fx:Declarations>
<fx:String id="txt">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</fx:String>
<s:ArrayCollection id="collection">
<s:Label text="Using DefaultComplexItemRenderer." />
<s:Button label="button 1" />
<s:DropDownList dataProvider="{new ArrayCollection(txt.split(' '))}" />
<s:CheckBox selected="true" />
<mx:Button label="button 2" />
</s:ArrayCollection>
</fx:Declarations>
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:DataGroup id="myContent"
rendererAdd="itemAdded(event);"
rendererRemove="itemRemoved(event);"
itemRenderer="spark.skins.spark.DefaultComplexItemRenderer">
<s:layout>
<s:HorizontalLayout />
</s:layout>
<s:dataProvider>
<s:ArrayCollection />
</s:dataProvider>
</s:DataGroup>
<s:Button label="add" click="addItem();" />
<s:Button label="remove" click="removeItem();" />
</s:Application>Create a custom component that implements the IVisualElement and IDataRenderer interfaces and supply the class
as the itemRenderer property value
for a DataGroup container.
The DataGroup container handles
making visual representations of data items. In order to properly render
visual elements in the display list of a DataGroup and for its layout delegate to
handle the size and position of those children, item renderers need to
implement at least two interfaces: IVisualElement and IDataRenderer. The layout delegate of a
container attributes the visual elements as IVisualElement implementations. The
implementation type is also attributed when dispatching RendererExistenceEvents. Implementing the
IDataRenderer interface for a custom
item renderer exposes the data
property, which is used internally by the DataGroup to supply data items to the
renderer.
Along with the convenient DefaultItemRenderer and DefaultComplexItemRenderer classes provided in
the Flex 4 SDK, Adobe provides a convenience base class for item
renderers to be used with DataGroup
containers. The item renderer base class—aptly named ItemRenderer—is an extension of spark.components.DataRenderer, which is a
Group container that exposes a
data property, fulfilling the
contract of an item renderer being an implementation of IVisualElement and IDataRenderer. In addition, spark.components.supportClasses.ItemRenderer
also provides extra support for styling, states, and event handling and
is a good jumping-off point for creating a custom item
renderer.
The following custom item renderer is an extension of ItemRenderer and displays the firstName and lastName property values of the supplied data
item:
<s:ItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
width="100%" height="24">
<s:states>
<s:State name="normal" />
<s:State name="hovered" />
</s:states>
<s:Rect width="100%" height="100%">
<s:fill>
<s:SolidColor color="0xDDDDDD" color.hovered="0xDDDDFF" />
</s:fill>
</s:Rect>
<s:Group width="100%" height="100%">
<s:layout>
<s:HorizontalLayout verticalAlign="middle" />
</s:layout>
<s:Label text="{data.lastName}," />
<s:Label text="{data.firstName}" />
</s:Group>
</s:ItemRenderer>Because ItemRenderer is an
extension of Group, visual elements
can be declared directly on the display for visual representation. In
this example, two s:Label components
are laid out horizontally and placed above a background s:Rect graphic element that updates its color property based on state. When creating a
custom item renderer by extending the ItemRenderer class, remember that the item
renderer manages its current state internally. You can override the
getCurrentRendererState() method to
specify how the current state is determined, but in general the
extending class will need to at least declare a normal state.
In this example, the background color is updated in response to
the current state using dot notation for color property values inline. The CustomItemRenderer created in the previous
example is applied to a DataGroup
using the itemRenderer property to
present visual representations of data from an IList collection:
<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[
private var authors:XML =
<authors>
<author>
<firstName>Josh</firstName>
<lastName>Noble</lastName>
</author>
<author>
<firstName>Garth</firstName>
<lastName>Braithwaite</lastName>
</author>
<author>
<firstName>Todd</firstName>
<lastName>Anderson</lastName>
</author>
</authors>;
]]>
</fx:Script>
<s:DataGroup width="200"
itemRenderer="com.oreilly.f4cb.CustomItemRenderer">
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:dataProvider>
<s:XMLListCollection source="{authors..author}" />
</s:dataProvider>
</s:DataGroup>
</s:Application>As the collection of XML data is
iterated through, a new instance of
CustomItemRenderer is created and its data property is attributed to the current
data item in the iteration.
If a namespace is declared for the package where a custom item
renderer resides, the itemRenderer
property of the DataGroup container
can also be set using MXML markup, as in the following
example:
<s:DataGroup width="200">
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:dataProvider>
<s:XMLListCollection source="{authors..author}" />
</s:dataProvider>
<s:itemRenderer>
<fx:Component>
<f4cb:CustomItemRenderer />
</fx:Component>
</s:itemRenderer>
</s:DataGroup>You want to use more than one type of item renderer for a
collection of data items in a DataGroup container.
Use the itemRendererFunction
property to specify a callback method that will return an item renderer
based on the data item.
When working with a collection of data items in a DataGroup container, the itemRenderer and itemRendererFunction properties are used to
specify the visual representation of elements. Using the itemRenderer property, instances of a single
item renderer class are created and optionally recycled when rendering
data elements. Assigning a factory method to the itemRendererFunction property of the DataGroup affords more control over the type
of item renderer used for a data element.
The return type of the method specified for the itemRendererFunction property is IFactory. As item
renderers are created for data elements internally within the DataGroup, item renderers are instantiated
using the newInstance() method of the
IFactory implementation that is
returned. The following is an example of a method attributed as an
itemRendererFunction for a DataGroup container:
private function getItemRenderer( item:Object ):IFactory
{
var clazz:Class;
switch( item.type )
{
case "normal":
clazz = NormalRenderer;
break;
case "special":
clazz = SpecialRenderer;
break;
}
return new ClassFactory( clazz );
}Depending on the type of data element provided to the itemRendererFunction, a specific item renderer
is returned. Any method specified as an itemRendererFunction must adhere to the method
signature shown in this example. The single argument, of type Object, is the current data element within the
collection being iterated through and is attributed as the dataProvider property of the DataGroup. The return type, as mentioned
previously, is of type IFactory. The
ClassFactory class is an
implementation of IFactory, and class declarations are
passed to its constructor and used to instantiate new instances of the
class when the newInstance() method
is invoked.
The following example demonstrates setting a method delegate for
the itemRendererFunction property of
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">
<fx:Declarations>
<s:ArrayCollection id="animalList">
<fx:Object type="dog" name="Walter" />
<fx:Object type="cat" name="Zoe" />
<fx:Object type="dog" name="Cayuga" />
<fx:Object type="dog" name="Pepper" />
</s:ArrayCollection>
</fx:Declarations>
<fx:Script>
<![CDATA[
import com.oreilly.f4cb.DogItemRenderer;
import com.oreilly.f4cb.CatItemRenderer;
import spark.skins.spark.DefaultItemRenderer;
private function getItemRenderer( item:Object ):IFactory
{
var clazz:Class;
switch( item.type )
{
case "dog":
clazz = DogItemRenderer;
break;
case "cat":
clazz = CatItemRenderer;
break; default:
clazz = DefaultItemRenderer;
break;
}
return new ClassFactory( clazz );
}
]]>
</fx:Script>
<s:DataGroup width="200" height="150"
dataProvider="{animalList}"
itemRendererFunction="getItemRenderer">
<s:layout>
<s:VerticalLayout paddingTop="5" paddingBottom="5"
paddingLeft="5" paddingRight="5" />
</s:layout>
</s:DataGroup>
<s:Rect width="200" height="150">
<s:stroke>
<s:SolidColorStroke color="#000000" />
</s:stroke>
</s:Rect>
</s:Application>As the items of the dataProvider collection are iterated through,
the getItemRenderer() factory method is invoked and,
based on the type property of the
passed-in data item, a custom item renderer is returned. If the data
type is not found, the spark.skins.spark.DefaultItemRenderer class of
the Flex 4 SDK is returned.
It is important to note that, though a DataGroup can support virtualization through
its layout delegate, an item renderer returned using itemRendererFunction is instantiated each time
the delegate method is invoked and not inherently recycled as elements
are added to and removed from the display list.
You want to add scrolling capabilities to a container whose child elements are positioned outside of the defined viewport bounds.
Wrap a Group or DataGroup container in an instance of spark.components.Scroller and define the
dimensions and clipAndEnableScrolling
property of the container, or assign a container as the viewport property of a spark.components.ScrollBar instance.
Unlike MX containers, which support scrolling internally, the
separation of responsibilities within the Spark architecture provides
more lightweight containers and affords more control over delegating
tasks. Within the new Spark paradigm, you can assign specific controls that handle navigating within a
container. The spark.components.supportClasses.GroupBase
class, which both DataGroup and
Group extend, is an implementation of
the IViewport interface. By default,
the clipAndEnableScrolling property
of a GroupBase-based container is set
to false and the container renders
child elements outside of any specified bounds. Setting the clipAndEnableScrolling property to true and wrapping the IViewport instance in a Scroller component renders child elements
within a defined area and updates the read-only contentWidth and contentHeight properties of the container to
the specified dimensions.
To enable scrolling for an IViewport, the container can be wrapped in a
Scroller instance with the declared child container
attributed as the viewport property
value:
<fx:Declarations>
<fx:String id="txt">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</fx:String>
</fx:Declarations>
<s:Scroller>
<s:DataGroup width="100" height="100"
clipAndEnableScrolling="true"
itemRenderer="spark.skins.spark.DefaultItemRenderer">
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:dataProvider>
<s:ArrayCollection source="{txt.split(' ')}" />
</s:dataProvider>
</s:DataGroup>
</s:Scroller>Any data elements that are rendered outside of the viewport bounds
of a container explicitly set using the width and height properties are displayed based on the
scrolling properties of the s:Scroller component. Based on the positions
of the child elements within the container viewport, a VScrollBar control and a HScrollBar control are added to the display.
The scroll bars are positioned at the viewport’s width and height property values, unless you specify
custom values for the Scroller
instance’s width and height properties.
Containers that support skinning, such as BorderContainer, SkinnableContainer, and SkinnableDataContainer, do not implement the
IViewport interface. However, the
content layer to which child elements are added for each skinnable
container is attributed as an IViewport implementation. As such, you have a
couple of options for enabling scrolling of child content in a skinnable
container.
The skin part that serves as the content layer for a BorderContainer and a SkinnableContainer is the contentGroup. When children are declared for a
skinnable container directly in MXML markup, the child elements are
added and laid out within the content layer. One way to enable scrolling
of the content is to declare an IViewport implementation wrapped in a Scroller component as the only child of the
skinnable container, as in the following example:
<s:BorderContainer width="120" height="100" backgroundColor="#FFFFFF">
<s:Scroller width="100%" height="100%">
<s:Group>
<s:layout>
<s:VerticalLayout horizontalAlign="justify"
clipAndEnableScrolling="true" />
</s:layout>
<s:Button label="button (1)" />
<s:Button label="button (2)" />
<s:Button label="button (3)" />
<s:Button label="button (4)" />
<s:Button label="button (5)" />
<s:Button label="button (6)" />
</s:Group>
</s:Scroller>
</s:BorderContainer>The Scroller-wrapped Group declared as the only child for the
BorderContainer is added as the only
child within the contentGroup for the
container. Scrolling is not applied to the contentGroup layer specifically, but because
its only child element is a Group
container wrapped in a Scroller whose
dimensions are updated to equal the dimensions of the skinnable
container, it appears as if the content for the BorderContainer is made scrollable.
Another approach to providing scrolling for the content of a
skinnable container is to apply a custom skin to the container that
wraps its content-layer skin part in a Scroller. This is a custom skin for a SkinnableDataContainer:
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
name="CustomScrollableSkin">
<fx:Metadata>
<![CDATA[
[HostComponent("spark.components.SkinnableDataContainer")]
]]>
</fx:Metadata>
<s:states>
<s:State name="normal" />
<s:State name="disabled" />
</s:states>
<s:Rect width="100%" height="100%">
<s:stroke>
<s:SolidColorStroke color="#000000" />
</s:stroke>
<s:fill>
<s:SolidColor color="#FFFFFF" />
</s:fill>
</s:Rect>
<s:Scroller width="100%" height="100%"
left="2" right="2" top="2" bottom="2">
<s:DataGroup id="dataGroup"
left="0" right="0" top="0" bottom="0"
minWidth="0" minHeight="0" />
</s:Scroller>
</s:Skin>The CustomScrollableSkin
fulfills a contract to serve as a skin to a
SkinnableDataContainer by declaring the [HostComponent] metadata and required states.
Also declared is the required skin part, dataGroup, which is the content layer for item
renderers and is wrapped in a
s:Scroller component to enable
scrolling within the container.
The custom skin is supplied to the skinnable container as a
qualified class name attributed to
the skinClass property, as in the
following example:
<fx:Declarations>
<fx:String id="txt">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</fx:String>
</fx:Declarations>
<s:SkinnableDataContainer width="120" height="100"
itemRenderer="spark.skins.spark.DefaultItemRenderer"
skinClass="com.oreilly.f4cb.CustomScrollableSkin">
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:dataProvider>
<s:ArrayCollection source="{txt.split(' ')}" />
</s:dataProvider>
</s:SkinnableDataContainer>The ability to use a Scroller
to enable scrolling of content within containers is a major convenience.
The skin layout of a Scroller
component is a private implementation, however, and the skin parts for
the scroll bars are considered read-only. To have more control over the
layout and the relationship between a viewport and scroll bars, add a
ScrollBar-based control to the
display list directly and assign an instance of a target container as
the viewport property
value:
<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:HorizontalLayout />
</s:layout>
<s:DataGroup id="group" width="100" height="100"
clipAndEnableScrolling="true"
itemRenderer="spark.skins.spark.DefaultItemRenderer">
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:dataProvider>
<s:ArrayCollection source="{txt.split(' ')}" />
</s:dataProvider>
</s:DataGroup>
<s:VScrollBar viewport="{group}" height="100" />
</s:Application>In this example, a DataGroup is
attributed as the IViewport
implementation instance of a s:VScrollBar control. The thumb size of the
scroll bar is based on the height of
the scroll bar and the contentHeight
value of the target container. As the scroll position changes on the
scroll bar, the verticalScrollPosition value is passed down to
the IViewport implementation and
handed to the layout delegate for the container.
While wrapping a container with a Scroller internally detects the need for
scroll bars based on the content of the container and dimensions of its
viewport, with this approach you
have less control over the layout with relation to the target viewport.
Targeting a container using scroll
bars declared directly on the display list allows more control over the
layout, but its visibility is not inherently set based on the contentWidth and contentHeight of a viewport. The visibility of
a scroll bar control can, however, be determined based on the
container’s dimensions and its viewport counterpart value, as in the
following example for updating the visibility of a s:VScrollBar control:
<s:VScrollBar viewport="{group}" height="100"
visible="{group.height < group.contentHeight}" />When the clipAndEnableScrolling
property of an IViewport
implementation is set to true, the
read-only contentWidth and contentHeight properties are set based on the
bounds of the container’s display list. By comparing the defined
height property of the viewport with
the contentHeight, the visibility and
necessity of the VScrollBar control
in this example can be determined.
You want to resize, scale, and lay out the child elements of a container based on the dimensions of the container.
Layout delegates applied to Group and DataGroup containers have properties that
modify the layout and size of child elements directly or through
transformations. Additionally, the layout of children can be modified
based on the resizeMode property
value of GroupBase-based containers
that take the size of the container into consideration. The default
value of the resizeMode property is
noScale, which specifies that the
container resizes itself and children are subsequently resized based on
the properties of the layout delegate. The child content of a container
can be scaled uniformly by setting the resizeMode property
value to scale, which bases the
layout of its children on the measured size of the container.
The following example demonstrates switching between the two
resize modes within a Group
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.events.SliderEvent;
import spark.components.ResizeMode;
private function toggleResizeMode():void
{
group.resizeMode = ( group.resizeMode == ResizeMode.NO_SCALE )
? group.resizeMode = ResizeMode.SCALE
: group.resizeMode = ResizeMode.NO_SCALE;
}
]]>
</fx:Script>
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:Group id="group"
width="{slider.value}" height="{slider.value}"
resizeMode="{ResizeMode.NO_SCALE}">
<s:layout>
<s:TileLayout orientation="columns" />
</s:layout>
<s:Button label="button" />
<s:Rect width="100" height="100">
<s:fill>
<s:SolidColor color="#DDDDDD" />
</s:fill>
</s:Rect>
<s:DropDownList />
<s:Ellipse>
<s:fill>
<s:SolidColor color="#FF5500" />
</s:fill>
</s:Ellipse>
</s:Group>
<s:HSlider id="slider"
width="120"
minimum="100" maximum="300"
value="300"
liveDragging="true" />
<s:Button label="toggle resize" click="toggleResizeMode();" />
</s:Application>As the value of the s:HSlider
control changes, the dimensions of the container are reflected through
binding. The resizeMode is changed
when a click event is received from
the s:Button control, swapping
between the scale and noScale modes enumerated in the ResizeMode class.
The TileLayout delegate applied
to the Group container has an
orientation property value specifying
that children should be laid out in columns based on the dimensions of
the container. As the width and height of the container change, the
layout is based on the amount of space available within the container to
display children within a grid of columns and rows. With the resizeMode set to noScale, children are sized based on their
defined or inherited properties within the layout. When the resizeMode property is set to scale, the child elements are scaled to fill
the dimensions of the container, while still adhering to the column/row
rules of the layout delegate.
Use either a SkinnableContainer
or a BorderContainer as a container
for visual child elements and a SkinnableDataContainer as a container for data
items, and modify the available style properties.
The Group and DataGroup containers are considered
lightweight containers and as such do not support skinning or expose
style properties. To customize the look of a container, the Flex 4 SDK
offers the SkinnableContainer,
BorderContainer, and SkinnableDataContainer classes. Which you use
depends on the type of content provided to the container. The SkinnableContainer and BorderContainer take instances of
IVisualElement as child elements; think of them
as Group containers that support
skinning. The SkinnableDataContainer
can be considered a DataGroup
container that supports skinning and uses item renderers to display
visual representations of data items.
BorderContainer is actually a
subclass of SkinnableContainer and is
a convenient container to use if you want to apply border and background
styles directly without applying a
custom skin. You can set border styles (such as cornerRadius and borderColor) inline within the MXML
declaration for the container, or through Cascading Style Sheet (CSS)
style declarations. The following example demonstrates setting the
border styles of a BorderContainer for
IVisualElement children:
<s:BorderContainer width="200" height="200"
cornerRadius="10" borderColor="#000000"
borderWeight="2" borderStyle="inset">
<s:layout>
<s:VerticalLayout paddingLeft="5" paddingTop="5" paddingBottom="5"
paddingRight="5" horizontalAlign="justify" />
</s:layout>
<s:Label text="Lorem ipsum dolor sit amet consectetur adipisicing elit." />
<s:Button label="click me" />
</s:BorderContainer>Alternatively, you can supply an IStroke instance for the borderStroke property of a BorderContainer. The following example sets a
borderStroke on a BorderContainer through MXML markup:
<s:BorderContainer width="200" height="200"
cornerRadius="10" borderStyle="inset">
<s:layout>
<s:VerticalLayout paddingLeft="5" paddingTop="5" paddingBottom="5"
paddingRight="5" horizontalAlign="justify" />
</s:layout>
<s:Label text="Lorem ipsum dolor sit amet consectetur adipisicing elit." />
<s:Button label="click me" />
<s:borderStroke>
<s:SolidColorStroke color="#0000" weight="2" />
</s:borderStroke>
</s:BorderContainer>The s:SolidColorStroke element
supplied as the borderStroke for the
BorderContainer overrides any
previously declared color or weight style properties. However, because
the IStroke interface does not expose
a cornerRadius or borderStyle property, some border style
properties need to be provided directly to the BorderContainer if they are desired. Because
BorderContainer is an extension of
SkinnableContainer, skinning is also
supported along with the border convenience styles.
Custom skin classes are set on the SkinnableContainerBase-based containers (such
as BorderContainer) through the
skinClass property, whose value can
be set either inline using MXML markup or via CSS, because skinClass is considered a style property. When
you create a custom skin for a container, the skin is entering into a
contract with the container to provide the necessary skin parts and
states for the host. These skin parts are referenced using an
agreed-upon id property value and,
depending on the type of skinnable container, relate to content layers
for visual elements.
To create a custom skin for a skinnable container, extend the
spark.skins.SparkSkin class and
declare the HostComponent metadata
and necessary state and skin part elements. The following is an example
of a custom skin fulfilling a contract to be applied to a SkinnableContainer:
<s:SparkSkin name="CustomGroupSkin"
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:Metadata>
[HostComponent("spark.components.SkinnableContainer")]
</fx:Metadata>
<s:states>
<s:State name="normal" />
<s:State name="disabled" />
</s:states>
<s:Rect width="100%" height="100%">
<s:fill>
<s:LinearGradient>
<s:entries>
<s:GradientEntry color="0xFF0000" />
<s:GradientEntry color="0x00FF00" />
<s:GradientEntry color="0x0000FF" />
</s:entries>
</s:LinearGradient>
</s:fill>
<s:fill.disabled>
<s:RadialGradient>
<s:entries>
<s:GradientEntry color="0xFF0000" />
<s:GradientEntry color="0x00FF00" />
<s:GradientEntry color="0x0000FF" />
</s:entries>
</s:RadialGradient>
</s:fill.disabled>
</s:Rect>
<s:Group id="contentGroup"
width="100%" height="100%"
left="10" right="10" top="10" bottom="10">
<s:layout>
<s:VerticalLayout horizontalAlign="justify" />
</s:layout>
</s:Group>
</s:SparkSkin>The CustomGroupSkin declares
the type of container component that the skin will be applied to within
the [HostComponent] metatag. In this
example the host component is a SkinnableContainer, and as such contains a
Group container with the id property value of contentGroup. The contentGroup property of SkinnableContainer is considered a skin part
and represents the content layer on which visual child elements are
drawn. Along with the host component and contentGroup, contractual states are declared
to represent the enabled and disabled visual states of the container.
You can declare additional states as needed, but at a minimum, normal and disabled need to be added to the available
states to support the enabled
property of the container. Styles can be applied to elements based on
these states, as is shown using inline dot notation for the fill type of the s:Rect element.
To apply the custom skin to a SkinnableContainer instance, set the skinClass style property to a Class reference either inline or using CSS.
The following example applies the CustomGroupSkin skin inline using a fully
qualified class name:
<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">
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:SkinnableContainer id="container"
width="200" height="200"
skinClass="com.oreilly.f4cb.CustomGroupSkin">
<s:Label text="Lorem ipsum dolor sit amet consectetur adipisicing elit." />
<s:Button label="button (1)" />
<s:DropDownList />
</s:SkinnableContainer>
<s:Button label="enable container"
click="{container.enabled=!container.enabled}" />
</s:Application>When applying a custom skin, the layout can be set within the skin class (as in this example). It should be noted, however, that if a layout is applied to a container directly in MXML markup, that layout will override any layout supplied to the content layer declared in the skin.
Creating a custom skin for a SkinnableDataContainer that uses data elements
to represent children is similar to creating a custom skin for a
SkinnableContainer instance. The
difference between the two involves the type of host component
declaration and skin part reference. The following custom skin declares
the host component references as the SkinnableDataContainer class and contains a
DataGroup container with the
reference id of dataGroup:
<s:SparkSkin name="CustomDataGroupSkin"
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:Metadata>
[HostComponent("spark.components.SkinnableDataContainer")]
</fx:Metadata>
<s:states>
<s:State name="normal" />
<s:State name="disabled" />
</s:states>
<s:Rect width="100%" height="100%">
<s:fill>
<s:LinearGradient>
<s:entries>
<s:GradientEntry color="0xFF0000" />
<s:GradientEntry color="0x00FF00" />
<s:GradientEntry color="0x0000FF" />
</s:entries>
</s:LinearGradient>
</s:fill>
<s:fill.disabled>
<s:RadialGradient>
<s:entries>
<s:GradientEntry color="0xFF0000" />
<s:GradientEntry color="0x00FF00" />
<s:GradientEntry color="0x0000FF" />
</s:entries>
</s:RadialGradient>
</s:fill.disabled>
</s:Rect>
<s:Scroller width="100%" height="100%">
<s:DataGroup id="dataGroup"
width="100%" height="100%">
<s:layout>
<s:VerticalLayout paddingLeft="10" paddingRight="10"
paddingTop="10" paddingBottom="10" />
</s:layout>
</s:DataGroup>
</s:Scroller>
</s:SparkSkin>The CustomDataGroupSkin
fulfills a contract with SkinnableDataContainer to provide a DataGroup instance as the content layer for
data elements supplied to the skinnable container. With the host
component metatag and necessary states declared, the custom skin is
applied to a SkinnableDataContainer
through the skinClass style
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">
<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:SkinnableDataContainer id="container" width="200" height="200"
itemRenderer="spark.skins.spark.DefaultItemRenderer"
skinClass="com.oreilly.fcb4.CustomDataGroupSkin">
<s:dataProvider>
<s:ArrayCollection
id="collection" source="{txt.split(' ')}" />
</s:dataProvider>
</s:SkinnableDataContainer>
<s:Button label="enable container"
click="{container.enabled=!container.enabled}" />
</s:Application>The full extent of skinning and styling possibilities that the
Flex 4 SDK provides is discussed in Chapter 6, Skinning and Styles, but these examples demonstrate the
basic contractual agreement that custom skins must fulfill when working
with SkinnableContainerBase-based
containers.
You want to set the background image of a BorderContainer and control how the graphic is
applied as a fill.
Use either the background image style properties of the BorderContainer or the backgroundFill property to apply a BitmapFill directly.
The BorderContainer is a
convenience container for IVisualElement child elements that exposes
style properties pertaining to the border and background displays of a
container not found directly on its superclass, the SkinnableContainer. When using a SkinnableContainer, border and background
styles are handled by a skin class applied to the container. The
BorderContainer class provides style
properties for the border and background that can be set inline in MXML
markup or through CSS; it also provides two properties, borderStroke and backgroundImage, that allow you to apply
styles using graphic elements.
The following example demonstrates setting the style properties
for a background image inline on a BorderContainer:
<s:BorderContainer width="200" height="200"
cornerRadius="10" borderStyle="inset"
backgroundImage="@Embed(source='background.jpg')"
backgroundImageFillMode="{BitmapFillMode.REPEAT}">
<s:layout>
<s:VerticalLayout paddingLeft="5" paddingTop="5" paddingBottom="5"
paddingRight="5" horizontalAlign="justify" />
</s:layout>
<s:Label text="Lorem ipsum dolor sit amet consectetur adipisicing elit." />
<s:Button label="click me" />
</s:BorderContainer>Alternatively, the style properties of a BorderContainer can be applied using CSS and
set using the styleName
property:
<fx:Style>
@namespace s "library://ns.adobe.com/flex/spark";
@namespace mx "library://ns.adobe.com/flex/mx";
.imageBorder {
backgroundImage: Embed(source='background.jpg');
backgroundImageFillMode: repeat;
}
</fx:Style>
<s:BorderContainer width="200" height="200"
cornerRadius="10" borderStyle="inset"
styleName="imageBorder">
<s:layout>
<s:VerticalLayout paddingLeft="5" paddingTop="5" paddingBottom="5"
paddingRight="5" horizontalAlign="justify" />
</s:layout>
<s:Label text="Lorem ipsum dolor sit amet consectetur adipisicing elit." />
<s:Button label="click me" />
</s:BorderContainer>In the previous examples, an image from the local resource is
embedded and supplied as the backgroundImage style property value. The fill
mode for the image is set using the backgroundImageFillMode style property, which
can take three values—clip, scale, and repeat—all of which are enumerated properties
of the BitmapFillMode class. When a
background image is styled with clipping, the image is rendered at its
original dimensions within the container. A background image with a
scale value for the fill mode is
scaled to the dimensions of the container, and the repeat value repeats the image in a grid to
fill the container region. Each background image fill mode also takes
into account the container display when the cornerRadius style property is set.
Background image styles can also be applied using the backgroundFill property of a BorderContainer. The backgroundFill property takes an
implementation of the IFill interface
and will override any background style properties that have been set.
You can assign to the backgroundFill
property a BitmapFill instance that
exposes a fillMode property that you
can use to apply the same styling you might do with the style properties
of a BorderContainer:
<s:BorderContainer width="200" height="200"
cornerRadius="10" borderStyle="inset">
<s:layout>
<s:VerticalLayout paddingLeft="5" paddingTop="5" paddingBottom="5"
paddingRight="5" horizontalAlign="justify" />
</s:layout>
<s:Label text="Lorem ipsum dolor sit amet consectetur adipisicing elit." />
<s:Button label="click me" />
<s:backgroundFill>
<s:BitmapFill source="@Embed('background.jpg')"
fillMode="{BitmapFillMode.REPEAT}" />
</s:backgroundFill>
</s:BorderContainer>Use controlBarContent to add
visual elements to a control bar group, and use controlBarLayout to define the layout for the
control bar group.
Both the Application and
Panel containers support the addition
of a control bar group by declaring an array of IVisualElement instances as the value of the
controlBarContent property. The contentBarGroup property is a Group container whose default property value
for child elements is the controlBarContent property. Visual elements
declared in MXML markup are added
to the group to display a control bar, 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:Declarations>
<s:ArrayCollection
id="authors" source="{['Josh Noble',
'Garth Braithwaite', 'Todd Anderson']}" />
</fx:Declarations>
<s:Panel title="Control Bar Example" width="300" height="120">
<s:controlBarContent>
<s:DropDownList id="authorCB" width="120" dataProvider="{authors}" />
<s:Button label="select"
click="{printField.text=authorCB.selectedItem}" />
</s:controlBarContent>
<s:Group width="100%" height="100%">
<s:Label id="printField" horizontalCenter="0" verticalCenter="0" />
</s:Group>
</s:Panel>
</s:Application>The s:DropDownList and s:Button controls are displayed in a control
bar on the bottom region of the Panel
container, and upon the receipt of a click event from the Button, the selected item from the DropDownList is printed in the s:Label component of the content group for the
Panel.
Panel and Application each have a default layout for the
optional control bar when it is added to the display list that has
predefined constraints and displays child elements in a horizontal
sequence. The layout of the control bar can be modified using the
controlBarLayout property:
<s:controlBarLayout>
<s:VerticalLayout horizontalAlign="justify" />
</s:controlBarLayout>Any LayoutBase-based layout can
be attributed as a controlBarLayout.
In this example, all IVisualElement
instances from the controlBarContent
list will be added to the control bar group in a vertical sequence and
resized to the elemental region width-wise within the group.
When a control bar is added to an Application, it resides by default in the
upper region of the Application
container. The contentBarGroup
property is considered a skin part, allowing for the location and style
of the control bar to be modified by setting a custom skin.
You want to modify the default layout of the content elements in
the display list of a Panel
container.
Create a custom skin class that fulfills the contract of a skin
for a Panel component and set it as
the skinClass property value. Within
the custom skin, modify the layout and declaration of skin parts to
change the display of the control bar and content.
By default, the layout property
of a Panel is applied to the contentGroup skin part, which is the group of
visual elements that are displayed between the title bar and optional
control bar of the container. The Panel’s default skin class handles the layout
of the title bar, content group, and control bar, ordering them in a
top-down fashion. You can modify the position and size of each of these
content elements by providing a custom skin class to the Panel using the skinClass property.
When creating a custom skin, you must ensure that the skin adheres
to a contractual agreement with the target host component and declares
any required states and content references, referred to as
skin parts. The titleDisplay and contentGroup skin parts refer to the title bar
and main content display regions of a panel. The optional contentBarGroup skin part refers to the
control bar. When creating a custom skin class for a Panel container that supports a control bar,
you must declare all three skin parts as well as the corresponding
states of normal, disabled, normalWithControlBar, and disabledWithControlBar, as in the following
example:
<s:SparkSkin name="CustomPanelSkin"
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx">
<s:states>
<s:State name="normal" />
<s:State name="normalWithControlBar" />
<s:State name="disabled" />
<s:State name="disabledWithControlBar" />
</s:states>
<fx:Metadata>
[HostComponent("spark.components.Panel")]
</fx:Metadata>
<s:RectangularDropShadow id="shadow" alpha="0" />
<!-- Border -->
<s:Rect left="0" right="0" top="0" bottom="0">
<s:stroke>
<s:SolidColorStroke color="0" alpha="0.5" weight="1" />
</s:stroke>
</s:Rect>
<!-- Background -->
<s:Rect id="background" left="1" top="1" right="1" bottom="1">
<s:fill>
<s:SolidColor color="0xFFFFFF" />
</s:fill>
</s:Rect>
<!-- Content -->
<s:Group width="100%" height="100%" top="1" left="1" right="1" bottom="1">
<s:layout>
<s:VerticalLayout gap="0" horizontalAlign="justify" />
</s:layout>
<!-- Control Bar -->
<s:Group>
<s:Rect width="100%" height="30">
<s:fill>
<s:SolidColor color="0xCCCCCC" />
</s:fill>
</s:Rect>
<s:Group id="controlBarGroup"
left="5" right="5" top="5" bottom="5"
minWidth="0" minHeight="0" height="30">
<s:layout>
<s:HorizontalLayout />
</s:layout>
</s:Group>
</s:Group>
<!-- Content -->
<s:Scroller width="100%" height="100%" >
<s:Group id="contentGroup" />
</s:Scroller>
<!-- Title Bar -->
<s:Group>
<s:Rect width="100%" height="30">
<s:fill>
<s:SolidColor color="0xEEEEEE" />
</s:fill>
</s:Rect>
<s:Label id="titleDisplay" lineBreak="toFit"
left="10" height="30" verticalAlign="middle"
fontWeight="bold" />
</s:Group>
</s:Group>
</s:SparkSkin>CustomPanelSkin is an extension
of spark.skins.SparkSkin. It enters
into a contract with spark.components.Panel (the host component
declared in the [HostComponent]
metatag) to expose the content elements for the title, content group,
and control bar and any necessary elements for display. The default
layout of the target panel in this example is changed by positioning the
control bar at the top and the title bar at the bottom. These content
elements are declared as controlBarGroup and titleDisplay, respectively, within a Group container with a VerticalLayout.
Visual elements provided in the controlBarContent property of a panel are added to the controlBarGroup skin part, and visual elements
added directly to the panel are
displayed in the contentGroup skin
part. The title property value of the
Panel container is printed out in the
titleDisplay component. To apply a
custom skin to a Panel, set the
skinClass property value to the fully
qualified name of the custom skin class, 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:Declarations>
<s:ArrayCollection
id="authors" source="{['Josh Noble',
'Garth Braithwaite', 'Todd Anderson']}" />
</fx:Declarations>
<s:Panel title="Control Bar Example"
width="300" height="120"
skinClass="com.oreilly.f4cb.CustomPanelSkin">
<s:controlBarContent>
<s:DropDownList id="authorCB" width="120" dataProvider="{authors}" />
<s:Button label="select"
click="{printField.text=authorCB.selectedItem}" />
</s:controlBarContent>
<s:Group width="100%" height="100%">
<s:Label id="printField" verticalCenter="0" horizontalCenter="0" />
</s:Group>
</s:Panel>
</s:Application>The full extent of skinning and style possibilities available in the Flex 4 SDK is discussed in Chapter 6, Skinning and Styles, but the examples presented here demonstrate the basic contractual agreement that a custom skin class must adhere to in order to modify the look and feel of a panel containing an optional control bar group display.
Add an event handler for a mouse gesture event and use the
contentMouseX and contentMouseY
properties to retrieve the mouse position within the container,
regardless of the position of the interactive element that dispatched
the event.
When an event handler for a mouse gesture is declared for an event
on a container, a MouseEvent object is passed to the
method with the localX and localY properties attributed to the mouse position within
the interactive element that originally dispatched the event. UIComponent-based elements have contentMouseX and contentMouseY read-only
properties that relate to the mouse position within those elements,
regardless of its position within any child elements. Because the
properties are read-only, their values cannot be bound to. You can
retrieve these values within an event handler for a mouse gesture, 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:Library>
<fx:Definition name="RadialBox">
<s:Rect width="100" height="100">
<s:fill>
<s:LinearGradient>
<s:entries>
<s:GradientEntry color="0xFF0000" />
<s:GradientEntry color="0x00FF00" />
<s:GradientEntry color="0x0000FF" />
</s:entries>
</s:LinearGradient>
</s:fill>
</s:Rect>
</fx:Definition>
<fx:Definition name="LinearBox">
<s:Rect width="100" height="100">
<s:fill>
<s:RadialGradient>
<s:entries>
<s:GradientEntry color="0xFF0000" />
<s:GradientEntry color="0x00FF00" />
<s:GradientEntry color="0x0000FF" />
</s:entries>
</s:RadialGradient>
</s:fill>
</s:Rect>
</fx:Definition>
</fx:Library>
<fx:Script>
<![CDATA[
import mx.graphics.SolidColor;
private var bmd:BitmapData;
private function handleGroupCreation():void
{
bmd = new BitmapData(group.contentWidth, group.contentHeight);
bmd.draw( group );
}
private function handleMouseMove( evt:MouseEvent ):void
{
var xpos:int = group.contentMouseX;
var ypos:int = group.contentMouseY;
var rectColor:SolidColor = new SolidColor(bmd.getPixel(xpos,
ypos));
chip.fill = rectColor;
contentLocalPoint.text = "Content Local: " + xpos + " : " + ypos;
mouseLocalPoint.text = "Mouse Local: " + evt.localX + " : "
+ evt.localY;
}
]]>
</fx:Script>
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:Group id="group"
creationComplete="handleGroupCreation();"
mouseMove="handleMouseMove(event);">
<s:layout>
<s:HorizontalLayout gap="2" verticalAlign="middle" />
</s:layout>
<fx:RadialBox />
<s:Button label="button (1)" />
<fx:LinearBox />
</s:Group>
<s:Label id="contentLocalPoint" />
<s:Label id="mouseLocalPoint" />
<s:Rect id="chip" width="30" height="30">
<s:stroke>
<s:SolidColorStroke color="0x000000" />
</s:stroke>
</s:Rect>
</s:Application>As each mouseMove event is
received in the Group container, the
handleMouseMove() method is invoked
and the fill color value for the s:Rect graphic element is updated based on the
pixel color under the mouse cursor using the contentMouseX and contentMouseY properties. Two s:Label components print out the mouse
position in its relation to the container and the interactive element,
such as the s:Button control, that
first dispatched the event. Because graphic elements are not considered
interactive (and thus do not dispatch mouse events), the local mouse
positions printed out will be the same as the content mouse positions
when the cursor is over a graphic element.
The contentMouseX and contentMouseY properties represent the mouse
position within the target UIComponent-based element, including regions
that are only accessible through scrolling. The global mouse position
with regards to any layout constraints applied to the container itself
can be retrieved using the contentToGlobal() method:
var pt:Point = group.contentToGlobal( new Point( group.contentMouseX,
group.contentMouseY ) );
globalPoint.text = "Global Point: " + pt.x + " : " + pt.y;Likewise, any global point can be converted to content-local
coordinates using the globalToContent() method of a UIComponent-based component.
Enable any interactive visual element as a drag initiator by
assigning a mouseDown event handler
to the element and enable any container as a drag recipient by assigning
it drag-and-drop event handlers. Upon invocation of the mouseDown handler, assign the relevant data to
a DragSource instance to be handed to
the DragManager. When entering a
dragEnter event for the drop target,
determine the acceptance of a drop operation on the container based on
the appropriate data. Once a drop container has accepted a dragDrop event, remove the dragged visual
element from its owner and add it to the target drop container.
Drag-and-drop support can be added to any element that extends
mx.core.UIComponent. Within a
drag-and-drop operation there is an initiator and a receiver. Any
instance of UIComponent can receive
the series of operations initiated by a drag gesture and dispatch events
accordingly; these events include dragEnter, dragExit, dragOver, dragDrop, and dragComplete.
To initialize a drag-and-drop gesture, add data relevant to the
drag-and-drop operation to a DragSource object within a mouseDown event handler. The DragSource object is given to the DragManager through the static doDrag() method. The DragSource object held by the DragManager is used to determine the
acceptance of a drop event on a target container and is handled in the
dragDrop event handler to perform the
appropriate action.
The following example demonstrates moving children from one visual element container to another:
<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.IUIComponent;
import mx.managers.DragManager;
import mx.core.DragSource;
import spark.components.SkinnableContainer;
import mx.events.DragEvent;
import mx.core.IVisualElement;
private function handleStartDrag( evt:MouseEvent ):void
{
// grab the item renderer and relevant data
var dragItem:IUIComponent = evt.target as IUIComponent;
var dragSource:DragSource = new DragSource();
dragSource.addData( dragItem, "item" );
DragManager.doDrag( dragItem, dragSource, evt );
}
protected function handleDragEnter( evt:DragEvent ):void
{
if( evt.dragSource.hasFormat( "item" ) )
DragManager.acceptDragDrop( evt.target as IUIComponent );
}
protected function handleDragDrop( evt:DragEvent ):void
{
var dragItem:Object = evt.dragSource.dataForFormat( "item" );
var dragItemOwner:SkinnableContainer = ( dragItem.owner as
SkinnableContainer );
dragItemOwner.removeElement( dragItem as IVisualElement );
var targetOwner:SkinnableContainer = ( evt.target as
SkinnableContainer );
targetOwner.addElement( dragItem as IVisualElement );
}
]]>
</fx:Script>
<s:SkinnableContainer width="200" height="180"
dragEnter="handleDragEnter(event);"
dragDrop="handleDragDrop(event);"
skinClass="com.oreilly.f4cb.CustomBorderSkin">
<s:layout>
<s:HorizontalLayout />
</s:layout>
<s:Button label="drag me (1)" mouseDown="handleStartDrag(event);" />
<s:Button label="drag me (2)" mouseDown="handleStartDrag(event);" />
<s:Button label="drag me (3)" mouseDown="handleStartDrag(event);" />
</s:SkinnableContainer>
<s:SkinnableContainer x="210" width="200" height="180"
dragEnter="handleDragEnter(event);"
dragDrop="handleDragDrop(event);"
skinClass="com.oreilly.f4cb.CustomBorderSkin">
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:Button label="drag me (4)" mouseDown="handleStartDrag(event);" />
<s:Button label="drag me (5)" mouseDown="handleStartDrag(event);" />
<s:Button label="drag me (6)" mouseDown="handleStartDrag(event);" />
</s:SkinnableContainer>
</s:Application>When a s:Button control in the
display list of either of the SkinnableContainers dispatches a mouseDown event, the handleStartDrag() method is invoked and a
DragSource object is added to the
DragManager. The static doDrag() method of the
DragManager
initiates a drag-and-drop gesture. It requires at least three arguments:
the drag initiator item reference,
a DragSource object, and the
initiating MouseEvent. The image
rendered during a drag operation is a rectangle with alpha transparency,
by default. The dragged image (referred to as a drag
proxy) can be changed through the dragImage and imageAlpha arguments of the doDrag() method.
Assigning event handlers for dragEnter and dragDrop events identifies the containers as
targets for the drag-and-drop actions initiated by the Button controls. Within the handleDragEnter()
method, the data format is evaluated to see whether the target container
accepts drop actions. The static acceptDragDrop() method of the DragManager registers the container as a drop
target. Once a container is accepted as a receiver for drag-and-drop
actions, any subsequent actions associated with the gesture are passed
to the container and the appropriate events are dispatched. Within the
dragDrop event handler, the drag
initiator held on the DragSource
object of the operation is used to remove that object from its current
owner container and add it to the target drop container.
Though the previous example demonstrates using the default operation of moving an element from one container to another, it is possible to implement a copy operation. The following example demonstrates copying a visual element from one container to another without allowing the initiating owner to receive a drop operation:
<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.components.Button;
import mx.managers.DragManager;
import mx.core.DragSource;
import spark.components.SkinnableContainer;
import mx.events.DragEvent;
import mx.core.IVisualElement;
import mx.core.IUIComponent;
private function handleStartDrag( evt:MouseEvent ):void
{
// grab the item renderer and relevant data
var dragItem:Button = evt.target as Button;
var transferObject:Object = {label:dragItem.label,
owner:dragItem.owner};
var dragSource:DragSource = new DragSource();
dragSource.addData( transferObject, "item" );
DragManager.doDrag( dragItem, dragSource, evt, null, 0, 0, 0.5,
false );
}
protected function handleDragEnter( evt:DragEvent ):void
{
if( evt.dragSource.hasFormat( "item" ) )
{
var targetOwner:SkinnableContainer = ( evt.target as
SkinnableContainer );
var transferObject:Object = evt.dragSource.dataForFormat(
"item" );
if( targetOwner != transferObject.owner )
{
DragManager.acceptDragDrop( evt.target as IUIComponent );
}
}
}
protected function handleDragDrop( evt:DragEvent ):void
{
var transferObject:Object = evt.dragSource.dataForFormat( "item" );
var dragItem:Button = new Button();
dragItem.label = transferObject.label;
var targetOwner:SkinnableContainer = ( evt.target as
SkinnableContainer );
targetOwner.addElement( dragItem as IVisualElement );
}
]]>
</fx:Script>
<s:SkinnableContainer width="200" height="180"
dragEnter="handleDragEnter(event);"
dragDrop="handleDragDrop(event);"
skinClass="com.oreilly.f4cb.CustomBorderSkin">
<s:layout>
<s:HorizontalLayout />
</s:layout>
<s:Button label="drag me (1)" mouseDown="handleStartDrag(event);" />
<s:Button label="drag me (2)" mouseDown="handleStartDrag(event);" />
<s:Button label="drag me (3)" mouseDown="handleStartDrag(event);" />
</s:SkinnableContainer>
<s:SkinnableContainer x="210" width="200" height="180"
dragEnter="handleDragEnter(event);"
dragDrop="handleDragDrop(event);"
skinClass="com.oreilly.f4cb.CustomBorderSkin">
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:Button label="drag me (4)" mouseDown="handleStartDrag(event);" />
<s:Button label="drag me (5)" mouseDown="handleStartDrag(event);" />
<s:Button label="drag me (6)" mouseDown="handleStartDrag(event);" />
</s:SkinnableContainer>
</s:Application>When the mouseDown event
handler is invoked, a generic object representing the initiating
s:Button (with any appropriate
property values preserved) is created and passed as the data transfer
object of the DragSource instance.
The handleDragEnter event handler is
used not only to determine the validity of the drag initiator, but also
to see if the source and target of the operation are the same. If so, no
further drag-and-drop operations are allowed on the container that
dispatched the dragEnter event. If
the target container is a valid receiver for the drag-and-drop action,
the handleDragDrop() method is
invoked and a new Button control is
created based on the generic object of the DragSource and is added to the target
container.
Using the DragManager is a
convenient way to move and copy visual elements from one container to
another. If you need more control over the drag-and-drop operation,
however, you can transfer elements from one container to another using
mouse event handlers and methods of the content API, such as addElement() and removeElement().
You want to enable drag-and-drop capabilities between multiple
DataGroup containers so you can
easily add and remove data items.
Assign a mouseDown event
handler to item renderers as they are added to a DataGroup container and assign drag-and-drop
event handlers to any receiving data containers. Upon receipt of the
mouseDown event, assign the data held
on the target item renderer as the DataSource handled by the DragManager to initiate the drag-and-drop
operation. Use the data from the DataSource object to determine the acceptance
of a drop gesture for the target container as drag events are dispatched
and, when the dragDrop event is
received, remove the dragged data from the collection of the initiating
container and add the data to the collection of the target drop
container.
Within a drag-and-drop operation, there is an initiator and a
receiver. The initiator begins the drag-and-drop operation by invoking
the static doDrag() method of the
DragManager, typically in response to
a user gesture such as a mouseDown
event. Any UIComponent-based element
can be a receiver of drag-and-drop gestures and dispatch events
accordingly. Some list-based components in the Flex SDK, such as
List, have built-in support for
managing drag-and-drop operations to help automate the process of moving
data from one container to another or within a container itself.
DataGroup and SkinnableDataContainer do not have built-in
support, but they can be enabled to receive drag-and-drop operations as
they are extensions of UIComponent.
The example for this recipe is split up into two parts, a view and
a controller, to better demonstrate how to programmatically move data
from one data container’s collection to another. The view is an
extension of an ActionScript-based controller and is made up of two
scroll-enabled s:SkinnableDataContainer containers with their
own specified data collections and itemRenderer instances:
<ApplicationViewController xmlns="*"
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 com.oreilly.f4cb.CustomScrollableSkin;
]]>
</fx:Script>
<fx:Declarations>
<s:ArrayCollection id="collectionOne">
<s:Button label="button (1)" />
<s:Button label="button (2)" />
<s:Button label="button (3)" />
</s:ArrayCollection>
<s:ArrayCollection id="collectionTwo">
<s:Button label="button (4)" />
<s:Button label="button (5)" />
<s:Button label="button (6)" />
</s:ArrayCollection>
</fx:Declarations>
<layout>
<s:HorizontalLayout />
</layout>
<s:SkinnableDataContainer width="160" height="140"
rendererAdd="handleRendererAdd(event)"
dataProvider="{collectionOne}"
dragEnter="handleDragEnter(event);"
dragDrop="handleDragDrop(event);"
itemRenderer="spark.skins.spark.
DefaultComplexItemRenderer"
skinClass="com.oreilly.f4cb.CustomScrollableSkin">
<s:layout>
<s:VerticalLayout paddingLeft="5" paddingRight="5"
paddingTop="5" paddingBottom="5" />
</s:layout>
</s:SkinnableDataContainer>
<s:SkinnableDataContainer width="160" height="140"
rendererAdd="handleRendererAdd(event)"
dataProvider="{collectionTwo}"
dragEnter="handleDragEnter(event);"
dragDrop="handleDragDrop(event);"
itemRenderer="spark.skins.spark.DefaultItemRenderer"
skinClass="com.oreilly.f4cb.CustomScrollableSkin">
<s:layout>
<s:HorizontalLayout paddingLeft="5" paddingRight="5"
paddingTop="5" paddingBottom="5" />
</s:layout>
</s:SkinnableDataContainer>
</ApplicationViewController>The collection for each SkinnableDataContainer container is a set of
s:Button controls. The containers’
itemRenderer instances differ in how
they render data on the content layer: the first declared container
renders each button with its skin intact by assigning the DefaultCompleteItemRenderer class as the item
renderer, while the second declared container renders only the label
assigned to the button control by assigning the DefaultItemRenderer
class as the item renderer.
The handleRendererAdd() method
is assigned as an event handler for rendererAdd. Similar to the elementAdd and elementRemove events of the content API,
DataGroup and SkinnableDataContainer dispatch rendererAdd and rendererRemove events whenever an element
representing a data object from the collection is added to or removed
from the content layer of the container, respectively. Event handlers
for the dragEnter and dragDrop events are assigned to each container
in order to handle those specific operations during a drag-and-drop
operation:
package
{
import flash.display.DisplayObjectContainer;
import flash.events.MouseEvent;
import mx.collections.IList;
import mx.core.DragSource;
import mx.core.IUIComponent;
import mx.core.IVisualElement;
import mx.events.DragEvent;
import mx.managers.DragManager;
import spark.components.Application;
import spark.components.Group;
import spark.components.SkinnableDataContainer;
import spark.components.supportClasses.ItemRenderer;
import spark.events.RendererExistenceEvent;
public class ApplicationViewController extends Application
{
private var dragItem:Group;
protected function handleRendererAdd( evt:RendererExistenceEvent ):void
{
// assign weak reference listener to visual item renderer
var item:IVisualElement = evt.renderer;
( item as DisplayObjectContainer ).mouseChildren = false;
item.addEventListener( MouseEvent.MOUSE_DOWN, handleStartDrag, false,
0, true );
}
private function handleStartDrag( evt:MouseEvent ):void
{
// grab the item renderer and relevant data
var target:UIComponent = evt.target as UIComponent;
var dragItem:Object = {owner:target.owner,
data:( target as
IDataRenderer ).data};
var dragSource:DragSource = new DragSource();
dragSource.addData( dragItem, "itemRenderer" );
DragManager.doDrag( target, dragSource, evt );
}
protected function handleDragEnter( evt:DragEvent ):void
{
if( evt.dragSource.hasFormat( "itemRenderer" ) )
DragManager.acceptDragDrop( evt.target as IUIComponent );
}
protected function handleDragDrop( evt:DragEvent ):void
{
var dragItem:Object = evt.dragSource.dataForFormat( "itemRenderer" );
var ownerCollection:IList = ( dragItem.owner as
SkinnableDataContainer ).dataProvider;
ownerCollection.removeItemAt( ownerCollection.getItemIndex(
dragItem.data ) );
var targetCollection:IList = ( evt.target as
SkinnableDataContainer ).dataProvider;
targetCollection.addItem( dragItem.data );
}
}
}The event object for a rendererAdd event dispatched from a DataGroup or a SkinnableDataContainer is a RendererExistenceEvent. The item renderer that
dispatched the event can be referenced using the renderer property of the event object and is
attributed as an IVisualElement. In
this example, a weak-referenced mouseDown event handler is assigned to the
item renderer upon receipt of a rendererAdd event by the ApplicationViewController and is attributed as
the handleStartDrag() method.
When the mouseDown event
handler is invoked, the initiating element is attributed as a UIComponent instance and a generic Object is created to hold the data property assigned to the element during
instantiation. The generic Object is
assigned as the drag data on a DragSource object, which is passed into the
DragManager to initiate a
drag-and-drop operation. When the dragged item enters the content layer
of a container, the dragEnter event
handler assigned to that container is invoked and is used to determine
whether the data being dragged is acceptable for the container, using
the static acceptDragDrop() method of
DragManager. If the container is
accepted as a receiver, the dragDrop
event handler is invoked upon a drop operation. Upon an accepted drop
operation, the generic Object handled
by the DragSource is used to remove
the dragged data from the collection of its original container and add
it to the collection of the drop target container.
Using the DragManager is a
convenient way to move data items from one container to another.
However, if more control over the operation is needed, data items can be
transferred between containers or within a single container using the
mouse event handlers and methods of the collections API, such as
addItem(), addItemAt(), and
removeItem().
Add a Spark NavigatorContent
container as a child of the desired MX navigation container. Elements
from both the Spark and MX component sets can be added as children to
the NavigatorContent
container.
Although it is recommended to use Spark containers in preference
to MX containers because of their improved runtime performance and
separation of responsibilities, the two sets do not have identical
navigation containers in the Flex 4 SDK. Consequently, depending on
development requirements, use of MX navigation containers may be
necessary. Containers and components from the Spark set cannot be
declared directly as content for Halo containers, however, because child
containers are attributed as implementations of INavigatorContent.
To add Spark elements to a MX container, they must be added to a
NavigatorContent container 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">
<mx:Accordion width="300" height="300" headerHeight="50">
<s:NavigatorContent label="Container 1"
width="100%" height="100%">
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:Button />
<s:Ellipse width="100" height="100">
<s:fill>
<s:SolidColor color="0xFFCCFF" />
</s:fill>
</s:Ellipse>
<s:DropDownList />
</s:NavigatorContent>
<s:NavigatorContent label="Container 2"
width="100%" height="100%">
<s:layout>
<s:HorizontalLayout />
</s:layout>
<s:CheckBox />
<s:Rect width="100" height="100">
<s:fill>
<s:SolidColor color="0xCCFFCC" />
</s:fill>
</s:Rect>
<s:HSlider />
</s:NavigatorContent>
</mx:Accordion>
</s:Application>NavigatorContent is an
extension of SkinnableContainer and
implements the INavigatorContent
interface. The INavigatorContent
interface exposes common properties, such as label and icon, for child content of MX navigation
containers and extends IDeferredContentOwner. The IDeferredContentOwner interface is an
extension of IUIComponent. It fulfills a contract for
NavigatorContent, being a valid child
of a navigation container from the MX set, and also allows for deferred
instantiation of the container. Because Spark containers support
children from both the Spark and MX component sets, elements from both
architectures (including GraphicElement-based elements) can be added to
a NavigatorContent
container.
You want to create a container that holds multiple child containers that are lazily instantiated upon request.
Create a custom GroupBase-based
container and assign an Array-based
property to the [DefaultProperty]
metatag for the container that represents the declared MXML children.
Expose selectedIndex and selectedChild properties to represent the
currently displayed child container, and override the protected commitProperties() method to add the
appropriate child to the display list of the view stack.
The Spark container set does not provide equal parity to the
navigational containers in the MX container set (such as Accordion and ViewStack). You can create Spark equivalents
to these MX navigational containers, however, using the content API, as
well as state management and the new skinning capabilities of the Spark
architecture.
The ViewStack container from
the MX component set acts as a navigation container for multiple child
containers within a single display. As the selected container is
changed, the current container is removed from the display list of the
ViewStack and replaced with the
requested container. Optionally, child containers can be lazily created
using what is referred to as deferred
instantiation. Although the Spark container set does not
offer such a container, you can create a similar one, as shown in the
following example:
package com.oreilly.f4cb
{
import mx.core.IVisualElement;
import spark.components.BorderContainer;
import spark.events.IndexChangeEvent;
[Event(name="change", type="spark.events.IndexChangeEvent")]
[DefaultProperty("content")]
public class CustomViewStack extends BorderContainer
{
[ArrayElementType("mx.core.IVisualElement")]
protected var _content:Array;
protected var _selectedIndex:int = −1;
protected var _selectedChild:IVisualElement
protected var _pendingSelectedIndex:int = −1;
override protected function commitProperties() : void
{
super.commitProperties();
// if pending change to selectedIndex property
if( _pendingSelectedIndex != −1 )
{
// commit the change
updateSelectedIndex( _pendingSelectedIndex );
// set pending back to default
_pendingSelectedIndex = −1;
}
}
protected function updateSelectedIndex( index:int ):void
{
// store old for event
var oldIndex:int = _selectedIndex;
// set new
_selectedIndex = index;
// remove old element
if( numElements > 0 )
removeElementAt( 0 );
// add new element
selectedChild = _content[_selectedIndex];
addElement( _selectedChild );
// dispatch index change
var event:IndexChangeEvent = new IndexChangeEvent(
IndexChangeEvent.CHANGE,
false, false,
oldIndex, _selectedIndex );
dispatchEvent( event );
}
private function getElementIndexFromContent( element:IVisualElement ):int
{
if( _content == null ) return −1;
var i:int = _content.length;
var contentElement:IVisualElement;
while( --i > −1 )
{
contentElement = _content[i] as IVisualElement;
if( contentElement == element )
{
break;
}
}
return i;
}
[Bindable]
[ArrayElementType("mx.core.IVisualElement")]
public function get content():Array /*IVisualElement*/
{
return _content;
}
public function set content( value:Array /*IVisualElement*/ ):void
{
_content = value;
// update selected index based on pending operations
selectedIndex = _pendingSelectedIndex == −1 ? 0 :
_pendingSelectedIndex;
}
[Bindable]
public function get selectedIndex():int
{
return selectedIndex = _pendingIndex == -1"
"? 0"
": _pendingIndex
}
public function set selectedIndex( value:int ):void
{
if( _selectedIndex == value ) return;
_pendingSelectedIndex = value;
invalidateProperties();
}
[Bindable]
public function get selectedChild():IVisualElement
{
return _selectedChild;
}
public function set selectedChild( value:IVisualElement ):void
{
if( _selectedChild == value ) return;
// if not pending operation on selectedIndex, induce
if( _pendingSelectedIndex == −1 )
{
var proposedIndex:int = getElementIndexFromContent( value );
selectedIndex = proposedIndex;
} // else just hold a reference for binding update
else _selectedChild = value;
}
}
}The content property of the CustomViewStack in this example is an array of
IVisualElement-based objects and is declared as
the [DefaultProperty] value for the
class. Consequently, any child elements declared within the MXML markup
for a CustomViewStack instance are
considered elements of the array, and the view stack manages how those
child elements are instantiated.
The selectedIndex and selectedChild properties are publicly exposed
to represent the requested child to display within the custom view
stack. Lazy creation of the child containers is accomplished by
deferring instantiation of children to the first request to add a child
to the display list using the addElement() method of the content
API.
The CustomViewStack container
can be added to an application in MXML markup just like any other
container, as long as the namespace for the package in which it resides
is defined:
<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="lorem">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</fx:String>
</fx:Declarations>
<fx:Script>
<![CDATA[
private function changeIndex():void
{
var index:int = viewstack.selectedIndex;
index = ( index + 1 > viewstack.content.length - 1 )
? 0 :
index + 1;
viewstack.selectedIndex = index;
}
]]>
</fx:Script>
<s:layout>
<s:VerticalLayout />
</s:layout>
<f4cb:CustomViewStack id="viewstack" width="300" height="300"
skinClass="com.oreilly.f4cb.CustomBorderSkin">
<s:Group id="child1"
width="800" height="100%"
clipAndEnableScrolling="true">
<s:layout>
<s:VerticalLayout horizontalAlign="justify" />
</s:layout>
<s:Button label="top" />
<s:Button label="bottom" bottom="0" />
</s:Group>
<s:Panel id="child2"
width="100%" height="200"
title="Child 2">
<s:Scroller>
<s:Group width="100%" height="100%">
<s:layout>
<s:VerticalLayout horizontalAlign="center" />
</s:layout>
<s:Button label="panel button 1" />
<s:Button label="panel button 2" />
</s:Group>
</s:Scroller>
</s:Panel>
<s:DataGroup id="child3"
width="100%" height="100%"
itemRenderer="spark.skins.spark.DefaultItemRenderer">
<s:layout>
<s:VerticalLayout />
</s:layout>
<s:dataProvider>
<s:ArrayCollection source="{lorem.split(' ')}" />
</s:dataProvider>
</s:DataGroup>
</f4cb:CustomViewStack>
<s:Button label="switch index" click="changeIndex();" />
<s:HGroup>
<s:Button label="select child 1"
enabled="{viewstack.selectedChild != child1}"
click="{viewstack.selectedChild = child1}" />
<s:Button label="select child 2"
enabled="{viewstack.selectedChild != child2}"
click="{viewstack.selectedChild = child2}" />
<s:Button label="select child 3"
enabled="{viewstack.selectedChild != child3}"
click="{viewstack.selectedChild = child3}" />
</s:HGroup>
</s:Application>Children of the CustomViewStack
are declared in markup, but they are added to the defined [DefaultProperty] metatag and are not
initially added to the display list of the
view stack. Instead, it is deferred to the container to create
children as they are requested
using the selectedIndex and
selectedChild properties. The
selectedIndex and selectedChild
properties are bindable and allow for visual and functional updates to
the s:Button controls in the Application container for this
example.
To enable scrolling within the view stack, a custom skin is
applied that fulfills the contract for a BorderContainer-based container. A Group container with a reference id of contentGroup is declared and wrapped within a
Scroller component, as in the
following example:
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark">
<fx:Metadata>
<![CDATA[
[HostComponent("spark.components.BorderContainer")]
]]>
</fx:Metadata>
<s:states>
<s:State name="normal" />
<s:State name="disabled" />
</s:states>
<s:Rect width="100%" height="100%">
<s:stroke>
<s:SolidColorStroke color="#000000" />
</s:stroke>
<s:fill>
<s:SolidColor color="#FFFFFF" />
</s:fill>
</s:Rect>
<s:Scroller width="100%" height="100%"
left="2" right="2" top="2" bottom="2">
<s:Group id="contentGroup"
left="0" right="0" top="0" bottom="0"
minWidth="0" minHeight="0" />
</s:Scroller>
</s:Skin>This example demonstrates a technique for accomplishing deferred instantiation of child elements of a Spark-based navigation container that can be applied to creating equivalents of navigation containers from the MX set within the Flex 4 SDK.
If you enjoyed this excerpt, buy a copy of Flex 4 Cookbook.
