In this tutorial I'll show you how to create a menu like Apple's Dock using AS3 classes. We will create a single AS file that will perform all the magic, extending it to add new features.
Every few weeks, we revisit some of our reader's favorite posts from throughout the history of the site. This tutorial was first published in March of 2010.
Final Result Preview
First, let's take a look at what we'll be creating. Roll your mouse over the icons to see how they move and scale.
Step 1: Create a New ActionScript File
Begin by creating a new ActionScript file and saving it as "DockItem.as". I'm saving mine at c:/macmenu/org/effects/DockItem.as.
Note that our document root (where the .fla lives) will be c:/macmenu; the folder /org/effects will form the package for the DockItem class.
Step 2: Create a New FLA
Create a new ActionScript 3.0 Flash File and open it, so that we have both DockItem.as and this .fla file opened. Save this .fla in the root folder (the DockItem.as is at c:/macmenu/org/effects, so our site root is c:/macmenu) the /org/effects is the package of DockItem Object and we save the .fla as c:/macmenu/macmenu.fla.
Step 3: Import Icons
Now we import or draw some icons to the .fla. I've imported some icons I have here from an Illustrator file, but you can of course draw your own and apply a gradient to them.
Step 4: Begin Converting Icons to Symbols
Select any icon and click Modify > Convert To Symbol.
In the box that opens, give it a name (I named this symbol "Star") and pay attention to the registration point; it needs to be bottom center. For the class use the same name (remember that you can't use spaces) and for the Base class, use org.effects.DockItem (the class that we'll create). Also, make sure your Type is set to Movie Clip.
Then, align all the objects to the bottom: select all, click Window > Align, make sure the button "To stage" is unselected (otherwise it will align at the botton of the stage), then click the top-right button in this panel to align all the objects.
Step 5: Convert All Icons to Symbols
We can have as many buttons as we want, so let's convert all our icons to symbols. Remember to give them a name and a Class, set all their registration points to bottom center and set the Base class to org.effects.DockItem.
See below for how our library and the icons should look; note the space between them, it's important for creating a good effect.
Step 6: Start Coding the DockItem Class
If we test the movie now it will throw an error saying that an ActionScript file must have at least one external and visible definition; that's because all our menu items are extending the DockItem class, which we haven't yet written. Let's write it now...
Start creating the package by extending the Sprite class (we will extend Sprite since we don't have a timeline animation.)
package org.effects{ import flash.display.Sprite; public class DockItem extends Sprite{ } }
At this point we have our DockItem extending the Sprite class, so if you test it now it will work, but you'll see no effects.
(Confused? Not used to coding with classes? Check out this Quick Tip on using a document class for an introduction.)
Step 7: Import Necessary Classes
Now we will import all the necessary classes. A custom class is being used here, the TweenLite class, which you can download from GreenSock.com. When you've downloaded TweenLite, extract it to your /macmenu/ folder (so you will have a folder /macmenu/com/greensock/).
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; //http://www.greensock.com/tweenlite import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ } }
I've imported the Sprite class because it's what we are extending; if you have animations on the timeline, extend the MovieClip class. We will use the Event class when the custom object is added to stage and we'll use the MouseEvent when checking the distance of each icon from the mouse.
Step 8: Declare Necessary Variables
During this step we'll declare the necessary variables:
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ private var _initPosition:Number; public var maxXDistance:Number; public var maxYDistance:Number; public var maxScale:Number; } }
Note that I used the _initPosition as private: it just sets the initial x-position of the icon. The distance of the mouse will always be measured from this point, because the actual x-position of the item will always be changing.
maxXDistance is the maximum x-distance over which the mouse will affect the icon, maxYDistance is the maximum y-distance over which mouse will affect the icon and maxScale is the maximum scale that will be added to the icon (for example, if you set it to 2, the maximum scale the object can reach is 3.)
I've used public variables for the last three so we can change them at runtime.
Step 9: Coding the Constructor Function
The constructor function must have the same name as the class (and therefore the same name as the file), hence DockItem():
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ private var _initPosition:Number; public var maxXDistance:Number; public var maxYDistance:Number; public var maxScale:Number; public function DockItem($maxXDistance:Number=60,$maxYDistance:Number=30,$maxScale:Number=2):void{ maxXDistance=$maxXDistance; maxYDistance=$maxYDistance; maxScale=$maxScale; if(stage) init(); else addEventListener(Event.ADDED_TO_STAGE,init); addEventListener(Event.REMOVED_FROM_STAGE,end); } } }
Why do we have some parameters here? This allows us to use different combinations of distances and scales: we can have a short distance with a very big scale or a long distance with a small scale. Also, we can determine the y distance within which the mouse will affect the icon.
As we are extending the Sprite class we can add children or even code a custom class for each icon extending the DockItem class, so if we extend it we can use the super() function to pass the new parameters to the superclass. We can then use the DockItem class anytime and anywhere.
In this step we set the maxXDistance variable, maxYDistance variable and the maxScale variable to the values passed as parameters. Also, we check if the object is on the stage - if not, we add an Event to check when it is. We also add another event listener to detect when the icon is removed from the stage. We'll add a MOUSE_MOVE event to the stage to get the distance, so it's important to know whether it's on the stage.
Step 10: The Init() Function
This is the function that will be run once the icon is created and added to the stage. In the init() function we just add an MouseEvent.MOUSE_MOVE listener to the stage, set the _initPosition variable to the x value of the object, and listen for the mouse leaving the area of stage.
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ private var _initPosition:Number; public var maxXDistance:Number; public var maxYDistance:Number; public var maxScale:Number; public function DockItem($maxXDistance:Number=60,$maxYDistance:Number=30,$maxScale:Number=2):void{ maxXDistance=$maxXDistance; maxYDistance=$maxYDistance; maxScale=$maxScale; if(stage) init(); else addEventListener(Event.ADDED_TO_STAGE,init); addEventListener(Event.REMOVED_FROM_STAGE,end); } private function init(e:Event=null):void{ _initPosition=x; stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMove); stage.addEventListener(Event.MOUSE_LEAVE,mouseLeave); } } }
Step 11: The Mouse Functions
When the mouse moves over the stage, this function (triggered by the MOUSE_MOVE event we added a listener for in the last step) will check the mouse position of the parent object and measure the distance from the object to the mouse parent position.
We use parent.mouseX because that gets us the x-position of the mouse relative to whichever object contains the icon, rather than relative to the registration point of the icon.
We also tween the icons back to their starting positions if the mouse leaves the stage in the mouseLeave() handler.
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ private var _initPosition:Number; public var maxXDistance:Number; public var maxYDistance:Number; public var maxScale:Number; public function DockItem($maxXDistance:Number=60,$maxYDistance:Number=30,$maxScale:Number=2):void{ maxXDistance=$maxXDistance; maxYDistance=$maxYDistance; maxScale=$maxScale; if(stage) init(); else addEventListener(Event.ADDED_TO_STAGE,init); addEventListener(Event.REMOVED_FROM_STAGE,end); } private function init(e:Event=null):void{ _initPosition=x; stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMove); stage.addEventListener(Event.MOUSE_LEAVE,mouseLeave); } private function mouseMove(e:MouseEvent):void{ var yDistance:Number=Math.abs(parent.mouseY-y); if(yDistance>maxYDistance){ if(_initPosition==x) return; else{ TweenLite.to(this,.3,{x:_initPosition,scaleX:1,scaleY:1}); return; } } //get the difference between the parent mouse x position and the initial position of the object var xDistance:Number=parent.mouseX-_initPosition; //check if the distance of the mouse from the object is more than max distance, it can't be bigger... xDistance = xDistance > maxXDistance ? maxXDistance : xDistance; //check if the distance is lower than the negative of the max distance, it can't be lower... xDistance = xDistance < -maxXDistance ? -maxXDistance : xDistance; //create a variable for the position, assuming that the x position must be the initial position plus the distance of the mouse, but it can't be more than the max distance. var posX=_initPosition-xDistance; //we get the scale proportion here, it goes from 0 to maxScale variable var scale:Number=(maxXDistance-Math.abs(xDistance))/maxXDistance; //the minimum scale is 1, the original size, and the max scale will be maxScale variable + 1 scale=1+(maxScale*scale); //here we use a Tween to set the new position according to the mouse position TweenLite.to(this,.3,{x:posX,scaleX:scale,scaleY:scale}); } private function mouseLeave(e:Event):void{ TweenLite.to(this,.3,{x:_initPosition,scaleX:1,scaleY:1}); } } }
First, we check the y distance (vertical distance between the icon and the mouse); if it's further away than the range we set with the maxYDistanceVariable, then we check whether the icon is back in its original position, and, if not, we tween it there. The return keyword breaks out of the function, so none of the rest of the code will be run in this case.
If the mouse is close to the icon vertically, we use some maths to figure out a new scale and position for the icon based on its horizontal distance from the mouse, then tween it to those values.
Step 12: The End() Function
If we remove the object from the stage, we need to remove the mouseMove and mouseLeave listeners; if not we can get errors every time the mouse is moved. This function is the handler for the REMOVED_FROM_STAGE listener we added earlier, so will be triggered when the object is removed.
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ private var _initPosition:Number; public var maxXDistance:Number; public var maxYDistance:Number; public var maxScale:Number; public function DockItem($maxXDistance:Number=60,$maxYDistance:Number=30,$maxScale:Number=2):void{ maxXDistance=$maxXDistance; maxYDistance=$maxYDistance; maxScale=$maxScale; if(stage) init(); else addEventListener(Event.ADDED_TO_STAGE,init); addEventListener(Event.REMOVED_FROM_STAGE,end); } private function init(e:Event=null):void{ _initPosition=x; stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMove); stage.addEventListener(Event.MOUSE_LEAVE,mouseLeave); } private function mouseMove(e:MouseEvent):void{ var yDistance:Number=Math.abs(parent.mouseY-y); if(yDistance>maxYDistance){ if(_initPosition==x) return; else{ TweenLite.to(this,.3,{x:_initPosition,scaleX:1,scaleY:1}); return; } } //get the difference between the parent mouse x position and the initial position of the object var xDistance:Number=parent.mouseX-_initPosition; //check if the distance of the mouse from the object is more than max distance, it can't be bigger... xDistance = xDistance > maxXDistance ? maxXDistance : xDistance; //check if the distance is lower than the negative of the max distance, it can't be lower... xDistance = xDistance < -maxXDistance ? -maxXDistance : xDistance; //create a variable for the position, assuming that the x position must be the initial position plus the distance of the mouse, but it can't be more than the max distance. var posX=_initPosition-xDistance; //we get the scale proportion here, it goes from 0 to maxScale variable var scale:Number=(maxXDistance-Math.abs(xDistance))/maxXDistance; //the minimum scale is 1, the original size, and the max scale will be maxScale variable + 1 scale=1+(maxScale*scale); //here we use a Tween to set the new position according to the mouse position TweenLite.to(this,.3,{x:posX,scaleX:scale,scaleY:scale}); } private function mouseLeave(e:Event):void{ TweenLite.to(this,.3,{x:_initPosition,scaleX:1,scaleY:1}); } private function end(e:Event=null):void{ stage.removeEventListener(MouseEvent.MOUSE_MOVE,mouseMove); stage.removeEventListener(Event.MOUSE_LEAVE,mouseLeave); } } }
All we do in this function is remove the event listener from the stage.
Step 13: Test It!
At this point we can already test it; it will work since each object is linked with the Base class DockItem. However, we don't have a bounding box for clicking (if we set our object's buttonMode property to true, we'll see that we can click it only when it's over the actual graphic.)
Step 14: Start Turning Icons Into Buttons
So far we can see the effect working, so now let's turn each item into a button. We'll create a new ActionScript file and this one will extend the DockItem - let's name it DockButton. Its package will be the same as DockItem (org.effects), so we'll save itb in the same folder as DockItem.as (example: c:/macmenu/org/effects/DockButton.as)
Step 15: Change the Base Class
Now we change the base class of each object in the library. We are currently using org.effects.DockItem as Base class, let's now use org.effects.DockButton.
If we test it now, there will be an error. This is because DockButton.as is still empty, so let's code it.
Step 16: Start Coding DockButton.as
OK, now we'll extend the DockItem class, because we want to use everything that we have in DockItem and add some more tricks (allowing it to act as a button), but we don't want to add the new features to DockItem directly. This way, if we want to use the DockItem as anything other than a Button later on, we can, but if we want to use it as a Button we can use the DockButton.
package org.effects{ public class DockButton extends DockItem{ } }
If we test our project now, it will work, but it will work exactly as the DockItem as we haven't yet added anything new.
Step 17: Import Classes for DockButton
Let's import some things we will need to extend the DockItem. As we are extending the DockItem we don't need to import the classes that are already there, since we wont use them directly in DockButton.
package org.effects{ import flash.geom.Rectangle; public class DockButton extends DockItem{ } }
I've imported the Rectangle class, but why? It's because we will use the bounding box of our object to create a fake background, to allow the button to be clickable even if the mouse isn't precisely over a colored area. Let's create a background graphic with alpha 0 (transparent), so we will have a square to click.
Step 18: Constructor for DockButton
Since we need to create a bounding box for DockButton, we will get its own bounds, that's why we imported the flash.geom.Rectangle class
package org.effects{ import flash.geom.Rectangle; public class DockButton extends DockItem{ public function DockButton():void{ buttonMode=true; mouseChildren=false; var bounds:Rectangle=getBounds(this); this.graphics.beginFill(0,0); this.graphics.drawRect(bounds.x,bounds.y,bounds.width,bounds.height); this.graphics.endFill(); } } }
What we have done? We created a constructor which first sets the object's buttonMode to true, so our DockButton will be treated as a Button. Then we set mouseChildren to false, so mouse events will come from the DockButton object, not any other object inside it. Next we get the bounds of the object using getBounds() and draw a transparent rectangle using the graphics object. (The graphics property comes with the Sprite class, and we extended Sprite to make our DockItem object. Now we've extended our DockItem to make our DockButton object, DockButton has everything from the Sprite class and the DockItem class.)
Step 19: Check Everything and Test It
OK, let's perform a check:
- We need a .fla file (example: c:/macmenu/macmenu.fla).
- In the same folder as the .fla file we need to have another folder: /org/effects (example: c:/macmenu/org/effects).
- Inside this folder we need to have two .as documents (DockItem.as and DockButton.as)
- Within the .fla, each item in the library must be linked to a class, and the base class of each item must be org.effects.DockButton.
If it's all OK, test the movie...
(At this point, if you want to put the folder org/effects in your classpath you can, so you won't need to copy this folder to each project you create and use the DockItem or DockButton.)
Step 20: Change the Color on Mouse Over
Why not change the color of the button when the mouse passes over it? In this section I will teach how. For this we will use the TweenLite engine again to give some tint to the object. However, we are already using TweenLite in the DockItem object and we are extending this object at DockButton. We want to extend DockButton to change the color, but we can't use TweenLite anymore in the same object since the new TweenLite object will overwrite the other one (even with the property overwrite:false in TweenLite it will reduce the performance a lot if we use it directly in the same object). All is not lost; we have an icon inside each object of the library and we can apply the tint to that.
To do this, let's create another ActionScript File, but now save this one at the same folder as the .fla with the name "OverButton.as" (example: c:/macmenu/OverButton.as.)
Step 21: Coding the OverButton Object
First we create the package and import the necessary classes; since we saved the OverButton.as file in the same folder of the .fla file the package will be top level, so there's no need to write "package org.effects":
package{ import org.effects.DockButton; import flash.display.DisplayObject; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class OverButton extends DockButton{ } }
OK, so we're extending DockButton this time and we've imported the DisplayObject class because we will treat the icon as a DisplayObject. We've also imported MouseEvent which we'll use to check when the mouse is over the icon and when it's out. We also have TweenLite to create some tween effects with the color.
Step 22: OverButton Constructor
package{ import org.effects.DockButton; import flash.display.DisplayObject; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class OverButton extends DockButton{ private var _object:DisplayObject; public function OverButton():void{ _object=this.getChildAt(0) as DisplayObject; this.addEventListener(MouseEvent.MOUSE_OVER, mouseOver); this.addEventListener(MouseEvent.MOUSE_OUT, mouseOut); TweenPlugin.activate([TintPlugin]); } } }
Why have we created a private var _object as DisplayObject? Our actual icon is stored in this variable (that's what line 13 does) and is treated as a DisplayObject; we will use the color effect on our icon, not in the whole object.
We add the event listeners of the mouse to check when the mouse is over and when the mouse is out.
Step 23: Coding Mouse Functions
Since we have created the listeners for mouse over and mouse out, we will now create their functions:
package{ import org.effects.DockButton; import flash.display.DisplayObject; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class OverButton extends DockButton{ private var _object:DisplayObject; public function OverButton():void{ _object=this.getChildAt(0) as DisplayObject; this.addEventListener(MouseEvent.MOUSE_OVER, mouseOver); this.addEventListener(MouseEvent.MOUSE_OUT, mouseOut); TweenPlugin.activate([TintPlugin]); } private function mouseOver(e:MouseEvent):void{ new TweenLite(_object,.5,{tint:0x990099}); } private function mouseOut(e:MouseEvent):void{ new TweenLite(_object,.5,{tint:null}); } } }
Note that we are using the TweenLite on _object now, not on "this" any more. That's because the OverButton extends the DockButton which extends the DockItem where there is already a TweenLite being used. Also, in DockButton we have a fake alpha 0 background that doesn't need to be painted.
For the tint property of TweenLite I used a color code of 0x990099, which is a medium purple; if you use null as the value the tint will be removed softly.
Step 24: Change the Base Classes
At this point if you test the movie, you won't see any color change, because we need to change the base class of each object in the library again. Open the Library once more in the .fla (Window > Library). Right-click each object and change its base class to OverButton (not org.effects.OverButton, because the class file is not in the /org/effects folder).
OK, now you can test it!
Conclusion
In this tutorial I've explained about extending objects. The actual dock effect is pure math - it's distance calculations, scale settings - but it's important we see in the code that we cant use the "x" property as position reference, because the "x" property is changed every time. I hope now you all have a better understanding of the "extends" keyword, and can appreciate how the calculations are done here. Thanks for reading :)
Comments