Creating Your First Custom SkinnableComponent in Flex 4
It took me a couple of days to get to my next Flex 4 example, but here we finally are. I wanted to try making a component which had optional SkinPart
s, so I came up with the following example (get the source). For those who don't know, Flex 4 targets Flash Player 10 so you'll need that in order to run the SWF.
In this example we will build a component called QuestionAndAnswer
which will include a text field containing a question, a check box, and a text field containing an answer to the question. The check box and answer are both optional, so if the Skin file doesn't include those SkinParts, they won't be a part of the view. If they are included, then clicking the check box will show the text field containing the answer. Let's see what the code looks like.
QuestionAndAnswer.as
package com.smartlogicsolutions.flex.component {
import flash.events.Event;
import flex.component.CheckBox;
import flex.component.TextView;
import flex.core.Flags32;
import flex.core.SkinnableComponent;
[DefaultProperty("content")]
[SkinStates("question", "answer")]
/**
* Component to demonstrate optional SkinParts.
*
* @langversion ActionScript 3.0
* @author Greg Jastrab <greg@smartlogicsolutions.com
*/
public class QuestionAndAnswer extends SkinnableComponent {
/* --- Variables --- */
[Bindable]
public var content:*;
protected var answerVal:String;
protected var flags:Flags32;
protected var questionVal:String;
protected var toggleText:String;
// Skin invalidation flags
protected static const onQuestionFlag:uint = 1 << 0;
protected static const onAnswerFlag:uint = 1 << 1;
/* === Variables === */
/* --- Skin Parts --- */
[SkinPart]
public var questionText:TextView;
[SkinPart(required="false")]
public var answerText:TextView;
[SkinPart(required="false")]
public var toggleAnswer:CheckBox;
/* --- Constructor --- */
public function QuestionAndAnswer() {
super();
flags = new Flags32();
answerVal = questionVal = "";
toggleLabel = "Toggle Answer"
}
/* === Constructor === */
/* --- Functions --- */
override protected function getUpdatedSkinState():String {
return onAnswer ? "answer" : "question";
}
override protected function partAdded(partName:String, instance:*):void {
super.partAdded(partName, instance);
if(instance == toggleAnswer) {
toggleAnswer.addEventListener(Event.CHANGE, onToggleAnswer);
}
}
override protected function partRemoved(partName:String, instance:*):void {
super.partRemoved(partName, instance);
if(instance == toggleAnswer) {
toggleAnswer.removeEventListener(Event.CHANGE, onToggleAnswer);
}
}
/* === Functions === */
/* --- Event Handlers --- */
private function onToggleAnswer(evt:Event):void {
onAnswer = toggleAnswer.selected;
}
/* === Event Handlers === */
/* --- Public Accessors --- */
[Bindable("answerChanged")]
public function get answer():String { return answerVal; }
public function set answer(value:String):void {
answerVal = value;
dispatchEvent(new Event("answerChanged"));
}
[Bindable("onAnswerChanged")]
public function get onAnswer():Boolean { return flags.isSet(onAnswerFlag); }
public function set onAnswer(value:Boolean):void {
if(!flags.update(onAnswerFlag, value))
return;
dispatchEvent(new Event("onAnswerChanged"));
invalidateSkinState();
}
[Bindable("questionChanged")]
public function get question():String { return questionVal; }
public function set question(value:String):void {
questionVal = value;
dispatchEvent(new Event("questionChanged"));
}
[Bindable("toggleLabelChanged")]
public function get toggleLabel():String { return toggleText; }
public function set toggleLabel(value:String):void {
toggleText = value;
dispatchEvent(new Event("toggleLabelChanged"));
}
/* === Public Accessors === */
}
}
Things to note here:
[SkinPart]
declarations: this metadata tag describes what properties are visually placed within the Skin file- the
partAdded
andpartRemoved
functions: get called whenever the view adds aSkinPart
to the display list- I'm attaching/removing the event listener for when the CheckBox changes in these functions, but it's more appropriate to do so in the
attachBehaviors
andremoveBehaviors
functions. I I think this is alright for my example, but this was easier to code since the CheckBox is only optionally added - perhaps someone from the SDK team can give better insight on how it should be done to optional skin parts?
- I'm attaching/removing the event listener for when the CheckBox changes in these functions, but it's more appropriate to do so in the
- the call to
invalidateSkinStates
in theonAnswer
getter function: the component dictates when states should change, so callinvalidateSkinStates()
to indicate the state has changed - the
getUpdatedSkinState
function: using properties within the component, determines what the current state should be
On to the Skin files. com.smartlogicsolutions.flex.skin.QASkin.mxml
will place all of the SkinParts, while com.smartlogicsolutions.flex.skin.QSkin.mxml
will only place the questionText
.
QASkin.mxml
<?xml version="1.0" encoding="utf-8"?>
<Skin xmlns="http://ns.adobe.com/mxml/2009" layout="flex.layout.VerticalLayout">
<Style>
.text {
fontFamily: "Arial";
fontSize: 11pt;
color: #000000;
}
</Style>
<states>
<State name="question" />
<State name="answer" />
</states>
<content>
<TextView id="questionText" text="{data.question}" styleName="text" color.answer="#0000ff" />
<CheckBox id="toggleAnswer" label="{data.toggleLabel}" />
<TextView id="answerText" includeIn="answer" styleName="text" text="{data.answer}" />
</content>
</Skin>
The includeIn
property replaces the old AddChild
syntax in states, so includeIn="answer"
says to only include the answerText
in the answer
state.
QSkin.mxml
<?xml version="1.0" encoding="utf-8"?>
<Skin xmlns="http://ns.adobe.com/mxml/2009" layout="flex.layout.VerticalLayout">
<Style>
.text {
fontFamily: "Arial";
fontSize: 11pt;
color: #000000;
}
</Style>
<states>
<State name="question" />
<State name="answer" />
</states>
<content>
<TextView id="questionText" text="{data.question}" styleName="text" color.answer="#0000ff" />
</content>
</Skin>
And finally, the application file:
DemoQuestionAnswer.mxml
<?xml version="1.0" encoding="utf-8"?>
<Application xmlns="http://ns.adobe.com/mxml/2009"
xmlns:sls="com.smartlogicsolutions.flex.component.*"
layout="flex.layout.HorizontalLayout">
<Style>
QuestionAndAnswer { skinZZ: ClassReference("com.smartlogicsolutions.flex.skin.QASkin"); }
.question { skinZZ: ClassReference("com.smartlogicsolutions.flex.skin.QSkin"); }
</Style>
<sls:QuestionAndAnswer id="qa"
question="What is the coolest new language?"
answer="Flex 4, duh!" />
<sls:QuestionAndAnswer id="q" styleName="question" question="Only A Question" />
</Application>
This places 2 QuestionAndAnswer
components horizontally in the Application, giving one the QASkin
and the other the QSkin
skins. Compile and run and you should see:
And after you click the CheckBox: