The Artima Developer Community
Articles | Discuss | Print | Email | Screen Friendly Version | Previous | Next
This article is sponsored by Adobe.

Creating a Custom Look and Feel for Flex 4 Components
by Frank Sommers
September 1, 2009

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.

Halo vs Spark

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:

To view this page ensure that Adobe Flash Player version 10.0.0 or greater is installed.

Get Adobe Flash Player

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.

Controller and View Separation

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).

Skin Parts and State

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.

Putting It All Together

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.

Share Your Opinion

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.

Resources

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

About the Authors

Frank Sommers is Editor-in-Chief of Artima.

Articles | Discuss | Print | Email | Screen Friendly Version | Previous | Next

This article is sponsored by Adobe.

Copyright © 1996-2014 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use - Advertise with Us