Menu and Banner from XML with AS3 and Flash CS3

I’ve just finished remaking the menu/banner of Sea Bees, one of the bigger diving shops here in Phuket.

Update: If you look carefully you will see that the menu changes size when a menu item that lacks a background image is loaded, this is done dynamically and has its own explanation.

The old menu was a disaster from a maintenance standpoint. I’m not going to go into details as I treasure a low blood pressure, suffice to say it wasn’t really a professional job. Anyway, this tutorial will detail a lot of the aspects anyone looking to migrate from AS2 to AS3 might wonder about.


For once I have to exclaim, good job Adobe! AS3 is in my book a massive step up from AS2, making for shorter code despite the move towards a more Java-like syntax. Many strange constructs and convoluted approaches have been removed which is a relief, the end result is something much more rational and solid.

The goal of the new menu is to put as much power in the hands of my customer as possible, letting them change virtually anything relating to the menu themselves. After going through the awesome senocular AS3 in Flash CS3 tutorial (which really is a requirement for understanding this one) I realized that XML management is really a treat in ActionScript 3, hence I opted for XML as the configuration medium.

Sure I could’ve used something more JSON-like but my customer already has experience with HTML so going from there to XML is a no-brainer. Plus the fact that the XML handling in AS3 is awesome (didn’t I say that already?), there are no downsides to the XML approach worth mentioning.

Currently the menu is not setup on the customer’s homepage, in the meantime a demo can be found here. And the XML file here.

So through the contents of the XML file the main menu, sub menu, background pictures, various texts, animations and more are controlled. Let’s get on with it to see how this works, starting with the code in the first frame of the .fla file:

import flash.external.*;
import flash.net.*;
var loader:XMLLoader = new XMLLoader();
var menu:MenuHandler;
var xmlRes:XML;
var rootUrl = "http://localhost/sea-bees/eng/";
var curPage:String = ExternalInterface.call("getCurPage").split('/').pop();

function setCurrent(url){
	menu = new MenuHandler(xmlRes, this, rootUrl);
	addChild(menu);
	menu.buildMain();
	menu.setCurrent(url);
}

function resetMenu(){
	removeChild(menu);
	menu = null;
}

function menuClick(event:MouseEvent){
	navigateToURL(new URLRequest(rootUrl+event.currentTarget.name));
	/*
	var curUrl = event.currentTarget.name;
	resetMenu();
	setCurrent(curUrl);
	*/
}

loader.getXML(rootUrl+"menu_structure.xml", function(res){
	xmlRes = res;
	setCurrent(curPage);
});

XMLLoader is a small convenience class that we will go through at the end of this tutorial, the only thing you really need to know is that it’s a simple wrapper for loading XML files in a convenient and jQuery-like way.

MenuHandler is an extension of the MovieClip class, the majority of this tutorial will be concerned with that class. It’s responsible for managing basically everything we do.

We will constantly use absolute URLs, hence we need a rootUrl variable to avoid repeating ourselves.

Since the customer’s site is built on static HTML with a lot of duplication our shockwave will be loaded anew on each page, therefore we need to keep track of which page we are currently viewing. Since I didn’t care to look hard enough for a way of retrieving the current page in AS3 itself I opted for a solution where a Javascript function getCurPage simply returns window.location.href to the shockwave. We split and pop to get the current file name, for instance “index.htm”.

SetCurrent is the entry point for our whole logic that builds the menu. It will create the MenuHandler with the help of the parsed XML, this (which is the root movie clip) and our rootUrl.

BuildMain of the MenuHandler will build the topmost main menu and the setCurrent function of MenuHandler will figure out which sub-menu to build and which menu items are to be highlighted based on the page we are on, more on that later.

ResetMenu() will not be used in the final version, it’s just something I need to make the demo work, so is the stuff that is commented out in the menuClick function (which of course is called whenever a menu item is clicked).

Since the shockwave is repeatedly reloaded on the customer’s site we execute the getXML function of our custom loader every time and use setCurrent() to affect the menu based on which page we are on.

Let’s move on to the MenuHandler class:

package{
	import flash.utils.*;
	import flash.display.*;
	import flash.text.*;
  import flash.events.*;
	import flash.net.*;
	
	public class MenuHandler extends MovieClip{
		
		var menuXml:XML;
		var yPos;
		var fadeIns;
		var p:MovieClip;
		var rootUrl:String;
		var curFadeIn:Number;
		
		public function MenuHandler(menuXml:XML, p:MovieClip, rootUrl:String){
			this.menuXml 	= menuXml;
			this.yPos 		= 5;
			this.fadeIns 	= new Dictionary();
			this.p 			  = p;
			this.rootUrl	= rootUrl;
		}

Let’s go through the class variables:
menuXml is the XML we load, that controls the whole thing.
yPos is the y position of the main menu.
fadeIns is a dictionary which will keep track of how things are to fade in and are fading in, more on that one later.
p is the parent, in this case the root movie clip.
rootUrl has already been explained above.
curFadeIn will is needed inside an on complete callback function, also more on that one later.

public function buildMain():void{
  var xPos 	  = 15;
  var count 	= 0;
  for each (var item:XML in this.menuXml.item){
    var mainBtn:MainItem = this.buildCommon(xPos, this.yPos, new MainItem(), item);
    xPos += mainBtn.label.width + 13;
    if(count < this.menuXml.item.length() - 1)
      this.addMainBar(xPos - 7);
    
    mainBtn.addEventListener(MouseEvent.MOUSE_OVER, this.mainRollOver);
    mainBtn.addEventListener(MouseEvent.MOUSE_OUT, this.mainRollOut);
    count++;
  }
}

This one will build the topmost parent menu, the xPos variable controls where we start on the left, in this case 15 pixels from the edge of the shockwave, count will keep track of how many menu items we have during build.

Next is something new to AS3; for each, which is used to loop through the XML file, each main menu item will be marked with <item></item>, hence the use of this.menuXml.item.

BuildCommon() contains common functionality for building the main menu and the sub-menus, more on that one later. Here we make use of a linked library MovieClip with the class name of MainItem which contains a text called label and nothing else.

The buttons will have a gap of 13 pixels between them, hence the + 13. We will also add separators (addMainBar) between each item.

Finally we connect two event listeners for roll over and roll out which are being used to change the color of the text.

public function buildSub(yPos:Number, parentUrl:String, subUrl:String = ""):Object{
  var subMenu;
  var defUrl = "";
  if(subUrl == "")
    subMenu 	= this.getMain(parentUrl).subMenu;
  else{
    var temp 	= this.getSub(subUrl);
    if(temp.elements('subMenu').length() > 0){
      subMenu = temp.subMenu;
      defUrl 	= temp.url;
    }else{
      subMenu = temp.parent();
      defUrl 	= subMenu.parent().url;
    }
  }
    
  var xPos:Number 	     = subMenu.@x;
  var startX 			= xPos;
  var count 			= 0;
  var subXml 			= subMenu.item;
  for each (var item:XML in subXml){
    var subBtn:SubItem = this.buildCommon(xPos, yPos, new SubItem(), item, defUrl);
    xPos += subBtn.label.width + 13;
    if(count < subXml.length() - 1)
      this.addSubBar(xPos - 7, yPos);
    
    subBtn.addEventListener(MouseEvent.MOUSE_OVER, this.subRollOver);
    subBtn.addEventListener(MouseEvent.MOUSE_OUT, this.subRollOut);
    count++;
  }
  return {width: xPos - startX, startX: startX};
}

This one is similar to building the items in the main menu. Due to the fact that we will sometimes have a third level (the liveaboards) we need some code to manage this, if we have a third level it will be shown in place of the second level.

The rest is almost identical to the buildMain function except for the return value which we will get to later.

public function buildCommon(xPos:Number, yPos:Number, newBtn, item:XML, defUrl:String = ""){
  newBtn.label.htmlText 		  = String(item.label).replace(/\\n/g, "<br>");
  newBtn.x 					          = xPos;
  newBtn.y 					          = yPos;
  this.addChild(newBtn).name 	= item.url.length() > 0 ? item.url : defUrl;
  newBtn.label.width 			    = newBtn.label.textWidth + 7;
  newBtn.bkg.width			      = newBtn.label.width;
  newBtn.bkg.height			      = newBtn.label.textHeight + 5;
  newBtn.buttonMode 			    = true;
  newBtn.mouseChildren 		    = false;
  newBtn.addEventListener(MouseEvent.MOUSE_UP, p.menuClick);
  return newBtn;
}

The arguments usually tell most of the story, we need the positions, the object (newBtn) to work with, the XML item belonging to the item and a default URL that will be used to identify the object on stage by. Some items will not have an URL to identify them with, in that case the URL of the parent will be used (the liveaboards again).

We need to replace instances of \n (for some reasons they are taken literally and I never cared to try and remedy that) with <br> and use htmlText to properly create the newline. The new regular expressions capabilities are useful here.

Note how we add a child (addChild) to the stage and at the same time assign a name to it.

The background (bkg) is just a simple named movie clip in our simple SubItem library class, it is used to mark where the user currently is in the navigation.

Button mode is turned on and mouse children off to disable marking the text fields that are a part of the button and making the pointing hand appear.

Finally we add the click function that is a part of the root clip and return the object.

public function setCurrent(curUrl:String):void{
  var urlMain:String = "";
  var urlSub:String 	= "";
  if(this.isSub(curUrl)){
    urlMain = this.getMainFromSub(curUrl).url;
    urlSub 	= curUrl;
  }else
    urlMain = curUrl;
  
  var mainClip 				= this.getChildByName(urlMain);
  mainClip.bkg.alpha 	= 0.5;
  var subInfo 				= this.buildSub(50, urlMain, urlSub);
  
  if(subInfo.width > 0){
    var animBar 				= this.addChild(new AnimBar());
    animBar.x 					= mainClip.x + (mainClip.width / 2);
    animBar.y 					= mainClip.y + mainClip.height;
    animBar.leftAnim.scaleX 	= (animBar.x - subInfo.startX) / 700;
    animBar.rightAnim.scaleX 	= (subInfo.width - (animBar.x - subInfo.startX)) / 700;
    animBar.leftAnim.x 			= animBar.verticalAnim.x;
    animBar.rightAnim.x 		= animBar.verticalAnim.x;
  }
  
  if(urlSub != ""){
    var subClip = this.getChildByName(urlSub);
    subClip.bkg.alpha = 0.5;
    this.loadBkgPic(urlSub);
    this.loadTexts(urlSub);
  }else{
    this.loadBkgPic(urlMain);
    this.loadTexts(urlMain);
  }
}

The main entry point. We check if we have a sub menu, if we have we get the parent item in the main menu, if not then we are in fact dealing with the main item itself and not a sub menu item.

Since the main menu has been built before we end up here we can retrieve the main menu item in question by name (getChildByName). Next we build the sub-menu in question. If we have a sub menu we will instantiate that cool animated inverted T that expands outwards to give the menu more structure.

Finally we load various selling texts and an eventual background image, more on that below.

public function fadeInHandler(event:Event):void{
  event.currentTarget.alpha += this.fadeIns[event.currentTarget].fadeIn;
  if(event.currentTarget.alpha >= 1)
    event.currentTarget.removeEventListener(Event.ENTER_FRAME, this.fadeInHandler);
}

public function setFadeIn(toFade, fadeVal){
  this.fadeIns[toFade] = {fadeIn: 1 / fadeVal};
  toFade.addEventListener(Event.ENTER_FRAME, this.fadeInHandler);
}

This one is responsible for fading various things that are to be faded in (controlled by the XML). Here we use the dictionary we created in the constructor. The dictionary is something new to AS3, basically a hash/object that can have other objects as keys which is perfect now because that means we can use the object currently to be faded as the key to retrieve the information that will control its fading.

Yes at first I tried to set this as attributes of the object in question but since we work with more than just movie clips (loaders for instance) it wouldn’t work in a consistent fashion, hence the use of a dictionary.

public function loadBkgPic(curUrl:String){
  var curItem = this.getByUrl(curUrl).background;
  if(curItem.length() > 0){
    var bkg 	= new Loader();
    var urlReq 	= new URLRequest(this.rootUrl+"backgrounds/" + curItem);
    bkg.x 		= curItem.@x;
    bkg.y 		= curItem.@y;
    bkg.alpha 	= 0;
    this.curFadeIn = curItem.@fadeIn;
    bkg.load(urlReq);
    this.addChildAt(bkg, 0);
    bkg.contentLoaderInfo.addEventListener(Event.COMPLETE, this.onLoadComplete);
  }
}

public function onLoadComplete(event:Event){
  this.setFadeIn(event.target.loader, this.curFadeIn);
}

So this one loads the backround pictures, not much to say here. Note the ease with which we load and setup the onComplete callback, this was a comparative hell to do in AS2.

public function loadTexts(curUrl:String){
  var curItem 			= this.getByUrl(curUrl);
  var curFormat 			= new TextFormat();
  var curTxt 				= new TextField();
  for each (var item:XML in curItem.text){
    curTxt 				= new TextField();
    curFormat 			= new TextFormat();
    curFormat.size 		= item.@size;
    curFormat.color 	= 0xFFFFFF;
    curFormat.font		= item.@font;
    curTxt.embedFonts 	= true;
    curTxt.antiAliasType = AntiAliasType.ADVANCED;
    curTxt.selectable 	= false;
    curTxt.text 		= item;
    curTxt.x 			= item.@x;
    curTxt.y 			= item.@y;
    curTxt.width		= 700;
    curTxt.setTextFormat(curFormat);
    this.addChild(curTxt);
    if(String(item.@fadeIn).length > 0){
      curTxt.alpha = 0;
      this.setFadeIn(curTxt, item.@fadeIn);
      
      if(String(item.@offsetX).length > 0)
        setOffset(curTxt, item.@offsetX, item.@offsetY, item.@fadeIn);
    }
  }
}

And the texts, the most notable thing here is not the code but my efforts at creating a font in the library by going New Font… in the library menu. After doing that I tried to use my custom name in item.@font but it wouldn’t work.

I had to resort putting a text in question on the stage outside of the rendered area and doing embed on it in the properties menu, very strange. Anyway after doing that and using the exact name of the font as it appears in the font chooser it worked.

public function moveX(toMove){
  if(Math.abs(toMove.x - this.fadeIns[toMove].targetX) > 2)
    return Math.abs(toMove.x - this.fadeIns[toMove].targetX);
  return false;
}

public function moveY(toMove){
  if(Math.abs(toMove.y - this.fadeIns[toMove].targetY) > 2)
    return Math.abs(toMove.y - this.fadeIns[toMove].targetY);
  return false;
}

These are responsible for determining if a text is to be moved or not, i.e. if it is within a given treshold or not (we need to stop sometime).

public function offsetHandler(event:Event):void{
  if(this.moveX(event.currentTarget)){
    event.currentTarget.x -= Math.round(this.fadeIns[event.currentTarget].movX);
    if(this.moveX(event.currentTarget) < 30)
      this.fadeIns[event.currentTarget].movX /= 1.059;
  }
    
  if(this.moveY(event.currentTarget)){
    event.currentTarget.y -= Math.round(this.fadeIns[event.currentTarget].movY);
    if(this.moveY(event.currentTarget) < 30)
      this.fadeIns[event.currentTarget].movY /= 1.059;
  }
  
  if(!this.moveX(event.currentTarget) && !this.moveY(event.currentTarget))
    event.currentTarget.removeEventListener(Event.ENTER_FRAME, this.offsetHandler);
}

The main logic responsible for moving the texts, note the use of Math.round, if we allow fractions here the animation gets jerky for some reason.

Note also my pathetic attempt at easing.

public function setOffset(toMove, offsX:Number, offsY:Number, fadeVal:Number){
  this.fadeIns[toMove].movX 		= (offsX / Math.abs(offsX)) * 2.5;
  this.fadeIns[toMove].movY 		= (offsY / Math.abs(offsY)) * 2.5;
  this.fadeIns[toMove].targetX 	= toMove.x;
  this.fadeIns[toMove].targetY 	= toMove.y;
  toMove.x += offsX;
  toMove.y += offsY;
  toMove.addEventListener(Event.ENTER_FRAME, this.offsetHandler);
}

Responsible for setting up the animation by moving the texts initially and so on.

public function getByUrl(curUrl:String){
  return this.isSub(curUrl) ? getSub(curUrl) : this.getMain(curUrl);
}

public function isSub(urlSub:String){
  return this.getMain(urlSub).label.length() == 0 ? true : false;
}

public function isSubSub(urlSub:String){
  if(this.isSub(urlSub))
    return this.menuXml.item.subMenu.item.elements('subMenu').item.(elements('url') == urlSub).length() == 0 ? false : true;
  return false;
}

public function getMainFromSub(urlSub:String):XML{
  var temp = this.getSub(urlSub).parent().parent();
  return this.isSubSub(urlSub) == true ? temp.parent().parent() : temp;
}

public function getSub(urlSub){
  var temp = this.menuXml.item.subMenu.item;
  if(temp.(url == urlSub).length() > 0){
    return temp.(url == urlSub);
  }else
    return temp.elements('subMenu').item.(elements('url')[0] == urlSub);
}

public function getMain(mainUrl){
  return this.menuXml.item.(url == mainUrl);
}

This is maybe one of the most interesting parts, the above functions are responsible for retrieving various information from the XML data. Let’s cover them one by one:

getByUrl is responsible for retrieving any item by the contents of its URL, it looks like this: <item><url>index.htm</url>…</item>… As you can see it is just a wrapper and make use of functions further down.

isSub will check if any given url is the identifier of a sub menu item or a main menu item. Note the use of length() to determine if we get something or not.

isSubSub will check if we have a sub menu item on the third level (the liveaboards again). Let’s walk through that monster line there:
1.) this.menuXml.item.subMenu.item will retrieve all items in a submenu, note that this syntax only reliably works if you actually have for instance subMenus in each main item and so forth.
2.) elements(‘subMenu’), since not all sub menu items have a subMenu of their own we make use of elements() since it wont break down and cry if we don’t have third level sub menus everywhere.
3.) However, all third level subs will have one or more items, hence .item.
4.) (elements(‘url’) == urlSub), this is the search clause, we get all url elements from the third level items and note the use of elements(‘url’) because not all of them will contain url elements (it’s complicated). Anyway we match them with the urlSub argument.
5.) Finally is the length() equal to zero? If yes then the given URL is not a third level sub, if the length is bigger than zero then of course we have a third level item.

getMainFromSub will get the topmost main menu item given any second level or third level URL identifier. It does so by first using the getSub function to get the item in question and then checking if it’s a third level or second level sub menu item. Whatever is the case we act accordingly to get the main menu item it belongs to.

getSub will get a sub menu item regardless of whether it’s third or second level. Note the use of elements(‘url’)[0]. Since some elements wont have an url at all we need to use elements(‘url’), however since that one will return a list we can’t use that list to compare with a string. However we know it will only contain one URL since an element can’t have more than one, hence the use of [0].

getMain, not much to say here given the info above.

public function mainRollOver(event:MouseEvent):void{
			event.currentTarget.label.textColor = 0xFFFFFF;
		}
		
		public function mainRollOut(event:MouseEvent):void{
			event.currentTarget.label.textColor = 0xFFFF33;
		}
		
		public function subRollOver(event:MouseEvent):void{
			event.currentTarget.label.textColor = 0xFFFF33;
		}
		
		public function subRollOut(event:MouseEvent):void{
			event.currentTarget.label.textColor = 0xFFFFFF;
		}
	}
}

The mouse over events, different for the main and sub menus.

package{
    import flash.display.Sprite;
    import flash.events.*;
    import flash.net.URLLoader;
    import flash.net.URLLoaderDataFormat;
    import flash.net.URLRequest;

    public class XMLLoader extends Sprite{
		
		var callBack:Function;
		
		public function XMLLoader(){}
		
		public function getXML(targetUrl:String, callBack:Function){
			var request:URLRequest 	= new URLRequest(targetUrl);
      var loader:URLLoader 	= new URLLoader();
      this.callBack 			= callBack;
      loader.addEventListener(Event.COMPLETE, completeHandler);
      try{
          loader.load(request);
      }catch (error:ArgumentError){
          trace("An ArgumentError has occurred.");
      }catch (error:SecurityError){
          trace("A SecurityError has occurred.");
      }
		}
		
    private function completeHandler(event:Event):void{
      var dataXML:XML = XML(event.target.data);
			this.callBack(dataXML);
    }

  }
}

And finally the XML class, basically a copy paste from the CS3 help. Note the use of callBack:Function to enable that anonymous function to work in the first listing of this tutorial.

Related Posts

Tags: , , , ,