Skinning Spark components

All Spark components and subcomponents that are subclasses of SkinnableComponent can be reskinned. Common tasks when reskinning Spark components include adding borders, drop shadows, and transitions to a skin class.

Setting minimum sizes of a skin

A common task when creating Spark skins is to set minimum sizes of the skin. This causes the Flex layout to not reduce the size of a component below a pre-defined width or height. You do this with the minWidth and minHeight properties on the skin class’s top-level element.

In general, you should not change the values of the minimum height and minimum width properties once they are set.

The following example creates a custom Button skin that has a minimum width of 100 pixels and a minimum height of 100 pixels.
<?xml version="1.0"?> 
<!-- SparkSkinning/SquareButtonExample.mxml --> 
<s:Application   
    xmlns:fx="http://ns.adobe.com/mxml/2009"    
    xmlns:mx="library://ns.adobe.com/flex/mx"     
    xmlns:s="library://ns.adobe.com/flex/spark"> 
    
    <s:Button id="myButton" skinClass="mySkins.SquareButtonSkin" label="Click Me"/> 
 
</s:Application>

The executing SWF file for the previous example is shown below:

The following is the custom skin class used for this example:
<?xml version="1.0" encoding="utf-8"?>
<!-- SparkSkinning/mySkins/SquareButtonSkin.mxml -->
<s:SparkSkin 
    xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    minWidth="100" minHeight="100"
    alpha.disabled="0.5">

    <fx:Metadata>
        [HostComponent("spark.components.Button")]
    </fx:Metadata>
    
    <fx:Script>
        <![CDATA[         
            static private const exclusions:Array = ["labelDisplay"];
            override public function get colorizeExclusions():Array {return exclusions;}
        ]]>        
    </fx:Script>
    
    <s:states>
        <s:State name="up" />
        <s:State name="over" />
        <s:State name="down" />
        <s:State name="disabled" />
    </s:states>
    
    <!-- layer 1: shadow -->
    <s:Rect left="-1" right="-1" top="-1" bottom="-1" radiusX="2" radiusY="2">
        <s:fill>
            <s:LinearGradient rotation="90">
                    <s:GradientEntry color="0x000000" 
                                   color.down="0xFFFFFF"
                                   alpha="0.01"
                                   alpha.down="0" />
                    <s:GradientEntry color="0x000000" 
                                   color.down="0xFFFFFF" 
                                   alpha="0.07"
                                   alpha.down="0.5" />
            </s:LinearGradient>
        </s:fill>
    </s:Rect>
    
    <!-- layer 2: fill -->
    <s:Rect left="1" right="1" top="1" bottom="1" radiusX="2" radiusY="2">
        <s:fill>
            <s:LinearGradient rotation="90">
                <s:GradientEntry color="0xFFFFFF" 
                               color.over="0xBBBDBD" 
                               color.down="0xAAAAAA" 
                               alpha="0.85" />
                <s:GradientEntry color="0xD8D8D8" 
                               color.over="0x9FA0A1" 
                               color.down="0x929496" 
                               alpha="0.85" />
            </s:LinearGradient>
        </s:fill>
    </s:Rect>
    
    <!-- layer 2: border -->
    <s:Rect left="0" right="0" top="0" bottom="0" width="69" height="20" radiusX="2" radiusY="2">
        <s:stroke>
            <s:LinearGradientStroke rotation="90" weight="1">
                <s:GradientEntry color="0x000000" 
                               alpha="0.5625"
                               alpha.down="0.6375" />
                <s:GradientEntry color="0x000000" 
                               alpha="0.75" 
                               alpha.down="0.85" />
            </s:LinearGradientStroke>
        </s:stroke>
    </s:Rect>

    <s:Label id="labelDisplay"
             textAlign="center"
             verticalAlign="middle"
             lineBreak="toFit"
             horizontalCenter="0" verticalCenter="1"
             left="10" right="10" top="2" bottom="2">
    </s:Label>
    
</s:SparkSkin>
The default Button skin’s minimum size is 21 pixels wide by 21 pixels high. These minimums are set on the root tag of the skin class, as the following example shows:
<s:SparkSkin 
    xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    minWidth="21" minHeight="21" 
    alpha.disabled="0.5"> 
    ... 
</s:SparkSkin>
You might notice that in the default button skin’s class, though, the Rect border sets its width to a value that is larger than the minimum width on the skin:
 <s:Rect id="border" left="0" right="0" top="0" bottom="0" width="69" height="20" radiusX="2">

The width property on the Rect border is used for buttons whose size is unset. The minWidth property on the SparkSkin is used when the button’s width is variable, meaning that it hasn’t been explicitly set. The previous example sets a default width and height of 69 and 20. However, it is pinned to (left=0, right=0, top=0, bottom=0) so that if the label is too large or the button is explicitly sized, then this rectangle will resize.

Accessing application properties

In addition to accessing the host component’s properties, you can access properties of the application. This is useful if there are global settings that you want to access, or runtime information that is passed into the application that you want available in the skin class.

To access global variables in a custom skin, you use the FlexGlobals.topLevelApplication property. Using this property gives you access to all global variables, including variables that were passed into the application as flashVars variables.

The following example accesses a String that is a global variable on the application:
<?xml version="1.0" encoding="utf-8"?>
<!-- SparkSkinning/GlobalVariableAccessorExample.mxml -->
<s:Application 
    xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:s="library://ns.adobe.com/flex/spark">

    <fx:Script>
        public var myLabelString:String = "Hello World";
    </fx:Script>

    <s:Button skinClass="mySkins.GlobalVariableAccessorSkin"/>

</s:Application>

The executing SWF file for the previous example is shown below:

The following is the custom skin class for this example:
<?xml version="1.0" encoding="utf-8"?>
<!-- SparkSkinning\mySkins\GlobalVariableAccessorSkin.mxml -->
<s:Skin 
    xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    minWidth="21" minHeight="21">

    <fx:Metadata>
        [HostComponent("spark.components.Button")]
    </fx:Metadata> 

    <fx:Script>
        import mx.core.FlexGlobals;        
        
        [Bindable]
        private var localString:String = FlexGlobals.topLevelApplication.myLabelString;
    </fx:Script>
    
    <!-- Specify one state for each SkinState metadata in the host component's class -->
    <s:states>
        <s:State name="up"/>
        <s:State name="over"/>
        <s:State name="down"/>
        <s:State name="disabled"/>
    </s:states>

    <s:Rect 
        left="0" right="0" 
        top="0" bottom="0" 
        width="69" height="20" 
        radiusX="2" radiusY="2">
        <s:stroke>
            <s:SolidColorStroke color="0x000000" weight="1"/>
        </s:stroke>
    </s:Rect>
    
    <s:Label id="labelDisplay" 
        text="{localString}"
        horizontalCenter="0" verticalCenter="1"
        left="10" right="10" top="2" bottom="2">
    </s:Label>
</s:Skin>

Using style properties in custom skins

Within the skin, you can get the value of certain style properties on the host component. The component declares what styles can be set so that they can be set inline in MXML. For example, you can set the chromeColor property on all skins that extend the SparkSkin class. On other skins, such as container skins, you can set the backgroundColor.

The following example sets the color of the border inside the skin to the same value as the Button’s color style property:
<?xml version="1.0" encoding="utf-8"?>
<!-- SparkSkinning/BorderColorButtonExample.mxml -->
<s:Application 
    xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:s="library://ns.adobe.com/flex/spark">

    <s:Button id="myButton" 
        label="Basic Button" 
        skinClass="mySkins.BorderColorButtonSkin" 
        color="green"/>

</s:Application>

The executing SWF file for the previous example is shown below:

The following is the skin class for this example:

<?xml version="1.0" encoding="utf-8"?>
<!-- SparkSkinning\mySkins\BorderColorButtonSkin.mxml -->
<s:Skin 
    xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    minWidth="21" minHeight="21" 
    creationComplete="initSkin()">
    
    <fx:Script>
        private function initSkin():void {
            /* Note that because color can change, you could override the 
                updateDisplayList() method and set it there so that the 
                color is updated in the skin when it is updated on the 
                host component. */ 
            outline.color = getStyle('color');
        }
    </fx:Script>
    
    <fx:Metadata>
        [HostComponent("spark.components.Button")]
    </fx:Metadata> 
    
    <!-- Specify one state for each SkinState metadata in the host component's class -->
    <s:states>
        <s:State name="up"/>
        <s:State name="over"/>
        <s:State name="down"/>
        <s:State name="disabled"/>
    </s:states>

    <s:Rect left="0" right="0" top="0" bottom="0" width="69" height="20" radiusX="2" radiusY="2">
        <s:stroke>
            <s:SolidColorStroke id="outline" weight="1"/>
        </s:stroke>
    </s:Rect>
    
    <s:Label id="labelDisplay"
        horizontalCenter="0" verticalCenter="1"
        left="10" right="10" top="2" bottom="2">
    </s:Label>
</s:Skin>
You can use a binding expression to set properties in the skin based on values in the host component. The following example binds the value of the stroke’s color property to the host component’s color style property:
<?xml version="1.0" encoding="utf-8"?>
<!-- SparkSkinning/StyleWatcherExample.mxml -->
<s:Application    
    xmlns:fx="http://ns.adobe.com/mxml/2009"     
    xmlns:mx="library://ns.adobe.com/flex/mx"     
    xmlns:s="library://ns.adobe.com/flex/spark">

    <s:Button id="myButton" 
        label="Basic Button" 
        skinClass="mySkins.StyleWatcherSkin" 
        click="myButton.setStyle('color','red')" 
        color="green"/>

</s:Application>

The executing SWF file for the previous example is shown below:

The following is the skin class for this example:
<?xml version="1.0" encoding="utf-8"?>
<!-- SparkSkinning\mySkins\StyleWatcherSkin.mxml -->
<s:Skin 
    xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    minWidth="21" minHeight="21">
    
    <fx:Metadata>
        [HostComponent("spark.components.Button")]
    </fx:Metadata> 
    
    <!-- Specify one state for each SkinState metadata in the host component's class -->
    <s:states>
        <s:State name="up"/>
        <s:State name="over"/>
        <s:State name="down"/>
        <s:State name="disabled"/>
    </s:states>

    <s:Rect left="0" right="0" top="0" bottom="0" width="69" height="20" radiusX="2" radiusY="2">
        <s:stroke>
            <!-- Match the border color to the button label's color. -->
            <s:SolidColorStroke color="{getStyle('color')}" weight="1"/>
        </s:stroke>
    </s:Rect>
    
    <s:Label id="labelDisplay"
        horizontalCenter="0" verticalCenter="1"
        left="10" right="10" top="2" bottom="2">
    </s:Label>
</s:Skin>

This example sets the color of the stroke when the application starts. When you click the Button, the color property later changes while the application is running because style properties are bindable.

For custom components, you must declare the style metadata on the component itself so that inline MXML styling will work.

To expose a property as a style, you can also define a getter and setter for the property. You then override the styleChanged() method to dispatch the binding change event. In the application, you call the setStyle() method on the instance’s skin property, which sets the value of the style property on the skin.

The following example exposes a custom style property called borderColor on the custom skin:
<?xml version="1.0" encoding="utf-8"?>
<!-- SparkSkinning/mySkins/StyleableBorderSkin.mxml -->
<s:Skin 
    xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    alpha.disabled="0.5">
    
    <fx:Script>        
        [Bindable("borderColorChange")]
        
        public function get borderColor():uint {
            return getStyle("borderColor");
        }
        
        public function set borderColor(value:uint):void {
            setStyle("borderColor", value);
        }

        override public function styleChanged(styleProp:String):void {
            super.styleChanged(styleProp);

            if (styleProp == "borderColor" || styleProp == null)
                dispatchEvent(new Event("borderColorChange"));
        }
    </fx:Script>    
    <fx:Metadata>
        [HostComponent("spark.components.SkinnableContainer")]
    </fx:Metadata>     
    <s:states>
        <s:State name="normal" />
        <s:State name="disabled" />
    </s:states>    
    <s:Group id="contentGroup" left="0" right="0" top="0" bottom="0">
        <s:layout>
            <s:VerticalLayout/>
        </s:layout>
    </s:Group>
    <s:Rect left="0" right="0" top="0" bottom="0">
        <s:stroke>
            <s:SolidColorStroke color="{borderColor}" weight="1"/>
        </s:stroke>
    </s:Rect>
</s:Skin>
In the following application, you set the color of the border by using the ColorPicker control. This control uses the setStyle() method to change the color of the container’s border.
<?xml version="1.0" encoding="utf-8"?>
<!-- SparkSkinning/StyleableBorderExample.mxml -->
<s:Application 
    xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:s="library://ns.adobe.com/flex/spark" height="250" width="450">

    <s:layout>
        <s:HorizontalLayout/>
    </s:layout>

    <fx:Script>
        private function setSkinStyles(e:Event):void {
            myContainer.setStyle("borderColor",e.currentTarget.selectedColor);
        }
    </fx:Script>

    <s:SkinnableContainer id="myContainer" 
        height="200" width="200" 
        skinClass="mySkins.StyleableBorderSkin">        
    </s:SkinnableContainer>
        
    <mx:ColorPicker id="myColorPicker" change="setSkinStyles(event)"/>

</s:Application>

The executing SWF file for the previous example is shown below:

Another way to expose style properties to the skin is to add code to the updateDisplayList() method to manually push style values into the skin’s graphics. This is the more efficient method of pushing style properties to skins because it does not rely on binding. The following example overrides the updateDisplayList() method to push the background color’s style property:
<fx:Script> 
    override protected function updateDisplayList(unscaleWidth:Number, unscaledHeight:Number):void { 
        // Push style values into the graphics properties before calling super.updateDisplayList 
        backgroundFill.color = getStyle("backgroundColor"); 
        // Call super.updateDisplayList to do the rest of the work 
        super.updateDisplayList(unscaledWidth, unscaledHeight); 
    } 
</fx:Script> 
<s:Rect left="0" right="0" top="0" bottom="0"> 
    <s:SolidColor id="backgroundFill" /> 
</s:Rect>

Using events in custom skins

In general, you should use states to react to user interaction. If a skin part is removed from display list whenever the mouse hovers over a control, then that part cannot receive mouseDown events. Some skins, such as the Button control’s skin, block user interaction to the skin so that the underlying control only can dispatch events. This means that you cannot trigger events defined on InteractiveObject on the skin. These include user interaction events such as mouseDown, mouseOut, and click.

You can trigger events defined on UIComponent on the skin, however. These events include creationComplete and other lifecycle events.

A Button control’s default skin defines several Rect elements and a Label control. These simple classes do not support any events. To add lifecycle event listeners, you can wrap a simple element or series of elements in a Group tag. This lets you register lifecycle events that are defined on UIComponent.

The following example shows a Group inside the skin that triggers a creationComplete event:
<?xml version="1.0" encoding="utf-8"?>
<!-- SparkSkinning/mySkins/LifecycleEventExample.mxml -->
<s:Application 
    xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    height="200" width="200">
    <s:layout>
        <s:HorizontalLayout/>
    </s:layout>

    <s:Group width="150" height="100">
        <s:Button label="Click It or Ticket" skinClass="mySkins.LifecycleEventSkin"/>
    </s:Group>
</s:Application>

The executing SWF file for the previous example is shown below:

The custom skin class for this example is as follows:
<?xml version="1.0" encoding="utf-8"?>
<!-- SparkSkinning\mySkins\LifecycleEventSkin.mxml -->
<s:Skin 
    xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    minWidth="21" minHeight="21">

    <fx:Metadata>
        [HostComponent("spark.components.Button")]
    </fx:Metadata> 
    
    <s:states>
        <s:State name="up"/>
        <s:State name="over"/>
        <s:State name="down"/>
        <s:State name="disabled"/>
    </s:states>

    <s:Rect id="buttonBorder" width="100%" height="20" radiusX="2" radiusY="2">
        <s:stroke>
            <mx:SolidColorStroke color="0x000000" weight="1" weight.over="2"/>
        </s:stroke>
    </s:Rect>
    
    <s:Group creationComplete="trace('creationComplete')">
        <s:Label id="labelDisplay"
            horizontalCenter="0" verticalCenter="1"
            left="10" right="10" top="6" bottom="2">
        </s:Label>
    </s:Group>
</s:Skin>