I've trodded through several examples online and am assembling this tutorial a as way for developers to get started utilizing skins. There are two primary types of skinning to get started with. The first is skinning a native Flex component. This takes place when you want to skin an existing component such as the spark.components.Button class.
The second type of skinning is for Flex custom components. Skinning a custom component is much the same as skinning a native Flex component. Your custom component can extend any class that extends SkinnableComponent. When using Spark components, this is pretty much any visual component (another great reason for you to deprecate using mx based components in your apps for their spark counterparts). A recent example I needed to skin was a button with multiple images. I was able to create a subclass of spark.components.Button and add my images easily, unlike the hoops Flex 3 imposed.
Spark introduced a few new base containers for our use. spark.components.SkinnableContainer works like a Canvas, but it is a subclass of SkinnableComponent (note that Group is the spark counterpart to Canvas, but is not skinnable). For our example we will be extending SkinnableContainer to create a custom 'mirror' component. For kicks we are also skinning a Button. This example is easy so as not to delve to deeply at first, but should get you started to more in-depth topics like skin states. The final product looks like this:
First, we are going to tackle the Button skin. Flash Builder's workflow for creating a skin is very straightforward. Name your skin and pick what component (or component skin it models, and Flash Builder will copy the contents of the base skin into your new file. To create our button skin in Flash Builder, we select New > MXML Skin. After naming our skin we can select the host component.
The skin includes many sections, two of which we will focus on immediately:
32: [HostComponent("spark.components.Button")]
HostComponent tells the skin what component we are basing our skin on, and therefore gives us access to the properties of that class for use within the skin itself like style definitions. You can technically refer to any property including data proerties, but since we are trying to separate our view layer, that seems counterintuitive ;) The Button class itself defines SkinParts, which are a powerful way to separate all data from the design (more on SkinParts in our other example below). For our button we want to add an image, which is very easy to do. We could also change any SkinPart (like shadow) or state (like down or over), or even modify SkinParts only within a specific state (like what the shadow looks like when you hover over the button).
The second part we will look at is the MXML that defines the Button component view. In a Skin we can add any markup. Within a Skin each component has "includeIn" and "excludeFrom" attributes to determine when a component should be visible. If the component should be visible at all times, do not use either attribute. To add an image we use the spark.primitives.BitMapImage class:
209: <s:BitmapImage source="@Embed('assets/nav_refresh_blue.png')" top="5" bottom="5" left="5" />
Now that our button skin is ready to go, we need to create our ImageButton component. We are creating the ImageButton component as an MXML file in this instance, but could also create it in ActionScript (which is shown in the next example).
ImageButton.mxml
1: <?xml version="1.0" encoding="utf-8"?>
2: <s:Button xmlns:fx="http://ns.adobe.com/mxml/2009"
3: xmlns:s="library://ns.adobe.com/flex/spark"
4: xmlns:mx="library://ns.adobe.com/flex/mx" skinClass="com.nictunney.skindemo.view.skins.ImageButtonSkin">
5: </s:Button>
6:
Skins are bound to components using the "skinClass" attribute. There is absolutely nothing else to note here now, but we could add any properties or functionality in this file. Implementation of this ImageButton class will be shown at the end of this post.
For the next example we will be skinning a custom Flex component named MirrorGroup. The component doesn't do much other than display text, and then mirror the text back on the same line, reversed. To show that the component does not have to be created in MXML, this example uses all ActionScript. Our custom component extends SkinnableComponent so we can inherit the skinnable actions and parts of that base class. The component itself is simple, with caveats:
MirrorGroup.as
1:
2: package com.nictunney.skindemo.view
3: {
4: import com.nictunney.skindemo.view.skins.MirrorGroupSkin;
5:
6: import spark.components.Label;
7: import spark.components.supportClasses.SkinnableComponent;
8:
9: public class MirrorGroup extends SkinnableComponent
10: {
11: [SkinPart(required="true")]
12: public var plainText:Label;
13: [SkinPart(required="true")]
14: public var mirrorText:Label;
15:
16: public var content:String;
17:
18: override public function stylesInitialized():void {
19: super.stylesInitialized();
20: this.setStyle("skinClass",Class(com.nictunney.skindemo.view.skins.MirrorGroupSkin));
21: }
22:
23: override protected function partAdded(partName:String, instance:Object):void
24: {
25: super.partAdded(partName, instance);
26:
27: if( instance == plainText || instance == mirrorText )
28: {
29: instance.text = content;
30: }
31: }
32:
33: }
34: }
SkinParts are really cool (lines 11-14). What they define is a contract between the designer and the developer. The developer provides a list of SkinParts to the designer as IDs, Flex component type, and if they must implement the SkinPart. The designer then creates the skin with those matching IDs and types. The SkinPart metadata tag in the MirrorGroup component tells Flex to join them up, hence linking data to view at runtime. I know this workflow was completely developer focused and backwards, but you get the idea ;)
The caveats to using an AS3 class instead of MXML that just plain sucked (until I found the right blog posts):
- Since we are attaching the skin in ActionScript, we must override the public stylesInitialized() method (lines 18-21). This code comes from SEFOL. In ImageButton.mxml this was not necessary as we specified the skinClass attribute in the component definition.
- We need to override the protected partAdded() method and delay processing of any properties of a SkinPart until they are added to our component from the Skin (lines 23-31). If you do not do this you will get errors complaining about null references. This code comes from Ryan Stewart.
The differences from when we created the button example are that we can specify the custom component MirrorGroup as the HostComponent, and we specify that it should create our Skin file as a copy of SkinnableContainerSkin, which is the default skin for SkinnableContainer, which we extended to create MirrorGroup. Using the base skin gets us light years ahead. Here is the finished Skin:
MirrorGroupSkin.mxml (stripped out boilerplate comments and ActionScript for brevity)
1: <?xml version="1.0" encoding="utf-8"?>
2:
3: <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark"
4: xmlns:fb="http://ns.adobe.com/flashbuilder/2009" alpha.disabled="0.5">
5: <fx:Metadata>[HostComponent("com.nictunney.skindemo.view.MirrorGroup")]</fx:Metadata>
6:
7: <s:states>
8: <s:State name="normal" />
9: <s:State name="disabled" />
10: </s:states>
11:
12: <!--- Defines the appearance of the SkinnableContainer class's background. -->
13: <s:Rect id="background" left="0" right="0" top="0" bottom="0">
14: <s:fill>
15: <!--- @private -->
16: <s:SolidColor id="bgFill" color="#FFFFFF"/>
17: </s:fill>
18: </s:Rect>
19:
20: <s:HGroup id="contentGroup" left="10" right="0" top="10" bottom="0" minWidth="0" minHeight="0">
21: <s:Label id="plainText" />
22: <s:Label id="mirrorText" layoutDirection="rtl" alpha="0.3" />
23: </s:HGroup>
24:
25: </s:Skin>
26:
Line 5 shows the HostComponent has been properly referenced. The standard SkinnableContainerSkin that was copied for us comes loaded with a spark Group with a basic layout. We have replaced this with a HGroup and two labels. Notice all of our styling has been implemented in the skin, but our values are nowhere to be seen? The SkinParts we defined in MirrorGroup are seen here (matching IDs are a must) as "plainText" and "mirrorText".
The final step is to create an application and use the custom components and skins.
1:
2: <?xml version="1.0" encoding="utf-8"?>
3: <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
4: xmlns:s="library://ns.adobe.com/flex/spark"
5: xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600"
6: xmlns:view="com.nictunney.skindemo.view.*">
7:
8: <s:VGroup top="10" left="10">
9: <view:ImageButton label="Toggle Mirror Visibility" click="mirrorGroup.visible = !mirrorGroup.visible" />
10: <view:MirrorGroup id="mirrorGroup" content="This is my text to mirror" />
11: </s:VGroup>
12:
13: </s:Application>
14:
That's it! We are telling the button what to do when it is clicked (toggle visibility of the MirrorGroup). The text is also being passed into the MirrorGroup as its content property. Flex handles the rest!
Full code here