Animating overlay… REVISITED

About one year ago I posted this entry about Animating overlay made easy with Flex 4 effects and skins. While the example does not provide the ultimate practical value, it is a decent didactic material for novices. Also not so long ago I also posted about dynamic skinparts, which reminded me about this old post and the fact that if I were to use it is not flexible enough. Every time I would like to add or remove item from the animated collection, it would require me going inside the project and add the new element inside the Overlay class and a new corresponding graphical representation inside the skin. Yack! There’s gotta be a better way… Enter dynamic skin parts again. They provide a flexible way to manipulate skin parts at runtime.

Here is the new output and the explanations follows after:

Get Adobe Flash player

So, how does this affects the old example?
1. All former filled elements e.g.:

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

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

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

will become:

[SkinPart(required="true", type="spark.primitives.supportClasses.GraphicElement")]
public var graphicElement:IFactory;

(As you can see, I moved the type up one class on the inheritance hierarchy.)

2. Additionally, we need to inform the skin on how to parent these dynamic parts, so the parenting group from the skin will be declared as skin part on the component:

[SkinPart(required="true")]
public var graphicElementsGroup:Group;

3. Now skins parts are dynamic, so we need to keep track of how many they are. To spice things up a little bit, we declare a variable numGraphicElements and give it public access so it can be modified at runtime.

/**
* @private
* storage for numGraphicElements property
*/
private var _numGraphicElements:int = 3;

/**
* @private
* storage for numGraphicElementsChange property
*/
private var _numGraphicElementsChanged:Boolean;

/**
* The current number of animating elements
*/
[Bindable(event="numGraphicElementsChanged")]
public function get numGraphicElements():int
{
	return _numGraphicElements;
}

/**
* @private
* @param value
*
*/
public function set numGraphicElements(value:int):void
{
	if (_numGraphicElements != value)
	{
		_numGraphicElements = value;
		_numGraphicElementsChanged = true;
		invalidateProperties();

		if (hasEventListener("numGraphicElementsChanged"))
		{
			dispatchEvent(new Event("numGraphicElementsChanged"));
		}
	}
}

4. Creating and removing dynamic parts. The number of parts will vary according to numGraphicElements. In the example the default value for numGraphicElements is 3. However it can be modified at any time. Therefore the code that actually triggers the creation of skinparts is placed inside commitProperties() instead of createChildren(). Inside the latter we only set a “dirty” marker flag, to signal the need for creating children upon component instantiation. Alternatively the numGraphicElementsChanged can be initialized as dirty upon declaration, eliminating the need to override the createChildren() method.

/**
* @private
* just mark the flag as dirty here
* actual creation happens inside commitProperties
*/
override protected function createChildren():void
{
	super.createChildren();
	_numGraphicElementsChanged = true;
}

The same commitProperties() holds the de code for destroying skinparts. Codes figures out if it needs to create or destroy a part by caching the old numGraphicElements and comparing it against its new value.

override protected function commitProperties():void
{
	super.commitProperties();
	if (_numGraphicElementsChanged)
	{
		_numGraphicElementsChanged = false;
		if (_numGraphicElements > _oldValue)
		{
		createAnimatedElements(_numGraphicElements - _oldValue);
		}
		else
		{
		destroyAnimatedElements(_oldValue - _numGraphicElements);
		}

		_oldValue = _numGraphicElements;
	}
}

5. Adding removing parts to the skin. The former static skin parts:

<!-- 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>

become:

<fx:Declarations>
	<fx:Component id="graphicElement">
		<s:Rect width="15" height="15" radiusX="3" radiusY="3">
			<fx:Script>
				<![CDATA[
					/**
					* @private
					* Storage for default color
					*/
					private var _defaultColor:uint = 0xCC0000;
					
					//-------------------------------------------------------
					// updateDisplayList()
					//-------------------------------------------------------
					/**
					* @private
					* adjust the element color if the case
					*/
					override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
					{
					var filledElementColor:uint = outerDocument.getStyle("graphicElementColor");
					if (filledElementColor != _defaultColor)
					{
					bgColor.color = filledElementColor;
					}
					
					super.updateDisplayList(unscaledWidth, unscaledHeight);
					}
				]]>
			</fx:Script>
			<s:fill>
				<s:SolidColor id="bgColor" color="#CC0000"/>
			</s:fill>
		</s:Rect>
	</fx:Component>
<fx:Declarations>

6. So far so good…  Now we the effects targeting the elements needs to be created by hand in actionscript.  Since the bulk of code will pretty much be similar for both skins, I moved most it inside a base class (OverlaySkinBase) which will be extended by the 2 skins.  Additionally, I created an additional factory class (EffectsFactory) which is responsible for creating the effect instances.

OverlaySkinBase declares and instantiate the animator, the effects factory, starting/stopping the animation and provides hook methods for attaching/removing effects to the skin part. To achieve the latter the easiest way is to register for ElementExistenceEvent.ELEMENT_ADD / ElementExistenceEvent.ELEMENT_EVENT on the parenting group. Inside the handler effects targeting the element can be created or destroyed. For brevity only the code that adds/removes an effect is displayed below.

//------------------------------------------
// element_addHandler()
//------------------------------------------
/**
*
* @private
* handles the addition of a new skinpart/graphic element by creating an
* effect to target the new element
*
* @param event containing the new created element as payload
*/
protected function element_addHandler(event:ElementExistenceEvent):void
{
	var playing:Boolean = animator.isPlaying;
	if (playing)
	{
		animator.stop();
	}

	var effect:Effect = createEffectForElement(event.element as FilledElement);
	animator.addChild(effect);

	if (playing || (playRequested))
	{
		playRequested = false;
		animator.play();
	}
}

//------------------------------------------
// element_removeHandler()
//------------------------------------------
/**
*
* @private
* handles the removal of an existing skinpart/graphic element by removing the
* effect which targets this element
*
* @param event containing the element which was removed as payload
*/
protected function element_removeHandler(event:ElementExistenceEvent):void
{
	var playing:Boolean = animator.isPlaying;
	if (playing)
	{
	animator.stop();
	}

	animator.children.pop();

	if (playing)
	{
		if (!animator.children.length)
		{
			playRequested = true;
			return;
		}
		animator.play();
	}
}

//------------------------------------------
// createEffectForElement()
//------------------------------------------
/**
* Stub method used by subclasses to create a custom
* effect for the newly added skinpart
*
* @param element
* @return Effect
*
*/
protected function createEffectForElement(element:FilledElement):Effect
{
	throw new IllegalOperationError("Abstract method, override in subclass");
	return null;
}

7. Now that the mechanism is hooked up in the base class, the 2 skins only need to override their specific parts. That means overriding createEffectForElement() and/or profiving additional factory method function for creating glow filters (if the case).

//OverlayRectangleSkin.mxml
//-------------------------------------------------------
// createEffectForElement()
//-------------------------------------------------------
/**
* @override - add specific efect to target the element
*/
override protected function createEffectForElement(element:FilledElement):Effect
{
	var filter:AnimateFilter = factory.createAnimateFilter(element, 2, 325, "reverse", createGlowFilter);
	var simpleMotionPath:SimpleMotionPath = factory.createSimpleMotionPath("alpha", 0.7, 1);
	filter.motionPaths = new [simpleMotionPath];

	var animateColor:AnimateColor = factory.createAnimateColor(element.fill, 1, 650, "reverse", 0xCC0000, 0x990000);

	var parallel:Parallel = new Parallel();
	parallel.addChild(filter);
	parallel.addChild(animateColor);

	return parallel;
}

//-------------------------------------------------------
// createGlowFilter()
//-------------------------------------------------------
/**
* @private
* glow filter factory method
*/
private function createGlowFilter():GlowFilter
{
	return new GlowFilter(0x990000, 5, 5, 4);
}

//OverlayCircleSkin.mxml
//-------------------------------------------------------
// createEffectForElement()
//-------------------------------------------------------
/**
* @override - add specific effect to target the element
*/
override protected function createEffectForElement(element:FilledElement):Effect
{
	return factory.createAnimateColor(element.fill, 1, 600, "reverse", 0x00bc00, 0x003a00);
}

This is it, you can also DOWNLOAD the source/.fxp project.

Leave a Reply

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

*