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 SkinParts, 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 and partRemoved functions: get called whenever the view adds a SkinPart 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 and removeBehaviors 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?
  • the call to invalidateSkinStates in the onAnswer getter function: the component dictates when states should change, so call invalidateSkinStates() 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:

DemoQuestionAnswer Screenshot Checkbox not selected

And after you click the CheckBox:

 

DemoQuestionAnswer Screenshot with Checkbox selected