Component Class – Part Five

The last article in this series showed how to write the CycleSelectButton from scratch. In this article I’ll look at styling and skinning the component.

Skins versus Styles

One frequently asked question is "what’s the difference between styles and skins?" This is a good question and it is confusing a bit because you specify a component’s skins using specific styles on the component. For example, the upSkin style on the Button component.

Styles control the appearance of a component while skins are the appearence. Or put another way, skins use styles to present the component. Take the borderColor style. It’s purpose is to specify the color of a component’s edge. The component’s skin can use that style to draw its border – which may be round or square, thick or thin (using the borderThickness style). The skin contains the look of the component.

The reason skins are specified as style is so you can build an entire look or theme using just style sheets (.CSS files). If skins were specified in ActionScript you would have to deliver a new SWF for each theme. Having the skins and other styles in CSS means you can change the theme of an application with a new style sheet.

There are two types of skins: graphical and programmatic. Graphical skins are bitmaps: GIFs, JPGs, PNGs, etc. In Flex 3 you can import graphical skins from Adobe Illustrator and Adobe Photoshop. You can use Adobe Flash CS3 to create animated skins (picture a button that pulses with color).

Programmatic skins are written in ActionScript. They are class files that usually extend mx.skins.ProgrammaticSkin which is a very lightweight class. The class uses its override of updateDisplayList() to render the skin using the drawing API (see flash.display.Graphics).

Each component has its own set of skins. For a Button there are 8 possible skins: upSkin (its normal state), overSkin (when the mouse hovers over it), downSkin (when the mouse is pressed over it), disabledSkin (when the Button’s enabled property is false), selectedUpSkin (when the Button’s toggle property is true), selectedOverSkin (toggle is true and mouse is hovering over the Button), selectedDownSkin (toggle is true and mouse is pressed over it), and selectedDisabledSkin (toggle is true and enabled is false). If you want to use graphical skins for a Button, you should supply 8 different image files. If your Button will never be a toggle, then you can supply just 4 skins.

Download Example
This is a zip file and contains a full Flex Builder 3 project. You will either need Flex Builder 3 from Adobe Labs or you can use Flex Builder 2 and import the sources into a project.   This project contains the source from the previous articles as well.

If you decide to use a programmatic skin you can either make separate skin classes, or use a single class, or a combination. A programmatic skin can detect which skin style it is being used for and code within the programmatic skin class can adjust for it. If, for example, the skin class is being used for a Button’s upSkin, overSkin, downSkin, and disabledSkin, the class can decide to draw a green-filled circle for the upSkin, a blue-filled circle for the overSkin, a blue-filled circle for the downSkin, and a gray-filled circle for the disabledSkin.

You decide what works best for the look you want. You can wind up with a collection of skins – both graphical and programmatic – that make your application look unique (or follow your company’s user interface guidelines).

Applying Skins

Applying the skins is simple. I prefer to do it in a style-sheet to make them easier to change:

Button {
upSkin: Embed('assets/BlueButtonUp.gif');
overSkin: Embed(source='assets/CompanyIcons.swf',symbol='GreenButton');
downSkin: ClassReference('com.mycompany.skins.StandardButtonSkin');
disabledSkin: ClassReference('com.mycompany.skins.StandardButtonSkin');
}

This pretty wild Button has a mixture of skins: one is a GIF, another is a symbol out of a SWF, and two come from the same ActionScript class.

Specifying a graphical skin uses the Embed directive. For a simple image file the Embed names the file using a path that is relative to the application’s main file, or an absolute path within the project. The example above uses a relative path. When a Flash SWF is used, the skin can be the entire SWF file or a specific symbol within the SWF. If you chose to use a specific symbol, the Embed directive names the file and the symbol within it.

Specifying a programmatic skin uses the ClassReference directive. The full class name, including its package, is given for the reference. The compiler will find that class and pull it into the SWF.

The main advantage of programmatic skins over graphical skins is scaling. Because programmatic skins use the Flash Drawing API, the skins scale and rotate very well. Graphical skins can easily become distorted unless you scale9grid specifications in the Embed directive. The scale9grid specifications let you specify a grid overlay on the graphic that tells the Flash Player which parts of the graphic to scale. Think of a rectangle where you want the 4 corners to never scale, the top and bottom to scale only when the graphic is stretched horizontally, the left and right edges to scale when the graphic is stretched vertically, and the center to always scale.

Going back to the CycleSelectButton component, here is how the createChildren() function looks currently:

override protected function createChildren() : void
{
arrows = new Arrows();
arrows.width = 20;
arrows.height= 20;
addChild(arrows);

linkButton = new LinkButton();
addChild(linkButton);

// add a listener for the click on the LinkButton.
linkButton.addEventListener(MouseEvent.CLICK, handleClick);

super.createChildren();
}

To redo this component using skins, you have to think about which parts of the component should be skinable. It seems like a good idea for the circle of arrows to be a skin. Maybe you want to make your own arrows using Photoshop, for instance.

Here is the modified createChildren() function that introduces skins:

			var skin:Class;

skin = getStyle("arrowSkin");
if( skin == null ) skin = CycleSelectArrowSkin;
_arrowSkin = new skin();
_arrowSkin.name = "arrowSkin";
if( _arrowSkin is ProgrammaticSkin ) (_arrowSkin as ProgrammaticSkin).styleName = this;
_arrowSkin.width = 20;
_arrowSkin.height= 20;
addChild(_arrowSkin as DisplayObject);

This is a bit different. First, the value for the arrowSkin style is retrieved. The arrowSkin style is specified by metadata above the class declaration:

[Style(name="arrowSkin",type="Class",inherit="yes")]

Notice that the type of the style data is "Class" – you want to load the class definition for the skin, not just the name of the class. This works for Embed as well since a class is created from the embedded image data.

If no arrowSkin style has been specified, then the default class, CycleSelectArrowSkin, is given. Then the arrowSkin member variable is set with a new instance of whatever skin class was selected. This is the standard way to specify skins using styles.

Once the class instance has been created and arrowSkin is now set, you’ll see it is given a name ("arrowSkin") and its style is set to this. What it means is that the skin will get all of the styles set on the component. For example, the CycleSelectArrowSkin uses a style called "arrowColor" to draw the arrow graphic. There isn’t any way from outside of the CycleSelectButton code to associate this style with the arrow skin; the style is set on the component, along with the arrowSkin style shown above:

	[Style(name="arrowColor",type="Number",format="Color",inherit="yes")]
[Style(name="arrowSkin",type="Class",inherit="yes")]

With the skin inheriting the component’s style, arrowColor among them, the skin code can draw the arrows.

Details

In this section I go through the steps in more detail . I’ll use the sample CycleSelectButton available from the download with this article, but I will only show the skin for the arrows; the skins for the rest of the component work the same way and it will be less confusing to focus on one skin.

Step 1: Figure out what you want the skin to be used for. In this case, it is for the cycle of arrows and by making it a skin, gives a developer the chance to change the look of the component without re-writing the component.

Step 2: In the component class file (CycleSelectButton.as), define the style for the skin above the class definition:

	[Style(name="arrowSkin",type="Class",inherit="yes")]

public class CycleSelectButton extends UIComponent
{

Make sure the type of the style is "Class". The name will be used in the style sheet or on the MXML tag for the component:

StyleSheet.css:

CycleSelectButton {
arrowSkin: ClassReference('com.adobe.examples.skins.CycleSelectArrowSkin');
or
arrowSkin: Embed('assets/ArrowSkin.png');
}

MXML:

<buttons:CycleSelectButton arrowSkin="com.adobe.examples.skins.CycleSelectArrowSkin"... />
or
<buttons:CycleSelectButton arrowSkin="@Embed('assets/ArrowSkin.png')" ... />

Step 3: Declare a member variable to hold the skin instance:

		private var _arrowSkin:IFlexDisplayObject;

Notice that the type of the variable is IFlexDisplayObject – not CycleSelectArrow skin, not UIComponent, and not even ProgrammaticSkin. If you want your skin to be either a programmatic skin or a graphic skin, you need to use a data type that is common to both. IFlexDisplayObject fills that need. It is generic enough, but also allows you to position and size the skin.

Step 4: Create the skin. You can do this either in createChildren() or in commitProperties().

			var skin:Class;

skin = getStyle("arrowSkin");
if( skin == null ) skin = CycleSelectArrowSkin;
_arrowSkin = new skin();
_arrowSkin.name = "arrowSkin";
if( _arrowSkin is ProgrammaticSkin ) (_arrowSkin as ProgrammaticSkin).styleName = this;
_arrowSkin.width = 20;
_arrowSkin.height= 20;
addChild(_arrowSkin as DisplayObject);

The getStyle() function is used to get an alternative skin class (ProgrammaticSkin or graphic) from the styles for the component. This is how a custom skin can be used from a style sheet or MXML tag (from Step 2 above). If no skin was specified getStyle() returns null. In this case a default skin is used. It is important that when using skins you are consistent and create a default skin; creating a skin as a default is always a good idea and perhaps it too can be extended and customized.

Once the skin class is chosen, the arrowSkin member (from Step 3) is set with an instance of this class. Now it is either a graphic skin or a ProgrammaticSkin. If the latter you must set the styleName of the skin to be this (or some other object instance which holds the styles). If you don’t do this, the ProgrammaticSkin will fail when it uses getStyle().

You can size the skin at this step IF you know the size. If your skin is going to occupy the entire component’s space, you can set it within updateDisplayList() (see Step 5).

Finally you add the skin as a child of the component. Note that you have to cast the skin as a DisplayObject since addChild does not accept IFlexDisplayObject parameters.

Step 5: Position (and optionally, size) the skin in updateDisplayList():

		override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
super.updateDisplayList( unscaledWidth, unscaledHeight );

// position the arrowSkin
_arrowSkin.move( 10,10 );

Here the arrowSkin is moved into position. If you were to have a skin that required it to be sized, then you can do that too using skin.setActualSize( width, height ) where the width and height might be unscaledWidth, unscaledHeight or some derivative of those values.

That’s all you need to do to use a skin in your component. Notice that none of the component’s look has been done by the actual component code – it is all done by the skin. This gives your component a tremendous amount of flexability in how it is presented, not in how it behaves.

The Skin Itself

The CycleSelectArrowSkin is one of the files available in the download with this article. Here are some of the highlights:

	public class CycleSelectArrowSkin extends ProgrammaticSkin

The class extends mx.skins.ProgrammaticSkin which extends flash.display.Shape. That’s because skins should be very light-weight and be limited to just presenting graphics. However, one of the most powerful properties of Flex and the Flash Player is its flexability. You do not have to make your skins extend ProgrammaticSkin. You can use any class which implements the IFlexDisplayObject interface.

Since skins are so lightweight there isn’t much else they do except override updateDisplayList():

		override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
super.updateDisplayList( unscaledWidth, unscaledHeight );

var color:Number;

switch( name )
{
case "arrowSkin":
color = getStyle("arrowColor");
if( isNaN(color) ) color = getStyle("themeColor");
break;
case "arrowDisabledSkin":
color = 0xAAAAAA;
break;
}

drawArrows( graphics, color );
}

In this function, the skin’s name is used to determine its color. If the skin is the "arrowSkin" then the color is extracted from the "arrowColor" style. If that was not defined, then the skin’s color defaults to the "themeColor".

Using the themeColor as a default – whether it is the actual color or a darker or lighter version (see mx.utils.ColorUtil class) – is a good idea since that color flows nicely with the style sheet and theme idea.

Once the color is chosen the skin is drawn. The drawArrows function is the same as it was before:

		private function drawArrows( g:Graphics, color:uint ) : void
{
g.clear();
g.lineStyle(0, color, 1);

g.moveTo(-10,0);
g.curveTo(-10,-10,0,-10);
g.curveTo(2.5,-11,8,-8);
g.moveTo(8,-8);
g.lineTo(6.5,-13);
g.moveTo(8,-8);
g.lineTo(2.5,-6);

g.moveTo(10,0);
g.curveTo(10,10,0,10);
g.curveTo(-2.5,11,-8,8);
g.moveTo(-8,8);
g.lineTo(-6.5,13);
g.moveTo(-8,8);
g.lineTo(-2.5,6);
}

Summary

That’s all there is to skinning components:

  • Figure out what you want to skin,
  • extract the drawing into a class based on ProgrammaticSkin,
  • add the skin as a style to your component,
  • create an instance of the skin, and position it.

You should now be able to make reusable components for all of your Flex projects.