|
|
|
This article is sponsored by Adobe.
|
|
Advertisements
|
||
|
Summary
An important feature in Flex 4 is a new component architecture, Spark, that allows a complete separation of a component's view from its display logic. This article provides a tutorial introduction to creating custom Flex 4 skins using the Spark architecture.
A key innovation in the Flex 4 SDK is a thorough separation of a component's visual appearance from its display logic. By contrast, previous Flex versions required that a component be defined in a single MXML or ActionScript file: component layout, possible subcomponents, logic defining how a component should behave in the presence of data, as well as styling, all could be provided in a single component definition. The ability to reference external stylesheet files provided a modest measure of controller-view separation in earlier Flex versions.
While convenient, limited model-view separation in previous Flex SDKs also meant that developers interested in providing a custom look and feel—or skins—for their Flex components had to use FlexBuilder or similar Flex-centric developer tools to work with the visual aspects of a component. That, in turn, made it difficult for developers and designers to work together on a Flex application, since developers and designers are accustomed to different sets of tools. Flex 4 solves that problem by defining a new component architecture, Spark, that separates component logic and view into different artifacts. These artifacts are tailored to work well with developer and designer tools, respectively.
This article provides a tutorial on how to take advantage of Spark architecture features to design a custom look-and-feel for a Flex component. A custom look-and-feel brings to a Flex application benefits beyond visual pizzazz. For example, having a separate view definition allows an application to swap component skins at runtime and to radically alter almost every visual aspect of a component, such as layout, font sizing, and so on. Such a modular approach to component design, in turn, makes it easy to build Flex applications that gracefully adapt to their runtime environments, such as varying display sizes or the requirements of mobile devices.
Although becoming familiar with just a handful of Spark architecture concepts makes skinnable component development feel natural, Flex 4 does not require that every component in a Flex application follow the new architecture. Flex 4 applications can continue using components based on the earlier Flex component architecture, Halo, although the best practice is to use Spark components, whenever possible. In addition, it is also possible to mix and match Halo and Spark components within the same Flex application. Such component interoperability was a key requirement for Flex 4, as it enables seamless migration of older Flex applications to Flex 4.
When moving an existing Flex application to Flex 4, you can keep using your Halo-based components, which will continue to work as expected. At the same time, an instructive way to introduce the benefits of the new Spark architecture is to migrate an existing Halo-based Flex component to Spark. This article's example builds on the temperature converter application introduced in the earlier Artima article, Two-Way Data Binding in Flex 4. The simple application converts temperature values from Fahrenheit to Celsius and vice versa:
Enter a numeric value into either field and press enter. Press Toggle to enable or disable the component. To view the source code, right-click or control-click on the application.
Several applications may wish to make use of the temperature conversion functionality. It is advantageous, therefore, to define the converter as a Flex component so that it can be reused in any Flex application. Using Flex 3's Halo component architecture, one way to define such a component is as follows:
<?xml version="1.0" encoding="utf-8"?>
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
private function onCelsiusEntered(e: Event): void {
fahrenheitInput.text = (Number(celsiusInput.text) * 9/5 + 32) + "";
}
private function onFahrenheitEntered(e: Event): void {
celsiusInput.text = ((Number(fahrenheitInput.text) - 32) * 5/9) + "";
}
]]>
</mx:Script>
<mx:Form>
<mx:FormItem label="Celsius:">
<mx:TextInput id="celsiusInput" change="onCelsiusEntered(event)"/>
</mx:FormItem>
<mx:FormItem label="Fahrenheit:">
<mx:TextInput id="fahrenheitInput" change="onFahrenheitEntered(event)"/>
<mx:FormItem>
</mx:Form>
</mx:VBox>
A notable feature of this component definition is that it combines layout as
well as display logic. The component itself extends the VBox
container, and includes a single Form subcomponent that lays out
two text labels and two text input fields. The display logic—or
controller—part of the component is defined in ActionScript code inside
the Script tags: The two event handler methods capture input from
the Celsius or Fahrenheit input fields, respectively, and update the opposite
field's value.
The Halo-based converter component can be used in any Flex 3 application as follows:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*"> <local:Converter id="converter"/> </mx:Application>
The convenience of being able to provide such an all-in-one definition for a component comes at the cost of some flexibility, however. Consider, for instance, that some users would prefer the Fahrenheit input field to appear before the Celsius one, based on regional preferences. In the current design, the order of the text input fields is baked into the component, so to speak: You would have to define a new version of the entire component to achieve that requirement, duplicating a significant portion of the component's code. Even with two components, it would require tedious, boilerplate code to determine at runtime which of the two component to display based on the user's preferences.
Such inflexibilities in a component's presentation can become a problem as
Flex components are embedded into a larger application. For instance,
application requirements may dictate that the temperature converter application
enter a disabled state—a state in which it cannot accept user input. That
requirement can be implemented by setting the component's enabled
property to false, as the following example shows:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*">
<mx:VBox>
<local:Converter id="converter"/>
<mx:Button label="Toggle" click="converter.enabled = !converter.enabled"/>
</mx:VBox>
</mx:Application>
Using the Halo-based component design, setting the entire component to a
disabled state yields somewhat unattractive results, with the
entire application assuming a different background color. It would be better to
handle a disabled state more gracefully by disallowing input only on the text
fields, and leaving the component's background color unchanged. Installing a
component state listener could achieve that effect—however, that would add
further display-specific code to the entire component.
The Spark architecture addresses these, and many other, component limitations in an elegant way: The visual and display-logic aspects of the application are split into separate files. The two component definitions work together by each adhering to a contract defined in the Spark architecture. The rest of this article will illustrate how to refactor the temperature converter component to one based on the Spark architecture.
It is not always obvious what aspect of a Flex component belong in the view and what aspects should be defined inside the controller. As a general principle, behavior intended to be reused regardless of visual changes to the application belong in the controller; component aspects envisioned to change based on customized presentation, on the other hand, should be defined in the view.
Regardless of how the two text fields are displayed in the temperature converter component, entering text into one field should cause the value in the other field to change. That functionality is a good candidate for inclusion in the controller, since that behavior should be consistent across presentation. On other hand, the display and layout order of the text fields are better defined in the view.
The view aspects of a Spark component are defined in class that extends
SparkSkin, the root of the Spark skin hierarchy. A
SparkSkin is often defined in MXML. In fact, the relatively simple
XML structure of a skin is a key enabler when editing Flex skin definitions in
tools, such as Adobe's Catalyst or Illustrator.
A skin file contains every display-related aspect of a component, such as graphic elements, subcomponents, layouts, images, transitions, and so on. A novel feature of Spark skins is that they can include a new XML-based declarative graphics language for the Flash Player, FGX. FGX allows you to declare a wide range of graphics operations using XML tags—data structures that design tools can easily read and write. FGX-based declarations are translated by the Flex compiler to efficient Flash Player graphics bytecode, making FGX a good tool for defining the graphics-related aspects of a component.
Another useful feature of Spark skins that they allow developers to define skins and components in a modular fashion. Indeed, most Flex components consist of multiple subcomponents. Each subcomponent may have its independent skinning definition. The Flex runtime matches a component's skinnable sub-components with skin parts declared in the skin.
To see how this works in practice, consider the following skin definition for the temperature converter:
<?xml version="1.0" encoding="utf-8"?>
<s:SparkSkin xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/halo"
width="400" height="300">
<fx:Metadata>
[HostComponent("DegreeConverter")]
</fx:Metadata>
<s:states>
<s:State name="normal"/>
<s:State name="disabled"/>
</s:states>
<mx:Form>
<mx:FormItem label="Celsius:" alpha.disabled="0.5">
<s:TextInput id="celsiusInput" enabled.disabled="false"/>
<mx:FormItem>
<mx:FormItem label="Fahrenheit:" alpha.disabled="0.5">
<s:TextInput id="fahrenheitInput" enabled.disabled="false"/>
<mx:FormItem>
</mx:Form>
</s:SparkSkin>
The first element of this skin definition is metadata identifying the host
component for this skin. While optional, such a declaration enables the skin to
have access to the host component itself via the hostComponent
property.
A declaration of two skin states comes next. In Spark-based components a skin and a component each maintains its own state. As the component state changes, the component can notify its associated skin to change the skin state, too. The temperature converter component has only two states: One for the enabled status of the component, and the other one for the disabled state.
These states can be referred to anywhere inside the Flex skin definition using
the new Flex 4 state syntax: the state name, followed by a dot, followed by the
name of the property, and finally followed by the value the property should
assume in the specified state. For instance, the temperature converter skin
specifies that each FormItem should have an alpha
value of 0.5 in the disabled state, and that the text input boxes'
disabled property values in the enabled component
state should be false (in other words, the input fields should be
disabled when the component is disabled, and enabled when the component is
enabled).
Having defined a component skin, the next task is to code up the component
itself. Skinnable Spark components extend the SkinnableComponent
class. As the following implementation shows, there is no display-related code
in the component itself:
package com.artima {
import flash.events.Event;
import spark.components.TextInput;
import spark.components.supportClasses.SkinnableComponent;
[SkinState("normal")]
[SkinState("disabled")]
public class DegreeConverter extends SkinnableComponent {
[SkinPart(required="true")]
public var celsiusInput: TextInput;
[SkinPart(required="true")]
public var fahrenheitInput: TextInput;
override public function set enabled(value:Boolean) : void {
if (enabled != value)
invalidateSkinState();
super.enabled = value;
}
override protected function getCurrentSkinState() : String {
if (!enabled)
return "disabled";
return "normal";
}
override protected function partAdded(partName: String, instance: Object): void {
if (instance == celsiusInput)
celsiusInput.addEventListener(Event.CHANGE, onCelsiusInput);
if (instance == fahrenheitInput)
fahrenheitInput.addEventListener(Event.CHANGE, onFahrenheitInput);
}
override protected function partRemoved(partName:String, instance:Object) : void {
if (instance == celsiusInput)
celsiusInput.removeEventListener(Event.CHANGE, onCelsiusInput);
if (instance == fahrenheitInput)
fahrenheitInput.removeEventListener(Event.CHANGE, onFahrenheitInput);
}
private function onCelsiusInput(e: Event): void {
fahrenheitInput.text = (Number(celsiusInput.text) * 9/5 + 32) + "";
}
private function onFahrenheitInput(e: Event): void {
celsiusInput.text = ((Number(fahrenheitInput.text) - 32) * 5/9) + "";
}
}
}
The component's code centers around interacting with skin parts and managing
component state. As mentioned earlier, skin parts facilitate a modular approach
component design. The temperature converter's two text input fields are defined
as skin parts so that they can easily be referenced between the skin and the
component: Declaring the same id value in the skin as the
component's property name allows the Flex runtime to automatically associate a
skin element with sub-components inside a Spark component. For this association
to work, the SkinPart metadata must be attached to the
component's property. In this example, fahrenheitInput and
celsiusInput both have that annotation. The skin's corresponding
text fields, in turn, have fahrenheitInput and
celsiusInput id values.
In addition to associating skin elements with sub-components, skin parts play
another important role in the lifecycle of a Spark component: Skin parts can be
associated with component state, and adding and removing skin parts from a
component—perhaps as a result of changing component state—causes the Flex
runtime to call the component's partAdded() and
partRemoved() methods. Implementing those methods, in turn, allows
a component author to interact with the newly added sub-component, often for
the purpose of adding and removing event listeners.
The example code above adds event listeners to each text input field as those fields are added to the component, and removes those listeners if the input fields are removed as well.
This simple temperature converter example has only two application states:
disabled and normal. These states are declared both
in the skin and in the component as metadata elements. The Flex compiler
requires that a Flex skin reference only valid component states. The
component's current state is provided to the skin via the
getCurrentState() method. Since the enabled property
is defined for all Flex UIComponents, we must override that
property's setter method and cause the skin to request the current state value
by calling invalidateSkinState(). When the skin obtains the
current component state, it matches that state with the corresponding state
defined in the skin. The Flex runtime then ensure that all the skin property
values are set according to the specified state.
The skin and component files together define the temperature converter Spark component. The simplest way to use this component from a Flex application is to associate a skin with the component inside an MXML declaration:
<?xml version="1.0" encoding="utf-8"?>
<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/halo"
minWidth="1024" minHeight="768"
xmlns:artima="com.artima.*">
<fx:Script>
<![CDATA[
import com.artima.ConverterSkin;
]]>
</fx:Script>
<s:Group>
<s:layout>
<s:VerticalLayout/>
</s:layout>
<artima:DegreeConverter skinClass="com.artima.ConverterSkin" id="converter"/>
<s:Button label="Toggle" click="converter.enabled = !converter.enabled"/>
</s:Group>
</s:Application>
Alternate ways of assigning the component's skinClass property
includes ActionScript and CSS. Having defined a separate skin for a component
allows a Flex application to switch skins at runtime, or to pick the right skin
when the application starts up. For instance, an alternate skin can be
specified to reverse the order of the text input fields; such a skin class
could then be assigned to the component's skinClass property at
runtime.
Experimenting with this simple skinned component already shows improvement over
the older version: When the component is placed in a disabled
state, the text fields' alpha values are set to 0.5, providing a smooth,
semi-transparent appearance.
Have an opinion on styling Flex Spark components? Discuss this article in the Articles Forum topic, Creating a Custom Look and Feel for Flex 4 Components.
Adobe's Flash Builder 4
http://labs.adobe.com/technologies/flashbuilder4
Flex 4 SDK
http://opensource.adobe.com/wiki/display/flexsdk/Flex+SDK
Gumbo Project
http://opensource.adobe.com/wiki/display/flexsdk/Gumbo
Flex.org
http://www.flex.org
Frank Sommers is Editor-in-Chief of Artima.
|
This article is sponsored by Adobe.
|