Animating overlay made easy with Flex 4 effects and skins

There are several times when you need to show something to the user (better than a blank screen) while it waits for something else to be processed or loaded. Flex has its own preloader (SparkDownloadProgressBar) and also offers a ProgressBar control. However there are cases when you want to customize your own or have more flexibility. For experienced flash animators, this represents a trivial assignment. Even though I have been experimenting with Flash ever since its MX days and I could do it in Flash, I am not quite the animator. I just feel more comfortable with Flex… Fortunately with Flex 4 it is straightforward to build something decent without too much effort. It involves a little bit of skinning and the use of effects.

Here is output and some explanations follows after:

Get Adobe Flash player

To start, I will create a custom host component and then two skins to apply to it. The nice thing about Spark components model is that behavior of a component is decoupled from its visual representation. Hence the behavior sits in one class (host, usually .as file) and the visual representation in interchangeable MXML skin classes. The component is associated with the skin skin through it’s skinClass property. Furthermore a skinning contract exists to set the interactivity between the two. The skinning contract has these three parts: Data, Parts, and States. This works in the following way: data: component holds the data – skin defines a visual to display the data; parts: component defines the skin parts – skin implements the skin parts (or ignores them if they are not marked as required by the component); states: components define the supported states – skin reacts to the changes between the states (or at minimum also declares the states).

The Host Component
The overlay (Overlay.as) is pretty basic, has 3 graphic elements which will be animated inside the skin is able to display  text (custom text, loading percent, etc). It extends the spark.components.supportClasses.SkinnableComponent, and declares only data and parts. This component will have no states (no SkinState metatag in the code). Additionally it has two style properties: cornerRadius and filledElementColor (the color of the graphic element: circle or rectangle).

[Style(name="cornerRadius", type="Number", format="Length", inherit="no")]
[Style(name="filledElementColor", type="uint", format="Color", inherit="yes")]
public class Overlay extends SkinnableComponent
{
	private var _info:String;
	
	[SkinPart(required="true")]
	public var filledElementOne:FilledElement;

	[SkinPart(required="true")]
	public var filledElementTwo:FilledElement;
	
	[SkinPart(required="true")]
	public var filledElementThree:FilledElement;

	[SkinPart(required="true")] 
	public var descriptionLabel:TextBase; 
	
	public function Overlay()
	{
		super();
	}

	public function set info(value:String):void 
	{ 
		if (_info != value)
		{
			_info = value; 
			if (descriptionLabel)
			{
				descriptionLabel.text = value;
			}
		}
	}
	
	override protected function partAdded(partName:String, instance:Object) : void 
	{ 
		super.partAdded(partName, instance); 
		
		if (instance == descriptionLabel) 
		{	
			descriptionLabel.text = _info; 
		}
	}
}

The 3 filled elements don’t necessarily need to be declared as skin parts. I put them inside here in case I would one to add some behavior to them. They are typed as FilledElement because FilledElement is the superclass for both Rect and Ellipse primitives which will be used in the 2 skins. In order to hook up the skin to display the component’s data one needs to override partAdded() and partRemoved() in the component. For now the 3 filled elements don’t have any behavior, therefore listeners are not attached in partAdded() nor removed in partRemoved(). However the value of _info property gets pushed into the text property of the descriptionLabel skin part. Also in the info setter the skin part is being kept in sync with the _info property.

Basic usage:

	<overlay:Overlay
		width="200"
		height="100"
		x="10"
		y="10" 
		cornerRadius="5"
		filledElementColor="0xCC0000"
		info="Loading..."
		skinClass="ro.a223.overlay.skin.OverlayRectangleSkin">
	</overlay:Overlay>

Overlay skin with animating rectangles.
The skin defines along with the usual background fill rectangle 3 more rectangles (the rectangles id is the same as the name of the skin parts filled elements), the label for the displayDescription property and some effects. There are 3 parallel effects (one for each rectangle) wrapped inside a Sequence which loops for the whole component’s lifecycle. I use an AnimateFilter effect to animate the properties of GlowFilter filter object applied to the target element. (“The AnimateFilter effect differs from the other effects because it animates properties of a filter applied to a target rather than the properties of the target itself.”). First define the Glow filter and then set it as a bitmapFilter property for the the AnimateFilter, which is varying the alpha property of the the Glow filter over time. This is usefull when you want to add temporary filters to a target. Along with this I also animate the color of the rectangles with an AnimateColor effect. (“AnimateColor – animates a change in the color property over time, interpolating between the given colorFrom and colorTo values on a per-channel basis“). And voila! This is it…

<?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"
	addedToStage="animator.play();"
	removedFromStage="animator.stop();">

	<!-- host component -->
	<fx:Metadata>
		[HostComponent("ro.a223.overlay.comp.Overlay")]
	</fx:Metadata>

	<fx:Script>
		<![CDATA[
			private var _defaultColor:uint = 0xCC0000;

			/**
			 * @override - called by flex through the component lifecycle
			 */
			override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
			{
				var cornerRadius:Number = getStyle("cornerRadius");
				bgRect.radiusX = bgRect.radiusY = cornerRadius;

				var filledElementColor:uint = getStyle("filledElementColor");
				if (filledElementColor != _defaultColor)
				{
					colorOne.color = filledElementColor;
					colorTwo.color = filledElementColor;
					colorThree.color = filledElementColor;
				}

				super.updateDisplayList(unscaledWidth, unscaledHeight);
			}

		]]>
	</fx:Script>

	<fx:Declarations>
		<s:GlowFilter id="glow" blurX="5" blurY="5" strength="4" color="#990000"/>

		<s:Sequence id="animator" repeatCount="0">
			<!-- animate the first square -->
			<s:Parallel>
				<s:AnimateFilter 
					target="{filledElementOne}"
					repeatCount="2"
					duration="325"
					repeatBehavior="reverse"
					bitmapFilter="{glow}">
					<s:SimpleMotionPath 
						property="alpha"
						valueFrom=".7"
						valueTo="1">
					</s:SimpleMotionPath>
				</s:AnimateFilter>

				<s:AnimateColor 
					target="{colorOne}"
					colorFrom="#CC0000"
					colorTo="#990000"
					duration="650"
					repeatCount="1"
					repeatBehavior="reverse">
				</s:AnimateColor>
			</s:Parallel>
			
			<!-- animate the second square -->
			<s:Parallel>
				<s:AnimateFilter 
					target="{filledElementTwo}"
					repeatCount="2"
					duration="325"
					repeatBehavior="reverse"
					bitmapFilter="{glow}">
					<s:SimpleMotionPath 
						property="alpha"
						valueFrom=".7"
						valueTo="1">
					</s:SimpleMotionPath>
				</s:AnimateFilter>
				
				<s:AnimateColor 
					target="{colorTwo}"
					colorFrom="#CC0000"
					colorTo="#990000"
					duration="650"
					repeatCount="1"
					repeatBehavior="reverse">
				</s:AnimateColor>
			</s:Parallel>
			
			<!-- animate the third square -->
			<s:Parallel>
				<s:AnimateFilter 
					target="{filledElementThree}"
					repeatCount="2"
					duration="325"
					repeatBehavior="reverse"
					bitmapFilter="{glow}">
					<s:SimpleMotionPath 
						property="alpha"
						valueFrom=".7"
						valueTo="1">
					</s:SimpleMotionPath>
				</s:AnimateFilter>
				
				<s:AnimateColor 
					target="{colorThree}"
					colorFrom="#CC0000"
					colorTo="#990000"
					duration="650"
					repeatCount="1"
					repeatBehavior="reverse">
				</s:AnimateColor>
			</s:Parallel>
		</s:Sequence>
	</fx:Declarations>

	<!-- layer 0: shadow -->
	<!--- @private -->
	<s:Rect left="5" right="5" top="0" bottom="5" radiusX="4" radiusY="4">
		<s:fill>
			<s:LinearGradient rotation="90">
				<s:GradientEntry color="#001111"/>
				<s:GradientEntry color="#000000"/>
			</s:LinearGradient>
		</s:fill>
	</s:Rect>

	<!-- layer 1: background -->
	<s:Rect 
		id="bgRect"
		left="0"
		right="0"
		top="0"
		bottom="0"
		radiusX="5"
		radiusY="5">
		<s:stroke>
			<s:SolidColorStroke 
				color="#cccccc"/>
		</s:stroke>
		<s:fill>
			<s:LinearGradient rotation="90">
				<s:GradientEntry color="0x111111"/>
				<s:GradientEntry color="0x333333"/>
			</s:LinearGradient>
		</s:fill>
	</s:Rect>

	<!-- layer 2: animated shapes -->
	<s:HGroup 
		id="grp"
		horizontalCenter="0"
		verticalCenter="-10">
		<s:Rect 
			id="filledElementOne"
			width="15"
			height="15"
			radiusX="3"
			radiusY="3">
			<s:fill>
				<s:SolidColor id="colorOne" color="#CC0000"/>
			</s:fill>
		</s:Rect>

		<s:Rect 
			id="filledElementTwo"
			width="15"
			height="15"
			radiusX="3"
			radiusY="3">
			<s:fill>
				<s:SolidColor 
					id="colorTwo" color="#CC0000"/>
			</s:fill>
		</s:Rect>

		<s:Rect 
			id="filledElementThree"
			width="15"
			height="15"
			radiusX="3"
			radiusY="3">
			<s:fill>
				<s:SolidColor 
					id="colorThree"
					color="#CC0000"/>
			</s:fill>
		</s:Rect>
	</s:HGroup>

	<!-- info -->
	<s:Label 
		id="descriptionLabel"
		horizontalCenter="0"
		verticalCenter="10"
		color="0xFFFFFF">
	</s:Label>

</s:Skin>

Overlay skin with animating circles.
The other skin follows the same pattern only uses circles instead of rectangles. I gave up on the AnimateFilter an run only the AnimateColor effect. Also changed the position of the circles to reflect how flexible is the Spark component model. Same component, same skin parts different look and feel. The code is pretty self explanatory.

<?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"
	addedToStage="animator.play()"
	removedFromStage="animator.stop();">
	
	<!-- host component -->
	<fx:Metadata>
		[HostComponent("ro.a223.overlay.comp.Overlay")]
	</fx:Metadata>
	
	<fx:Script>
		<![CDATA[
			private var _defaultColor:uint = 0x00CC00;
			
			/**
			 * @override - called by flex through the component lifecycle
			 */
			override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
			{
				var cornerRadius:Number = getStyle("cornerRadius");
				bgRect.radiusX = bgRect.radiusY = cornerRadius; 
				
				var filledElementColor:uint = getStyle("filledElementColor");
				if (filledElementColor != _defaultColor)
				{
					colorOne.color = filledElementColor;
					colorTwo.color = filledElementColor;
					colorThree.color = filledElementColor;
				}
				
				super.updateDisplayList(unscaledWidth, unscaledHeight);
			}
		]]>
	</fx:Script>
	
	<fx:Declarations>
		<s:Sequence id="animator" repeatCount="0"> 
			<s:AnimateColor
				target="{colorOne}"
				colorFrom="#00CC00" 
				colorTo="#006600" 
				duration="600"
				repeatCount="1" 
				repeatBehavior="reverse"> 
			</s:AnimateColor>
			<s:AnimateColor
				target="{colorTwo}"
				colorFrom="#00CC00" 
				colorTo="#006600" 
				duration="600"
				repeatCount="1" 
				repeatBehavior="reverse"> 
			</s:AnimateColor>
			<s:AnimateColor
				target="{colorThree}"
				colorFrom="#00CC00" 
				colorTo="#006600" 
				duration="600"
				repeatCount="1" 
				repeatBehavior="reverse"> 
			</s:AnimateColor>
		</s:Sequence>	
	</fx:Declarations>
	
	<!-- layer 0: background -->
	<s:Rect id="bgRect" left="0" right="0" top="0" bottom="0" radiusX="5" radiusY="5">
		<s:stroke>
			<s:SolidColorStroke color="#cccccc"/>
		</s:stroke>
		<s:fill>
			<s:LinearGradient rotation="90">
				<s:GradientEntry color="0xFFFFFF"/>
				<s:GradientEntry color="0xEEEEEE"/> 
			</s:LinearGradient>
		</s:fill>
	</s:Rect>
	
	<!-- layer 1: animated shapes -->
	<s:Group horizontalCenter="0" verticalCenter="-13">
		<s:Ellipse id="filledElementOne" x="0" y="25" width="20" height="20">
			<s:fill>
				<s:SolidColor id="colorOne" color="green"/>
			</s:fill>
		</s:Ellipse>
		
		<s:Ellipse id="filledElementTwo" x="15" y="5" width="20" height="20">
			<s:fill>
				<s:SolidColor id="colorTwo" color="green"/>
			</s:fill>
		</s:Ellipse>	
		
		<s:Ellipse id="filledElementThree" x="30" y="25" width="20" height="20">
			<s:fill>
				<s:SolidColor id="colorThree" color="green"/>
			</s:fill>
		</s:Ellipse>	
	</s:Group>
	
	<!-- info -->
	<s:Label id="descriptionLabel" 
		horizontalCenter="0" 
		verticalCenter="27">
	</s:Label>
	
</s:Skin>

The exported .fxp project is up for DOWNLOAD.

P.S.
The component will work with the basic usage as long as it has the skin set through the skinClass property:

skinClass="ro.a223.overlay.skin.OverlayRectangleSkin"

Another discussion is about when to start/stop the animation. In case the component will be created inside mxml but kept invisible until some loading/processing occurs is pointless to start the animation on the when the skin is added to stage. Rather than that register for the FlexEvent.SHOW/FlexEvent.HIDE on the hostComponent and start/stop the animation when the host component is made visible/invisible.

...	
hostComponent.addEventListener(FlexEvent.SHOW, visibilityHandler);
hostComponent.addEventListener(FlexEvent.HIDE, visibilityHandler);
...
private function visibilityHandler(event:FlexEvent):void
{
	hostComponent.visible ? animator.play() : animator.stop();
}
//perform handlers cleanup on remove from stage

On the other hand is highly probably that will be used as a pop-up and created via PopUpManager.createPopUp() method. Then the skin needs to be set inside an external CSS file or script block.

@namespace overlay "ro.a223.overlay.comp.*";
	
overlay|Overlay
{
	skinClass: ClassReference("ro.a223.overlay.skin.OverlayRectangleSkin");
}

Alternatively it can be set through actionscript inside a static block intializer as follows:

private static var classConstructed:Boolean = classConstruct();
private static function classConstruct():Boolean 
{
	var styles:CSSStyleDeclaration = FlexGlobals.topLevelApplication.styleManager.getStyleDeclaration("ro.a223.overlay.comp.Overlay");
	if (!styles) 
	{
		var defStyles:CSSStyleDeclaration = new CSSStyleDeclaration();
		defStyles.defaultFactory = function():void 
		{
			this.skinClass = Class(OverlayRectangleSkin);
		}
FlexGlobals.topLevelApplication.styleManager.setStyleDeclaration("ro.a223.overlay.comp.Overlay", defStyles, true);
	} 
	else if (!styles.getStyle('skinClass')) 
	{
		styles.setStyle('skinClass', Class(OverlayRectangleSkin));
	}			
	return true;
}		

There is a more elegant way to set the default skin on a custom component, but about that in another post.

Leave a Reply

Your email address will not be published. Required fields are marked *

*