Wednesday, May 2, 2012

Some Fancy Skinning

Using Flex 4 skins, a single GUI component does not need to be extended to change appearance. If the developer creates a clever underlying object extending either the SkinnableContainer or SkinnableComponent Flex components, a highly extensible and easy to use visual component can be created. Demonstrated in this post is something I like to call a "Fancy Component", which demonstrates how a single underlying component can be reused with different skins.

The first code snippet is the definition of the FancyComponent object, which is an extension of the Flex SkinnableContainer. There are three SkinParts defined, all of which are required to render this component. The skin parts are s:Image, s:TextArea, and s:Button. Pay attention to the names of these variables, as they will be important later on.

The FancyComponent overrides a function of the parent called partAdded. This is the principal function responsible for adding skin parts to this container. After calling the parent implementation, notice that I use if-else logic to see if the incoming skin part is one of the parts I have defined. Also notice that if it is a part that I have defined, that I am doing something to it: for the image I am setting its source, for the textArea I am setting its parameter textFlow, and for the closeButton I am setting label and adding a MouseEvent listener to it. The class has public variables for setting those values set in the partAdded function (imgSrc, msgText, and closeButtonLabel respectively).

The Fancy Component
package com.charles.components
{
import flash.events.Event;
import flash.events.MouseEvent;

import flashx.textLayout.elements.TextFlow;

import mx.effects.IEffectInstance;
import mx.events.FlexEvent;

import spark.components.Button;
import spark.components.Image;
import spark.components.SkinnableContainer;
import spark.components.TextArea;
import spark.effects.Fade;

[Event(name="closeMe", type="flash.events.Event")]
public class FancyComponent extends SkinnableContainer
{
[SkinPart(required="true")]
public var image:Image;

[SkinPart(required="true")]
public var textArea:TextArea;

[SkinPart(required="true")]
public var closeButton:Button;

public var imgSrc:Object;
public var msgText:TextFlow;
public var closeButtonText:String;

protected var fadeEffect:Fade;

public function FancyComponent()
{
addEventListener(FlexEvent.CREATION_COMPLETE, addFadeEffect);
super();
}

override public function effectFinished(effectInst:IEffectInstance):void{
if(effectInst.effect == fadeEffect){
closeButton.removeEventListener(MouseEvent.CLICK, removeView);
dispatchEvent(new Event("closeMe"));
}

super.effectFinished(effectInst);
}

override protected function partAdded(partName:String, instance:Object):void{
super.partAdded(partName, instance);

if(instance == textArea){
textArea.textFlow = msgText;
} else if(instance == image){
image.source = imgSrc;
} else if(instance == closeButton){
closeButton.label = closeButtonText;
closeButton.addEventListener(MouseEvent.CLICK, removeView);
}
}

protected function addFadeEffect(event:FlexEvent):void{
removeEventListener(FlexEvent.CREATION_COMPLETE, addFadeEffect);

fadeEffect = new Fade();
fadeEffect.alphaFrom = 1;
fadeEffect.alphaTo = 0;
fadeEffect.duration = 1000;
}

protected function removeView(event:MouseEvent):void{
removeFancyComponent();
}

protected function removeFancyComponent():void{
fadeEffect.stop();
fadeEffect.play([this]);
}
}
}


Skinning isn't the only thing occurring the FancyComponent. In the constructor, I added a FlexEvent to this object to listen for the creationComplete event. When the event fires, this object will remove the event and set up a fade effect on the object. When the closeButton is clicked, the whole container will begin to fade away. Overriden is the effectFinished function, with special handling to remove the closeButton's event listener and to dispatch an event called closeMe. The event is defined as metadata above the class declaration; this allows implementing objects to explicitly assign functions (like a button's click parameter when defining it in mxml).

With the underlying component defined, skins are now created. Below are two different skins. Notice that there are common features to each of the skins:
  • Each component contains a spark group with id contentGroup.
  • Do you remember the skin part variable names? Within the contentGroup, I have image, textArea, and closeButton as ids for their respective components in the FancyComponent object (s:Image, s:TextArea, s:Button). When the skin is rendered, partAdded in FancyComponent will be called and each of these mxml components will have their appropriate attributes set.
ModernSkin

<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx">
<!-- host component -->
<fx:Metadata>
[HostComponent("spark.components.SkinnableContainer")]
</fx:Metadata>

<!-- states -->
<s:states>
<s:State name="disabled" />
<s:State name="normal" />
</s:states>

<s:Rect left="0" right="0" top="0" bottom="0"
radiusX="10" radiusY="10" width="100%" height="100%">
<s:fill>
<s:LinearGradient rotation="90">
<s:GradientEntry alpha="1"
color="#B0B0B0"
color.disabled="#E0E0E0"/>
<s:GradientEntry
color="#E8E8E8"
color.disabled="#E0E0E0"
alpha=".15"
/>
</s:LinearGradient>
</s:fill>
</s:Rect>
<!-- SkinParts
name=contentGroup, type=spark.components.Group, required=false
-->
<s:Group id="contentGroup">
<s:layout>
<s:HorizontalLayout
paddingBottom="12"
paddingLeft="12"
paddingRight="12"
paddingTop="12"
/>
</s:layout>
<s:Image id="image" width="75" height="75"/>
<s:VGroup width="100%" height="100%">
<s:TextArea id="textArea" height="100%" width="100%"/>
<s:HGroup width="100%">
<s:Spacer width="100%"/>
<s:Button id="closeButton"/>
</s:HGroup>
</s:VGroup>
</s:Group>
</s:Skin>

AlternativeModernSkin
<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx">
<!-- host component -->
<fx:Metadata>
[HostComponent("spark.components.SkinnableContainer")]
</fx:Metadata>

<!-- states -->
<s:states>
<s:State name="disabled" />
<s:State name="normal" />
</s:states>

<!-- SkinParts
name=contentGroup, type=spark.components.Group, required=false
-->

<s:Rect left="0" right="0" top="0" bottom="0"
radiusX="10" radiusY="10" width="100%" height="100%">
<s:fill>
<s:SolidColor color="#C80000"/>
</s:fill>
</s:Rect>
<!-- SkinParts
name=contentGroup, type=spark.components.Group, required=false
-->
<s:Group id="contentGroup">
<s:layout>
<s:HorizontalLayout
paddingBottom="12"
paddingLeft="12"
paddingRight="12"
paddingTop="12"
/>
</s:layout>
<s:VGroup height="100%">
<s:Image id="image" width="75" height="75"/>
<s:Spacer height="100%"/>
<s:Button id="closeButton"/>
</s:VGroup>
<s:TextArea id="textArea" height="100%" width="100%"/>
</s:Group>
</s:Skin>

Notice that the skins are different, and that they are simple and free of any implementation details. The implementation details are handled by the FancyComponent. Therefore, the developer can use this same FancyComponent in different ways by laying out differently the image, textArea, and closeButton components.

The last step is to implement the FancyComponent. Below is mxml that does this. Since we have defined required SkinParts in FancyComponent, the MINIMUM requirement for any skin assigned to the FancyComponent would be those components. Also, the FancyComponent cannot be rendered without a skin that implements those SkinParts.

Implementation
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
  xmlns:s="library://ns.adobe.com/flex/spark"
  xmlns:mx="library://ns.adobe.com/flex/mx"
  xmlns:components="com.charles.components.*"
  backgroundColor="#000000">
<fx:Script>
<![CDATA[
import com.charles.assets.Assets;

import mx.controls.Alert;
]]>
</fx:Script>
<fx:Declarations>
<!-- Place non-visual elements (e.g., services, value objects) here -->
</fx:Declarations>
<s:layout>
<s:VerticalLayout/>
</s:layout>
<components:FancyComponent width="300"
  height="300"
  imgSrc="{Assets.exclaim}"
  closeButtonText="Close Me!"
  skinClass="com.charles.skins.ModernSkin">
<components:msgText>
<s:TextFlow>
<s:p>
This is a <s:span fontWeight="bold">
fancy
</s:span> component. What is rather interesting is that thanks to the underlying
class and skin class, you don't really care about anything but putting in the
important information you desire.
</s:p>
</s:TextFlow>
</components:msgText>
</components:FancyComponent>
<components:FancyComponent width="300"
  height="210"
  imgSrc="{Assets.exclaim}"
  closeButtonText="Close Me!"
  skinClass="com.charles.skins.AlternativeModernSkin"
  closeMe="{Alert.show('Notice this!', 'listening for closeMe');}">
<components:msgText>
<s:TextFlow>
<s:p>
This is a <s:span fontWeight="bold">
 fancy
 </s:span> component. What is rather interesting is that thanks to the underlying
class and skin class, you don't really care about anything but putting in the
important information you desire.
</s:p>
</s:TextFlow>
</components:msgText>
</components:FancyComponent>
</s:WindowedApplication>

This is only a small taste of what clever component development and skinning can do. If you find this appealing, you might want to experiment with SkinParts that aren't required ([SkinPart(required="false")]). This means that a single component can be multi-purposed both visually and functionally (for example, you can have a three buttoned component that optionally implements two of the buttons, therefore fade effects and other features such as text areas need be applied once, and the partAdded method will handle the cases where the non-required skin parts are present).

No comments:

Post a Comment