Robotlegs, Part 2: Adding ImageLoader & TweenMax

Expanding on the previous Robotlegs entry. The original source code is generously provided by hubflanger. I’m modifying her code to show images using two popular asset loading & animation libraries. To do this we have to modify the XML data file and image preloading, processing & display code in these 3 classes: LoaderMaxDataService, SectionVO & SectionContainer.

Updated LoaderMaxDataService:

package com.hubflanger.robotlegsdemo.service
{
	import com.hubflanger.robotlegsdemo.model.vo.SectionVO;
	import com.hubflanger.robotlegsdemo.model.SiteModel;
	import com.hubflanger.robotlegsdemo.events.SystemEvent;
	
	import com.greensock.events.LoaderEvent;
	import com.greensock.loading.ImageLoader;
	import com.greensock.loading.LoaderMax;
	import com.greensock.loading.XMLLoader;
	import com.greensock.loading.display.ContentDisplay;
	
	import flash.events.Event;
	import flash.net.*;
	
	import org.robotlegs.mvcs.Actor;
		
	/**
	 * A Service class that handles loading and parsing of the site's xml data.
	 */	
	public class LoaderMaxDataService extends Actor implements ISiteDataService
	{
		/**
		 * Inject the <code>SiteModel</code> Singleton.
		 */		
		[Inject]
		public var model				:SiteModel; 		
		private var queue				:LoaderMax; 		
		private var xml					:XMLLoader;

		public function LoaderMaxDataService()
		{ 			
			LoaderMax.activate([XMLLoader, ImageLoader]);				
		}
				
		public function loadData():void 
		{			
			var xml:XMLLoader = new XMLLoader("assets/data.xml", 
											  {name:"mainXML", 
											  onComplete:xmlComplete, 
											  estimatedBytes:4000});
			xml.load();						
		}
		
		/**
		 * Handler for the XMLLoader's onComplete.	
		 * <p>Preloads the images</p>
		 */			
		private function xmlComplete(e:LoaderEvent):void
		{
			xml = e.target as XMLLoader; 
			
			queue = new LoaderMax({name:"mainQueue", 
								   onProgress:qProgressHandler,
								   onComplete:qCompleteHandler,
								   onError:qErrorHandler});

			var sections:XMLList = xml.content.sections.section; 			
			for each (var section:XML in sections) 
			{
				queue.append(new ImageLoader(section.img.@src, 
												 {name:"image_" + section.@id,											
												  alpha:0,
												  visible:false,											  											
												  container:this}));					
			}						
			
			queue.load();	
		}

		private function qProgressHandler(e:LoaderEvent):void 
        { 
            trace("progress: " + e.target.progress); 
        }
		private function qErrorHandler(e:LoaderEvent):void
        { 
            trace("qErrorHandler(): error occured with " + e.target + ": " + e.text); 
        } 
		private function qCompleteHandler(e:LoaderEvent):void 
		{
			trace(e.target + "queue complete");							
			imagesCompleteHandler();						
		}			
		 		 
		/**
		 * Process content data after images are preloaded.	
		 * <p> Tell the rest of the application when data is ready.</p>
		 */				 
		private function imagesCompleteHandler():void 
		{			
			model.header = xml.content.header.text(); 
			var sections:XMLList = xml.content.sections.section; 
			
			for each (var section:XML in sections) 
			{
				var sectionVO:SectionVO = new SectionVO(section.@id,
														section.label.toString(),
														LoaderMax.getContent("image_" + section.@id), 
														section.content.toString());				
				
				model.sectionsList.push(sectionVO);
				model.sectionsHash[sectionVO.id] = sectionVO;
			}			
			
			model.defaultSection = model.sectionsList[0].id;
			
			dispatch(new SystemEvent(SystemEvent.INIT_VIEW, false));					
		}				
	}
}

A few minor changes to SectionVO, to accomodate the new image property:

package com.hubflanger.robotlegsdemo.model.vo
{
	import com.greensock.loading.display.ContentDisplay;
	
	public class SectionVO
	{
		private var _id:String;
		private var _label:String;
		private var _image:ContentDisplay;
		private var _content:String;
		
		/**
		 * A custom  object for storing Section node data
		 */
		public function SectionVO( id:String, label:String, img:ContentDisplay, content:String )
		{
			_id = id;
			_label = label;
			_image = img;
			_content = content;
		}

		public function get id():String
		{
			return _id;
		}
		
		public function get label():String
		{
			return _label;
		}

		public function get image():ContentDisplay
		{
			return _image;
		}
		
		public function get content():String
		{
			return _content;
		}
	}
}

And finally, the addition of a Sprite image container & image display code with TweenMax:

package com.hubflanger.robotlegsdemo.view.components
{
	import com.greensock.TweenMax;

	import com.hubflanger.robotlegsdemo.model.vo.SectionVO;
	
	import flash.display.*;
	import flash.text.*;
	import flash.utils.Dictionary;
	
	/**
	 * The display container for the section content.
	 */	
	public class SectionContainer extends Sprite
	{
		private var sectionsHash:Dictionary = new Dictionary();
		private var textField:TextField;
		private var img:Sprite;
		
		/**
		 * The constructor. 
		 * <p>
		 * Creates a Shape object as background. Creates a TextField 
		 * for the content copy and a Sprite to hold images.
		 */	
		public function SectionContainer()
		{
			var bg:Shape = new Shape();
			bg.graphics.beginFill(0xF9E5C2);
			bg.graphics.drawRect(0, 0, 550, 400);
			bg.graphics.endFill();
			addChild(bg); 
				
			var tf:TextFormat = new TextFormat();
			tf.font = "Helvetica";
			tf.color = 0x4B1E18;
			tf.size = 12;
			
			textField = new TextField();
			textField.x = 222;
			textField.y = 81;
			textField.width = 310;
			textField.height = 290;
			textField.multiline = true;
			textField.wordWrap = true;
			textField.defaultTextFormat = tf;
			addChild(textField);
			
			img = new Sprite();
			img.x = 12;
			img.y = 95;
			addChild(img);			
		}
		
		/**
		 * Assigns a Dictionary object to the <code>sectionsHash</code> property.
		 *  
		 * @param hash A Dictionary instance containing <code>SectionVO</code>
		 * objects with Section IDs as key.
		 */		
		public function init(hash:Dictionary):void
		{
			sectionsHash = hash;
		}
		
		/**
		 * Retrieves the <code>SectionVO</code> object associated with the 
		 * Section ID and and populates the <code>textField</code> with the 
		 * value of its <code>content</code> property. Populates <code>img</code>
		 * with <code>SectionVO</code>'s value of <code>image</code>. 
		 *
		 * @param str The String ID of the Section selected.
		 */
		public function update(id:String):void
		{
			var sectionVO:SectionVO = sectionsHash[id];
			textField.htmlText = sectionVO.content;			

			cleanImgHolder(img);			
			img.addChild(sectionVO.image);
			TweenMax.to(sectionVO.image, 0.66, {autoAlpha:1});											
		}
		
		private function cleanImgHolder(i:Sprite):void 
		{
			if(i.numChildren > 0) { 
				TweenMax.to(i.getChildAt(0), 0, {autoAlpha:0});		
				i.removeChildAt(0);
			}
		}
	}
}

Don’t forget to update the XML file to contain an “img” element with an “src” attribute:

 <!-- ... -->
		<section id="home">
			<label>Home</label>
			<img src="images/1.jpg" />
			<content>
				<p>Lorem ipsum dolor sit amet...</p>
			</content>
		</section>
<!-- ... -->

Robotlegs with LoaderMax’s XMLLoader

This is a quick modification of hubflanger’s useful Robotlegs AS3.0 Site tutorial. I’m expanding her service class to use a content loading library called LoaderMax.

Since she built hers via an interface, there’s no need to refactor the existing class. Instead use her ISiteDataService interface and create a similar class called LoaderMaxDataService, like so:

package com.hubflanger.robotlegsdemo.service
{
	import com.hubflanger.robotlegsdemo.model.vo.SectionVO;
	import com.hubflanger.robotlegsdemo.model.SiteModel;
	import com.hubflanger.robotlegsdemo.events.SystemEvent;
	
	import com.greensock.events.LoaderEvent;
	import com.greensock.loading.LoaderMax;
	import com.greensock.loading.XMLLoader;
	
	import flash.events.Event;
	import flash.net.*;
	
	import org.robotlegs.mvcs.Actor;
		
	/**
	 * A Service class that handles loading and parsing of the site's xml data.
	 */	
	public class LoaderMaxDataService extends Actor implements ISiteDataService
	{
		/**
		 * Inject the <code>SiteModel</code> Singleton.
		 */		
		[Inject]
		public var model				:SiteModel; 
		
		//private var queue				:LoaderMax; 		

		public function LoaderMaxDataService()
		{ 			
			LoaderMax.activate([XMLLoader]);					
		}
				
		public function loadData():void 
		{			
			var xml:XMLLoader = new XMLLoader("assets/data.xml", 
											  {name:"mainXML", 
											  onComplete:loadCompleteHandler, 
											  estimatedBytes:4000});
			xml.load();						
		}
		
		/**
		 * Handler for the XMLLoader's onComplete.
		 * <p>
		 * Parses the xml data and stores the <code>SectionVO</code> objects in 
		 * the <code>SiteModel</code> Singleton.
		 * <p>
		 * Dispatches a <code>SystemEvent.INIT_VIEW</code> event.
		 * 
		 * @param event
		 */		
		private function loadCompleteHandler(e:LoaderEvent):void 
		{			
			var xml:XMLLoader = e.target as XMLLoader; 
			
			model.header = xml.content.header.text(); 
			var sections:XMLList = xml.content.sections.section; 
			
			for each (var section:XML in sections) 
			{
				var sectionVO:SectionVO = new SectionVO(section.@id,
														section.label.toString(),
														section.content.toString());				
				
				model.sectionsList.push(sectionVO);
				model.sectionsHash[sectionVO.id] = sectionVO;
			}			
			
			model.defaultSection = model.sectionsList[0].id;
			
			dispatch(new SystemEvent(SystemEvent.INIT_VIEW, false));					
		}
	}
}

Update 1 line in the ApplicationContext from this:

 
injector.mapClass(ISiteDataService, SiteDataService);

to this:

 
injector.mapClass(ISiteDataService, LoaderMaxDataService);

There’s no need to update the LoadDataCommand to use LoaderMaxDataService, instead of SiteDataService. hubflanger & Robotlegs’ (ISiteDataService) use of polymorphism allows this to continue working unaltered:

package com.hubflanger.robotlegsdemo.controller
{
	import com.hubflanger.robotlegsdemo.service.ISiteDataService;
	
	import org.robotlegs.mvcs.Command;
	
	/**
	 * Responds to the <code>ContextEvent.STARTUP_COMPLETE</code> framework event.
	 */	
	public class LoadDataCommand extends Command
	{
		/**
		 * Creates an instance of SiteDataService via dependency injection.
		 */		
		[Inject]
		public var siteDataService:ISiteDataService;
		
		/**
		 * Calls the loadData() method of the SiteDataService instance.
		 */		
		override public function execute():void
		{
			siteDataService.loadData();
		}
	}
}

Different interpretations of MVC in different AS3 books

Lately, I’ve been trying to read everything I can find / have time for on MVC in Actionscript and I noticed how different folks allocate different responsibilities to the Controller.

For example, compare the Controller class from Advanced Actionscript 3 with Design Patterns (MVC Clock in Ch. 3) with the Controller class from a book called Flash Video for Professionals (MVC Video Player in Ch. 5).

The former puts the display objects (input text fields) that grab user input into the Controller class called Clock, via this method: private function createField():TextField. The latter places display objects (next, prev, stop buttons) in a View class called MediaPlayerView. MediaPlayerController has a bunch of methods, like play(e:MouseEvent), that call similarly named methods in the Model, which act on the data it encapsulates.

The approach in the video example, where the Controller has only 1 responsibility, feels more solid. This is similar to the MVC chapter in Head First Design Patterns, where the example Swing application contains display elements that capture user input (buttons and menu) in a View class called DJView.

Some examples of the State design pattern

If you’ve ever been sick of writing convoluted if/else statements, this design pattern is a good thing to look into.

One thing it enables you to do in Actionscript — have the same button behave differently, depending on what State the application is in. For example, check out your average Flash video player, like this YouTube video from Crossfit Chesapeake / Wilkes Weightlifting:
http://crossfitchesapeake.com/2009/09/13/wod-mon-91309.aspx

In it’s embedded form, the video has a giant PLAY button with a hit area that covers the width & height of the video. In this example, this button could have three states:

  • State 1:
    If the video is dormant (hasn’t been played since the page loaded), clicking the button plays the video. The play icon graphic is displayed in the dormant State disappears once the button is clicked.
  • State 2:
    If the video is already playing, i.e. the application is in a different State, the same button behaves differently. At this time the play button click handler takes you to the YouTube.com page that originally hosts this video and pauses the embedded video.
  • State 3:
    If you go back to the embedded version of the video which has been paused by the play button’s click handler method (the new window with opened with youtube.com, etc) and click the video (not the navbar at the bottom), the smae “click handler” function from the above functionality resumes video play.

And of course, when the same “click handler” method behaves differently based on the State of the application, it’s an example of polymorphism, one of the 4 main elements of OOP. Here’s a decent definition:

Polymorphism allows two objects to be treated identically, using the same methods, even though the objects implement these methods in quite different ways. It is this concept of “same appearance, different behavior” that gets the 0 word, polymorphism.

There’s another State pattern example in the AS3 port of an open source physics library called Box2DFlashAS3.

The Main.as file, in this case the first file that gets the application going, has a listener that calls a function called update(). Among other things, this function
a) continuously checks for an id variable (altered by the user pressing a keyboard key) and
b) sets a variable of type Test to one of the subclasses of Test, like TestRagdoll (the default example when you run the SWF).

The state machine code is essentially this part of Main.as:

/* ...	*/
switch(m_currId){
	// Ragdoll
	case 0:
		m_currTest = new TestRagdoll();
		break;
	// Compound Shapes
	case 1:
		m_currTest = new TestCompound();
		break;
	// Crank/Gears/Pulley
	case 2:
		m_currTest = new TestCrankGearsPulley();
		break;
	// Bridge
	case 3:
		m_currTest = new TestBridge();
		break;
	// Stack
	case 4:
		m_currTest = new TestStack();
		break;
	// CCD
	case 5:
		m_currTest = new TestCCD();
		break;
	// Theo Jansen
	case 6:
		m_currTest = new TestTheoJansen();
		break;
		
	/* ...	*/
}
/* ...	*/

Testing a cool AS3-based video player

Martin Legris created a great little Actionscript-only AS3 Video player:

today I will explain how to play .FLV files that are hosted on a standard HTTP server, no fancy streaming. You can use pre-made components to do so, but sometimes, for whatever reason, you want to do it yourself. I wrote my first FLV playing algorithm in AS3 about 10 months ago, it has evolved since and here is the breakdown on how I make it work. It’s been used in many widgets, mostly for Music Nation.

His tutorial came in three parts, with final code located here, in Part 3.

Here’s a quick little Client class I wrote as an Actionscript Project in Flex Builder 3. If you’re using the Flash IDE, just use this as the Document Class. If you’re using FlashDevelop — you know what to do. This code can be used to test the code in Part 3:

package {

	import ca.newcommerce.media.FLVPlayer;
	import ca.newcommerce.media.MediaData;

	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.media.Video;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;

	public class FLVPlayerTest extends Sprite
	{
		private var myData:MediaData;
		private var vid:Video;
		private var myPlayr:FLVPlayer;
		private var txtBox:TextField;
		private var txtBox2:TextField;

		//in real life, populate this array from an external data source
		private var trackArray:Array = ["http://www.helpexamples.com/flash/video/clouds.flv",
             "http://www.helpexamples.com/flash/video/typing_short.flv",
             "http://www.helpexamples.com/flash/video/sheep.flv",
             "http://www.helpexamples.com/flash/video/water.flv",										       "http://www.helpexamples.com/flash/video/cuepoints.flv"];

		public function FLVPlayerTest()
		{
			inti();
			addBasicMenu();
		}

		public function inti():void
		{
			//start the player
			myData = new MediaData(trackArray[0], "some title", 300, "image.jpg", 320, 200);
			myPlayr = new FLVPlayer();
			vid = new Video(320, 200);

			myPlayr.video = vid;
			myPlayr.playMedia(myData);

			stage.addChild(vid);
		}

		public function addBasicMenu():void
		{
			//add some simple text boxes to test loading other videos
			txtBox2 = new TextField();
         	txtBox2.y = vid.height + 20;
         	txtBox2.autoSize = TextFieldAutoSize.LEFT;
         	txtBox = new TextField();
         	txtBox.autoSize = TextFieldAutoSize.LEFT;
         	txtBox.multiline = true;
         	txtBox.x = vid.x;
         	txtBox.y = txtBox2.y + 20;
         	txtBox.mouseEnabled = true;
         	stage.addChild(txtBox);
         	stage.addChild(txtBox2);

         	txtBox2.text = "Click a line below to load another movie:\n\n";

         	txtBox.appendText("line one\n");
         	txtBox.appendText("line two\n");
         	txtBox.appendText("line three\n");
         	txtBox.appendText("line four\n");
         	txtBox.appendText("line five\n");

			txtBox.addEventListener(MouseEvent.CLICK, handleClick, false, 0, true);
		}

		public function handleClick(e:MouseEvent):void
		{
			//tells us what line of the text box was clicked by the mouse
			var lineIndx:int = txtBox.getLineIndexAtPoint(e.target.mouseX, e.target.mouseY);

			trace(lineIndx);
			switch(lineIndx)
			{
				case 0:
					changeVid(trackArray[lineIndx]);
					break;
				case 1:
					changeVid(trackArray[lineIndx]);
					break;
				case 2:
					changeVid(trackArray[lineIndx]);
					break;
				case 3:
					changeVid(trackArray[lineIndx]);
					break;
				case 4:
					changeVid(trackArray[lineIndx]);
					break;
			}
		}

		public function changeVid(v:String, t:String="", l:int=120, img:String="", w:int=320, h:int=200):void
		{
			//calls the get uri() function of the MediaData class,
			//since myData is an instance of (or object of type) MediaData
			myData.uri = v;
			myData.title = t; // get title();
			myData.duration = l; // get duration();
			myData.width = w;
			myData.height = h;
			myPlayr.play(false);

		}
	}
}

I guess FLVPlayerTest(), as it’s written above, can be considered a View and a Controller in an MVC pattern. It can be a Controller, because of the handleClick() & changeVid() functions that tell the model to update itself. addBasicMenu() could be part of the Controller, since it ads a visual element whose sole purpose is to receive user input (mouse click events) and then tell the Model to update itself. Then again… it can also be part of the View, since it’s a visual component that the user sees and code associated with it can be considered to be display logic. In the MVC chapter of Advanced ActionScript 3 with Design Patterns by Joey Lott and Danny Patterson, put similar visual elements into their Controller. In their Clock example, the Controller class includes code for the button that toggles between Views and text fields that tell the Model to update itself based on user input.

If I add buttons to control play, pause, volume, those would be part of the Controller as well, if we stick with the example from Joey Lott and Danny Patterson.

Although the FLVPlayer class feels like a Controller, it’s actually the main part of the Model. If this was a middleware application, it would be the DB class, or the class that has all bulk of the app’s business logic. It would contain the CRUD methods that interact with the Database (in this case the video data source).

MediaData is part of the Model too. It encapsulates the properties of a video, like uri, image, title, duration, etc. The trackArray is part of how the View stores info from the Model, or at least, it would be, if it was coming from an external source. In real life it would be an Array of MediaData objects. I’m thinking of how in Budi Kurniawan‘s simple servlet-based MVC application a Database class (Model) grabs info from the database (external source) and instantiates objects (part of Model) that store the DB data for use by the application. In the same servlet app, the View then runs a foreach loop and populates a collection object like a List, HashMap or Vector with all the objects instantiated by the DB class.

What’re some helpful metaphors for the Composite Pattern?

The authors of Actionscript 3.0 Design Patterns offer this one: a branch with leaves, where a Branch is a Composite and a Leaf is a Component. Makes sense — a branch can contain multiple leaves, while a leaf is a self-contained object with no nested elements or no additional items inside it/contained in it. For simplicity’s sake & since no analogy is perfect, let’s leave biology & the fact that a leaf is made up of layers of cells aside.

As good as the branch/leaves metaphor is, different people learn differently, so having additional examples, with alternate ways to describe the same abstract concept can’t hurt.

Perhaps, some of these can be useful too:

  • Ice tray & ice cubes
  • Ammo belt & bullets
  • Moving truck (Composite), boxes (Composite), chair (Component), & box contents (Component or Composite)
  • Organizational chart: Department (Composite), Employee (Component)
  • Suitcase & clothes
  • Directory & files
  • Composition (the “Has-A” relationship) in Object Oriented Programming

MVC in AS3

Just got back to reading Actionscript 3.0 Design Patterns by William Sanders & Chandima Cumaranatunge.


It’s a super helpful book overall. It’s got some good information but feels hard to read. Perhaps, I’m judging by too harsh a standard, since this is a technical manual style book, after all, not a novel. I just wish every IT book was written with as much readability as Java 5: A Beginner’s Tutorial by Budi Kurniawan.

There’re a few typos in the MVC chapter. There’re a few places in Ch. 12, where a method’s signature gets cut off.

For example, on p. 440 the constructor is missing a param (or two) and the closing ‘)’ bracket, like so:

//some code here
public function ConstructorName (aParam1:Type,
{
 //some code here
}
//some code here

And on p. 435 an addEventListener() method inside the constructor is missing the event handler function name and the closing ‘)’ bracket.

There’s another type-o in Chapter 6: Composite Pattern on p. 239:

/* 
 *the "." inside the Composite method should be a "," 
 *since it's meant to separate two input  parameters 
 */
airPlane = new Composite(0.0);  //line 16 
//some code here