OK, let’s begin talking about Design Patterns.
The best thing of OOP, is the capability to create something in way to reuse it in another situation by defining some patterns to follow. Basicly this is Design Patterns. There are several kinds of Patterns and one of them is the Singleton Pattern. It consists in create only one instance of a class and control it by global functions and properties. Unless you are using a multi-touch device, a good example of usage is a CustomCursor class. There is no need to create more than one instance of a custom cursor, because you can point to only one thing at time. However you can change your cursor behavior from anywhere in your application.
The Basics
Let’s create our Singleton class
package { import flash.display.Sprite; public class CustomCursor extends Sprite { private static var _instance:CustomCursor; public function CustomCursor(singleton:SingletonEnforcer):void{ } public static function getInstance():CustomCursor{ if(CustomCursor._instance == null){ CustomCursor._instance = new CustomCursor(new SingletonEnforcer()); } return CustomCursor._instance; } } } class SingletonEnforcer { public function SingletonEnforcer():void{ } }
Basicly what is happening here is that you can’t instantiate this class unless you pass a SingletonEnforcer parameter to the constructor. However you can’t create a SingletonEnforcer instance because it’s a private class of your CustomCursor class, that means that no one but the CustomCursor class can instantiate it. So, to get an instance of this class you need to call the static method getInstance and he will return the only possible instance that your application can have.
package { import flash.display.Sprite; public class Main extends Sprite { public function Main():void{ var cursor:CustomCursor = CustomCursor.getInstance(); trace(cursor) } } }
OK, now that we have our single cursor instance, we need add it to our display list. But we must be careful to insure that our cursor will be over everything else in the display list. To do that, there is a great event that is dispatched when a DisplayObject is added to the stage and we can use it to check if our instance is above everything else.
public function CustomCursor(singleton:SingletonEnforcer):void{ addEventListener(Event.ADDED_TO_STAGE, bringToFront); } private function bringToFront(e:Event):void{ if(parent != stage){ stage.addChild(this); } }
There is a diference adding childs or event listeners to your document class and to your stage. Your document class is a child of your stage, that’s why when your add mouse move events to your document class it doesn’t work when you move the mouse on the background. Check it by tracing the number of childs of your stage before and after adding the cursor class.
var cursor:CustomCursor = CustomCursor.getInstance(); trace(stage.numChildren); // 1 addChild(cursor) trace(stage.numChildren); // 2
This will place our cursor over the main timeline.
Now you need to define what kind of behaviors your cursor will have. Like a grab behavior where when your pointer is over something that can be grabbed it assumes a image of an open hand and, when you press the mouse button, it changes to a image of a closed hand. So for each behavior you will have up to four pointer states, mouse over button up, mouse over button down, mouse out button up and mouse out button down. For this example we’ll make only one behavior with two states that shows an arrow for mouse out button up and down, and a hand for mouse over button up and down.
Let’s create a base class for every behavior. In the constructor we need to pass the bitmapData that the pointer will assume for each state and a setter that defines the state of the pointer.
package { import flash.display.Bitmap; import flash.display.BitmapData; public class CustomCursorBehavior extends Bitmap{ private var _outUp:BitmapData; private var _outDown:BitmapData; private var _overUp:BitmapData; private var _overDown:BitmapData; public function CustomCursorBehavior(outUp:BitmapData, outDown:BitmapData, overUp:BitmapData, overDown:BitmapData):void{ _outUp = outUp; _outDown = outDown; _overUp = overUp; _overDown = overDown; super(outUp); } public function set state(value:String):void { if(value == "outUp"){ bitmapData = _outUp; } else if(value == "outDown"){ bitmapData = _outDown; } else if(value == "overUp"){ bitmapData = _overUp; } else if(value == "overDown"){ bitmapData = _overDown; } } } }
Now we need to load our pointer images and create our behaviour in the CustomCursor class. We could embed our images in the SWF file, but this way is easier to change any image if we need to. So we will load two images, pointer.gid and hand.gif, then create our default behavior, add it as a child (since it extends a bitmap) to our custom cursor, hide de mouse cursor and add a MouseEvent listener to the stage that dispatches when the mouse moves, so we can set our cursor position as same as the mouse position.
private var initied:Boolean = false; private var loader:Loader; private var queue:Array; private var bmpData:Array; private static var defaultBehavior:CustomCursorBehavior; public function CustomCursor(singleton:SingletonEnforcer):void{ addEventListener(Event.ADDED_TO_STAGE, bringToFront); } private function bringToFront(e:Event):void{ if(parent != stage){ stage.addChild(this); } if(!initied) init(); } private function init():void{ initied = true; loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, loadNextItem); queue = new Array(); queue.push("pointer.gif"); queue.push("hand.gif"); bmpData = new Array(); loader.load(new URLRequest(queue[0])); } private function loadNextItem(e:Event):void{ var bmp:BitmapData = new BitmapData(e.currentTarget.content.width, e.currentTarget.content.height); bmp.draw(e.currentTarget.content.bitmapData); bmpData[queue[0]] = bmp; queue.splice(0, 1); if(queue.length > 0){ loader.load(new URLRequest(queue[0])); } else { setupBehaviors(); } } private function setupBehaviors():void{ defaultBehavior = new CustomCursorBehavior(bmpData["pointer.gif"], bmpData["pointer.gif"], bmpData["hand.gif"], bmpData["hand.gif"]); addChild(defaultBehavior); Mouse.hide(); stage.addEventListener(MouseEvent.MOUSE_MOVE, setPosition); } private function setPosition(e:MouseEvent):void{ x = stage.mouseX; y = stage.mouseY; }
Note that our cursor doesn’t move like the mouse cursor, it looks like it have a delay. This is because there is a gap between each frame that our cursor is still moving, so it is not rendered as it sould. To fix this we must add a method called updatAfterEvent() to the setPosition function so it forces flash to render the scene when the mouse is moving.
Another thing to note is that when we move the mouse out of the browser, the cursor stays in the scene. For that we need to add an event listener to the stage called MOUSE_LEAVE. This event is dispatched when we move out from the browser, and allow us to know when set the cursor invisible. But there’s no event called like MOUSE_RETURN to say when set the cursor visible again, so we need to force it when the mouse moves.
private function setupBehaviors():void{ defaultBehavior = new CustomCursorBehavior(bmpData["pointer.gif"], bmpData["pointer.gif"], bmpData["hand.gif"], bmpData["hand.gif"]); addChild(defaultBehavior); Mouse.hide(); stage.addEventListener(MouseEvent.MOUSE_MOVE, setPosition); stage.addEventListener(Event.MOUSE_LEAVE, mouseLeave); } private function setPosition(e:MouseEvent):void{ Mouse.hide(); visible = true; x = stage.mouseX; y = stage.mouseY; e.updateAfterEvent(); } private function mouseLeave(e:Event):void{ Mouse.show(); visible = false; }
OK, now comes the tricky part. How to change the cursor image without adding any code to our buttons?
Well, first let’s create some buttons and movies with buttonMode and check how the mouse behaves.
Note that when the mouse changes it state due the top object that it is over. If the mouse is over the red movie (buttonMode = false) and below it is the green movie (buttonMode = true), the pointer is an arrow, but if the mouse is over the blue movie (buttonMode = true) and below it is the red movie, the pointer is a hand.
So we need to check if the top object under the mouse is a SimpleButton or if it has the buttonMode set to true. To do that theres a method called getObjectsUnderPoint, that returns an array of DisplayObjects wher the last item is the top object. However, there is some exceptions to take carre.
First is that we need to check object from the main timeline. So we define the container like this. Second is that the array return Shapes and TextFields, so we must check from the current object to the Main timeline the mouse state. And finaly we must check if the display object is set to mouseEnabled = false.
This method will be called on enter frame because we can have objects moving in the scene while our mouse stands still.
private var onState:String = "out"; private function checkState(e:Event):void{ var container:Sprite = Sprite(stage.getChildAt(0)); var arr:Array = container.getObjectsUnderPoint(new Point(x, y)); var displayObject:DisplayObject; var bMode:Boolean for(var i:Number = 0; i<arr.length; i++){ displayObject = arr[i]; bMode = false; while(displayObject != stage){ if(displayObject is Sprite){ if(Sprite(displayObject).buttonMode){ bMode = true; } } else if(displayObject is SimpleButton){ if(SimpleButton(displayObject).mouseEnabled){ bMode = true; } } displayObject = displayObject.parent; } } if(bMode){ onState = "over"; } else { onState = "out"; } }
Another thig to define or cursor state is to kwon if the mouse button is up or down.
private var buttonState:String = "Up"; private function setUpState(e:MouseEvent):void{ buttonState = "Up"; } private function setDownState(e:MouseEvent):void{ buttonState = "Down"; }
Ok, now we need to create a method that changes the cursor state if needed.
private function changeState():void{ if(currentState != onState+buttonState){ currentState = onState+buttonState; defaultBehavior.state = currentState; } }
We are almost there. Now we need to create a method that changes the behavior of our customCusor, this method must be static because we need to call it from anywhere of our application.
private static const DEFAULT:String = "default"; private static var _behavior:String = DEAULT; private static var currentBehavior:CustomCursorBehavior; public static function set behavior(value:String):void{ if(value != _behavior){ if(value == DEFAULT){ currentBehavior = defaultBehavior; _behavior = DEFAULT; } } } public static function get behavior():String{ return _behavior; }
With that, we need to change this line of our changeState method.
//From defaultBehavior.state = currentState; //To currentBehavior.state = currentState;
OK, now let’s see the result
You can create any kind of behavior to you application, just define it in the customCursor class and load the images that you need.
Download the source here