Advertisement

Build an Adobe AIR Stopwatch Application

by

In this tutorial, we will build an Analog Timer AIR Application that we can use as a utility to time development work. We will be using a Custom Timer class that has Pause and Resume capabilities. This custom class will help you with a lot of Timer based applications as you'll see with a few examples.
I created this project both with Flash CS4 and FlashDevelop. Although FlashDevelop can greatly speed up code development, everything can be done through Flash alone. If you'd like to try out FlashDevelop, you can download it from here.

Note: If you plan to use Flash CS3 and do not already have Adobe AIR 1.5 set up with it, you can do so by following instructions from here.


Step 1: Initial Setup

Create a new folder anywhere you'd like and name it 'AnalogTimer'.

Open Flash CS3/CS4 and hit CTRL + N to open the new Document window and choose Flash File (ActionScript 3.0). Click OK then save it as test.fla inside the 'AnalogTimer' folder.

I've included the Inconsolata font with the source download. You might need to install this into your OS if you don't already have it.

Note: if using Flash CS3, set the frame rate to 40.


Step 2: Create the CustomTimer Class

If you are using Flash for code development, hit CTRL + N and this time, choose ActionScript File. Click OK and save the file inside the 'AnalogTimer' folder and name the class CustomTimer.as.

If you are using FlashDevelop for code development, open up FlashDevelop and hit CTRL + 2 to create a new ActionScript 3.0 file. Save it as CustomTimer in the same folder you have saved test.fla.


Step 3: Setting up the Class and all of its Required Imports

We will have it extend the flash.events.EventDispatcher class since it needs to dispatch custom events. Add the following lines of code:

package 
{ 
	import flash.events.Event; 
	import flash.events.EventDispatcher; 
	import flash.events.TimerEvent; 
	import flash.utils.getTimer; 
	import flash.utils.Timer; 
	 
	public class CustomTimer extends EventDispatcher 
   { 
 
   } 
}

(Not sure what you're doing with this? Check out this quick introduction to document classes.)


Step 4: Class Constants, Variables and the Constructor

Right inside the class brackets, we will add the public constants, variables and the constructor method. These constants will be the values of the events dispatched.

The variables will hold all the values we need to access about the CustomTimer's current status.

The constructor will have one required parameter, $delay, of type Number, which will be assigned as the delay for the Timer object. The second parameter is optional: $repeatCount, which is the total number of times the timer is set to repeat. If this is not assigned, the default value is set to 0.

Inside the constructor, we just assign the parameters to their class variable counterparts, _currentCount is set to zero and it then calls the init() method.

Add the following lines of code:

public static const TIMER_STARTED:String  = 'timerStarted'; 
public static const TIMER_PAUSED:String   = 'timerPaused'; 
public static const TIMER_RESUMED:String  = 'timerResumed'; 
public static const TIMER_FINISHED:String = 'timerFinished'; 
 
public static const TICK:String 		   = 'tick'; 
		 
private var _delay:Number;//assigned the $delay parameter from the constructor 
private var _timeLeft:Number;//derived from the current getTimer() value minus _changeTime 
private var _timer:Timer;//Timer object 
private var _startTime:Number;//assigned the current getTimer value when start() is called 
private var _changeTime:Number;//initially assigned with _startTime + _delay, then getTimer() + _delay after it is reached (see onTick() method) 
private var _repeatCount:Number; 
private var _currentCount:Number; 
 
public function CustomTimer ($delay:Number, $repeatCount:Number = 0):void 
{ 
	_delay = $delay; 
   _timeLeft = _delay; 
   _repeatCount = $repeatCount; 
   _currentCount = 0; 
	 
   init (); 
}

Step 5: The init() Method

When the constructor calls the init() method, the _timer variable is assigned a new Timer class with a delay of 1 millisecond and adds an onTick listener for the 'TimerEvent.TIMER' event.

I've included a copy of an EnterFrameCustomTimer class with the source download. If you plan on using more than 20 Timer classes at a time, with a delay parameter no smaller than 100 milliseconds, I would recommend to use the EnterFrameCustomTimer. Just increase the frameRate accordingly.

Anything smaller than 100, e.g. 10 milliseconds, and the EnterFrameCustomTimer will be way off track unless you set the fps to 120.

Add the following lines of code right below the constructor method:

private function init():void 
{ 
	_timer = new Timer (1); 
	_timer.addEventListener (TimerEvent.TIMER, onTick); 
}

(Note: the TIMER event won't fire every millisecond exactly, but it's good enough for our needs. See this article on the 'Elastic Racetrack' for more details.)


Step 6: External Controls

Before we go into the onTick() method, let's add the external controls for the CustomTimer class. The start() method is in charge of starting the CustomTimer. As mentioned earlier, the _startTime is assigned the getTimer() value. _changeTime is then calculated by adding the _startTime + _delay. The _timer is then started and a CustomTimer.TIMER_STARTED event is dispatched.

The pause() method checks to see if the _timer is running and if it is, stops it and dispatches a CustomTimer.TIMER_PAUSED event.

The resume() method takes care of adjusting _changeTime by adding getTimer() + _timeLeft then dispatches a CustomTimer.TIMER_RESUMED event. You'll see that _timeLeft is updated every time the onTick() method is called.

The quit() method stops the _timer, removes the TimerEvent.TIMER listener and then assigns null to the _timer variable, making it available for garbage collection.

Add the following lines of code right below the init() method:

public function start():void 
{ 
	_startTime = getTimer (); 
	_changeTime = _startTime + _delay; 
	_timer.start (); 
	dispatchEvent (new Event (TIMER_STARTED)); 
} 
		 
public function pause():void 
{ 
	if (_timer.running) 
	{ 
		_timer.stop (); 
		dispatchEvent (new Event (TIMER_PAUSED)); 
	} 
} 
	 
public function resume():void 
{ 
	_changeTime = getTimer() + _timeLeft; 
	_timer.start (); 
	dispatchEvent (new Event (TIMER_RESUMED)); 
} 
 
public function quit():void 
{ 
	_timer.stop (); 
	_timer.removeEventListener (TimerEvent.TIMER, onTick); 
	_timer = null; 
}

Save the file before moving on.


Step 7: The onTick() Method

This method is called every millisecond once the CustomTimer has started. It only stops when pause() is called or if the _repeatCount is greater than zero, when the_currentCount has reached _repeatCount.

Let's go through what goes on inside this method.

First a local variable named 'now' is assigned the current getTimer() value. Then, _timeLeft is calculated by subtracting 'now' from _changeTime, remember, changeTime is initially assigned when start() is called. This variable is important since it provides information of when exactly it is within the duration of each iteration. A good example for this is when we pause the CustomTimer, with the information from _timeLeft, we can resume from where we paused.

A conditional statement follows to see if now is greater than or equal to _changeTime. If it is, we add 1 to _currentCount and add _delay to _changeTime

We then have a nested conditional statement to check if _repeatCount is greater than zero; if it isn't, we simply dispatch a CustomTimer.TICK event but if it is, we use another nested conditional statement: this time, we check if _currentCount is less than _repeatCount or not.

If _currentCount is less than _repeatCount, we dispatch a CustomTimer.TICK and if _currentCount is greater than or equal to _repeatCount, it cleans up the Timer object by calling the quit() method. Two events are then dispatched - CustomTimer.TICK and CustomTimer.TIMER_FINISHED.

Note: It is crucial to keep code that is always executed in best form or in other words 'optimized'. You wouldn't want to waste precious memory and processing time over unnecessary calculations.

Add the following lines of code below the quit() method:

private function onTick (e:TimerEvent):void 
{ 
	var now:Number = getTimer (); 
 
	_timeLeft = _changeTime - now; 
 	if (now >= _changeTime) 
	{ 
		_currentCount++; 
		_changeTime += _delay; 
 
		if (_repeatCount > 0) 
		{ 
			if (_currentCount < _repeatCount) 
			{ 
				dispatchEvent (new Event (TICK)); 
			} 
			else 
			{ 
				quit (); 
 
				dispatchEvent (new Event (TICK)); 
				dispatchEvent (new Event (TIMER_FINISHED)); 
			} 
		} 
		else dispatchEvent (new Event (TICK)); 
 	} 
}

Step 8: Getter Methods

These two public methods allow read-only access to the two variables _delay and _currentCount. These properties are similar to the Timer class's delay and currentCount properties. Add the following lines of code:

public function get delay():Number { return _delay; } 
 
public function get currentCount():Number { return _currentCount; }

That completes our CustomTimer.as class. Make sure to save before moving forward.

Your CustomTimer class should now look something like this:

package 
{ 
   import flash.events.Event; 
   import flash.events.EventDispatcher; 
   import flash.events.TimerEvent; 
   import flash.utils.getTimer; 
   import flash.utils.Timer; 
 
   public class CustomTimer extends EventDispatcher 
   { 
		public static const TIMER_STARTED:String  = 'timerStarted'; 
		public static const TIMER_PAUSED:String   = 'timerPaused'; 
		public static const TIMER_RESUMED:String  = 'timerResumed'; 
		public static const TIMER_FINISHED:String = 'timerFinished'; 
 
		public static const TICK:String 		  = 'tick'; 
 
		private var _delay:Number; 
   	private var _timeLeft:Number; 
   	private var _timer:Timer; 
   	private var _startTime:Number; 
   	private var _changeTime:Number; 
   	private var _repeatCount:Number; 
   	private var _currentCount:Number; 
 
   	public function CustomTimer ($delay:Number, $repeatCount:Number = 0):void 
   	{ 
   		_delay = $delay; 
   		_timeLeft = _delay; 
   		_repeatCount = $repeatCount; 
   		_currentCount = 0; 
 
   		init (); 
  		} 
 
  		private function init():void 
  		{ 
   		_timer = new Timer (1); 
   		_timer.addEventListener (TimerEvent.TIMER, onTick); 
   	} 
 
   	public function start():void 
   	{ 
   		_startTime = getTimer (); 
   		_changeTime = _startTime + _delay; 
   		_timer.start (); 
   		dispatchEvent (new Event (TIMER_STARTED)); 
   	} 
 
   	public function pause():void 
   	{ 
   		if (_timer.running) 
   		{ 
   			_timer.stop (); 
   			dispatchEvent (new Event (TIMER_PAUSED)); 
   		} 
   	} 
 
   	public function resume():void 
   	{ 
   		_changeTime = getTimer() + _timeLeft; 
   		_timer.start (); 
   		dispatchEvent (new Event (TIMER_RESUMED)); 
   	} 
 
		public function quit():void 
		{ 
			_timer.stop (); 
			_timer.removeEventListener (TimerEvent.TIMER, onTick); 
			_timer = null; 
		} 
 
   	private function onTick (e:TimerEvent):void 
   	{ 
   		var now:Number = getTimer (); 
 
  			_timeLeft = _changeTime - now; 
 			if (now >= _changeTime) 
			{ 
				_currentCount++; 
				_changeTime += _delay; 
 
				if (_repeatCount > 0) 
				{ 
					if (_currentCount < _repeatCount) 
					{ 
						dispatchEvent (new Event (TICK)); 
					} 
					else 
					{ 
						quit (); 
 
						dispatchEvent (new Event (TICK)); 
						dispatchEvent (new Event (TIMER_FINISHED)); 
					} 
				} 
				else dispatchEvent (new Event (TICK)); 
 
			} 
   	} 
 
   	public function get delay():Number { return _delay; } 
 
   	public function get currentCount():Number { return _currentCount; } 
 
   } 
 
}

Step 9: CustomTimer Testing and Debugging

Now that we have the CustomTimer class built, let's test to see if everything works the way it should. Open up test.fla which we created back in Step 2. Open up the Timeline (CTRL + ALT + T), click Frame 1 and hit F9; this should open up the script window.

Enter the following code:

var delay:Number = 1000;//1 second delay 
var repeat:Number = 5;//repeat 5 times 
var ct:CustomTimer = new CustomTimer (delay, repeat); 
 
//add listeners 
ct.addEventListener (CustomTimer.TICK, onTick); 
ct.addEventListener (CustomTimer.TIMER_FINISHED, onFinish); 
 
//response to listeners 
function onTick (e:Event):void {trace ('tick: ' + e.target.currentCount);} 
function onFinish (e:Event):void {trace ('finished');} 
 
ct.start ();//start the custom timer.

Save and hit CTRL + ENTER to run the movie.

You should see result like the following print out the to output window, with one line every second (except "tick 5" and "finished" should print out at the same time, as this is when the _currentCount has reached _repeatCount and both TICK and TIMER_FINISHED are dispatched):

tick: 1 
tick: 2 
tick: 3 
tick: 4 
tick: 5 
finished

Now change 'repeat' to 0 then run the movie. This time the CustomTimer has no repeat limit and will simply go on indefinitely until the movie is stopped. It's the same as not supplying a second parameter to the CustomTimer class at instantiation. Go ahead and stop the movie.

The CustomTimer class is identical to the native 'flash.utils.Timer' class, but adds the pause and resume functionality.


Step 10: Testing Pause and Resume

In the Timeline window, name the layer we used for scripting 'actions'. Create a new layer below the actions layer inside the Timeline and name it 'text'. Click Frame 1 of the 'text' layer and add a dynamic TextField in the middle of the stage and name it 'textField'. Make sure to use Inconsolata as the font and embed only the numerical glyphs.

Set the font size to 50 and the color to 0x000000; also, the stage color should be 0xFFFFFF.

Convert it to a movieClip (F8) and name it 'textMovie'. Name the instance on the stage 'text_mc'.

In the Timeline, click the actions layer and click Frame 1, then hit (F9) to open the script window. Replace all the code inside the script window with the following lines of code:

var delay:Number = 1000; 
var repeat:Number = 10; 
 
var clicked:Boolean = false;//used for toggling every time the stage is clicked 
var inited:Boolean = false;//triggered only once, when the timer is started 
var completed:Boolean = false;//triggered only once, when the timer has finished (if the timer was given a max number of repeat times) 
 
text_mc.textField.text = repeat;//assign this text to the textField on stage 
var customTimer:CustomTimer = new CustomTimer (delay, repeat);//instantiate the timer 
 
//add listeners 
customTimer.addEventListener (CustomTimer.TIMER_STARTED, onStart); 
customTimer.addEventListener (CustomTimer.TIMER_PAUSED, onPause); 
customTimer.addEventListener (CustomTimer.TIMER_RESUMED, onResume); 
customTimer.addEventListener (CustomTimer.TIMER_FINISHED, onFinish); 
customTimer.addEventListener (CustomTimer.TICK, onTick); 
 
//add a click listener on the stage 
stage.addEventListener( MouseEvent.CLICK, onStageClick); 
 
//response for every stage click 
function onStageClick (e:MouseEvent):void 
{ 
   clicked = ! clicked;//toggles true or false 
   if (clicked) 
   { 
   	if (! inited)//when the timer is started 
   	{ 
  			customTimer.start (); 
   		inited = true; 
   	} 
   	else if (! completed)//after the inited has changed to true 
   	{ 
   		customTimer.resume (); 
   	} 
   } 
   else  if (! completed) //if the timer has not completed 
   { 
   	customTimer.pause (); 
   } 
} 
 
//when start is is dispatched from the timer 
function onStart (e:Event):void 
{ 
   trace ('started'); 
} 
 
//every time pause is dispatched from the timer 
function onPause (e:Event):void 
{ 
   trace ('paused'); 
} 
 
//every time resume is dispatched from the timer 
function onResume (e:Event):void 
{ 
   trace ('resumed'); 
} 
 
//every time tick is dispatched from the timer 
function onTick (e:Event):void 
{ 
   trace ('tick'); 
   text_mc.textField.text = repeat - e.target.currentCount; 
} 
 
//dispatched when the timer has finished (if we assigned a repeatCount greater than 0) 
function onFinish (e:Event):void 
{ 
   completed = true; 
   trace ('finished'); 
}

Save the file, run the movie and click on the stage.

On your first click, it starts the countdown. The second click pauses the timer and, as you'll notice, when you click the stage the 3rd time, the timer resumes. After that, the pause and resume just cycles until the timer ends. We also trace the current status of the timer when it starts, when the stage is clicked, and when the timer finishes.

Play with it for a moment. Try changing delay and repeat.

Your result should be similar to the flash movie below with additional information printing on the output window:


Step 11: A Simple CountDown Example

Now that you're familiar with the CustomTimer class, let's create something a little more visual. Go ahead and drag a copy of countDown.fla from the source download into the 'AnalogTimer' folder.

Double-Click the countDown.fla file inside the 'AnalogTimer' folder to open it.

Open up the Timeline and the Stage windows.

In the Timeline window, you will see three layers - the button layer, the clock layer, then the text layer. Inside the layer named 'button' is an instance of the Button component with an instance name of 'button_mc'. The clock layer holds two movieclips - an instance of seconds movieclip named 'seconds_mc' and an instance of border movieclip named 'clockFace_mc'. The text layer has a movieclip named 'text_mc' which contains a TextField named 'textField'.

Note: If you are using Flash CS3, be sure to uncheck the box for Automatically Declare Stage Instances in the Publish Settings under Flash, under ActionScript Settings.


Step 12: The CountDown.as Document Class

In the 'AnalogTimer' folder where you saved the countDown.fla, create a new class and name it CountDown.as and add the following lines of code:

package 
{//package brackets 
	 
	public class CountDown extends Sprite 
	{//class brackets 
		 
 
		public function CountDown() 
		{ 
			 
		} 
 
   }//class brackets 
}//package brackets

Step 13: Add The Class Imports

Right inside the package brackets, before the class declaration, insert the following lines of code:

 
import fl.controls.Button; 
import flash.display.MovieClip; 
import flash.display.Sprite; 
import flash.events.Event; 
import flash.events.MouseEvent; 
import flash.filters.DropShadowFilter; 
import flash.utils.getTimer;

Step 14: Add The Public and Private Variables

Add the following variables inside the Class brackets. The 4 public variables are references to the movieclips that are currently on the stage (see Step 11). The private variables are created when the class is instantiated. This is the same as what we had in test.fla. This time, we converted it into a class. You should be familiar with these variables from Step 10.

 
public var button_mc:Button; 
public var text_mc:MovieClip; 
public var clockFace_mc:MovieClip; 
public var seconds_mc:MovieClip; 
 
private var _startTime:Number = 0; 
private var _delay:Number = 10; 
private var _repeat:Number = 1000; 
private var _customTimer:CustomTimer; 
private var _clicked:Boolean = false; 
private var _inited:Boolean = false; 
private var _completed:Boolean = false;

Step 15: The Constructor Method

This is where we put everything we need done when the Class is instantiated. Insert the following lines of code inside the CountDown Constructor Method brackets:

 
text_mc.mouseChildren = false;//we just make sure the text could not be selected or clicked. 
text_mc.textField.text = _repeat;//we show the value of _repeat as the text of the textField. 
seconds_mc.filters = [new DropShadowFilter];//dropshadow is applied to the seconds_mc. 
_customTimer = new CustomTimer (_delay, _repeat);//we create a new custom timer. 
 
_customTimer.addEventListener (CustomTimer.TIMER_STARTED, onStart); 
_customTimer.addEventListener (CustomTimer.TIMER_PAUSED, onPause); 
_customTimer.addEventListener (CustomTimer.TICK, onTick); 
_customTimer.addEventListener (CustomTimer.TIMER_FINISHED, onFinish); 
 
button_mc.addEventListener( MouseEvent.CLICK, onButtonClick);//add a click listener to the button.

Step 16: Event Listeners

We just add the functions that are called when the events we are listening to are dispatched. Add the following lines right below the constructor method.

 
private function onButtonClick (e:MouseEvent):void 
{ 
	_clicked = ! _clicked 
	if (_clicked) 
	{ 
		if (! _inited) 
		{ 
			_customTimer.start (); 
			_inited = true; 
		} 
		else if (! _completed) 
		{ 
			_customTimer.resume (); 
		} 
		button_mc.label = 'PAUSE'; 
	} 
	else  if (! _completed) 
	{ 
		_customTimer.pause (); 
		button_mc.label = 'RESUME'; 
	} 
} 
private function onStart (e:Event):void {_startTime = getTimer ();} 
private function onPause (e:Event):void {trace (_customTimer.currentCount);} 
private function onTick (e:Event):void 
{ 
	text_mc.textField.text = _repeat - _customTimer.currentCount;//we assign the timer's currentCount value to the textField 
 
	seconds_mc.rotation = 360 - 360 / _repeat * _customTimer.currentCount;//this makes the second_mc rotate from 360 down to 0 
	var filter:Array = seconds_mc.filters; 
	filter = [new DropShadowFilter]; 
	seconds_mc.filters = filter;//this gives a nice realistic dropshadow effect 
} 
private function onFinish (e:Event):void 
{ 
	_completed = true; 
}

Step 17: The Complete Class

That completes our CountDown.as class, go ahead and save before moving on. Also, make sure to double-check your code for any errors. The CountDown.as Class should now look exactly like this:

 
package 
{ 
	import fl.controls.Button; 
	import flash.display.MovieClip; 
	import flash.display.Sprite; 
	import flash.events.Event; 
	import flash.events.MouseEvent; 
	import flash.filters.DropShadowFilter; 
	import flash.utils.getTimer; 
 
	public class CountDown extends Sprite 
	{ 
		public var button_mc:Button; 
		public var text_mc:MovieClip; 
		public var clockFace_mc:MovieClip; 
		public var seconds_mc:MovieClip; 
 
		private var _startTime:Number = 0; 
		private var _delay:Number = 10; 
		private var _repeat:Number = 1000; 
		private var _customTimer:CustomTimer; 
		private var _clicked:Boolean = false; 
		private var _inited:Boolean = false; 
		private var _completed:Boolean = false; 
 
		public function CountDown() 
		{ 
			text_mc.mouseChildren = false; 
			text_mc.textField.text = _repeat; 
 
			seconds_mc.filters = [new DropShadowFilter]; 
 
			_customTimer = new CustomTimer (_delay, _repeat); 
 
			_customTimer.addEventListener (CustomTimer.TIMER_STARTED, onStart); 
			_customTimer.addEventListener (CustomTimer.TIMER_PAUSED, onPause); 
			_customTimer.addEventListener (CustomTimer.TICK, onTick); 
			_customTimer.addEventListener (CustomTimer.TIMER_FINISHED, onFinish); 
 
			button_mc.addEventListener( MouseEvent.CLICK, onButtonClick); 
		} 
 
		private function onButtonClick (e:MouseEvent):void 
		{ 
			_clicked = ! _clicked; 
			if (_clicked) 
			{ 
  				if (! _inited) 
  				{ 
  					_customTimer.start (); 
  					_inited = true; 
  				} 
  				else if (! _completed) 
  				{ 
  					_customTimer.resume (); 
 				} 
 				button_mc.label = 'PAUSE'; 
 			} 
  			else  if (! _completed) 
  			{ 
  				_customTimer.pause (); 
  				button_mc.label = 'RESUME'; 
  			} 
  		} 
		private function onStart (e:Event):void {_startTime = getTimer ();} 
		private function onPause (e:Event):void {trace (_customTimer.currentCount);} 
		private function onTick (e:Event):void 
  		{ 
  			text_mc.textField.text = _repeat - _customTimer.currentCount; 
 
  			seconds_mc.rotation = 360 - 360 / _repeat * _customTimer.currentCount; 
  			var filter:Array = seconds_mc.filters; 
  			filter = [new DropShadowFilter] 
  			seconds_mc.filters = filter; 
  		} 
		private function onFinish (e:Event):void 
  		{ 
  			_completed = true; 
  		} 
   } 
}

Step 18: Assigning The Document Class

In Flash, open up the properties panel and assign 'CountDown' as the document class.

Hit CTRL + ENTER to test the movie. If everything went well and the code in the document class has no errors, you should see something like this:

Also, the output panel prints out the current value of the CustomTimer.currentCount.

Again, you can try changing the _delay and _repeat properties inside the CountDown class to really get the feel of what's going on.


Step 19: Setting Up for the Real Project

We will now create the AIR utility you saw at the beginning of this tutorial. Make sure to close all previous work you've done and start fresh.

We need to copy three files from the source download into the 'AnalogTimer' folder. They are the 'analogTimer.fla', The 'com' folder, and the 'MgOpenModernaBold.zip'.

Drag a copy of this 'analogTimer.fla' file and save it in the 'AnalogTimer' folder.

Open the 'MgOpenModernaBold.zip' and extract the 'MgOpenModernaBold.ttf' font into the 'AnalogTimer' folder. It should sit together with the analogTimer.fla. You don't need to install this font into your OS since we will assign this via Actionscript later.

The 'com' folder. This is the Greensock Tweening engine that I've included for this tutorial. Copy this 'com' folder into the 'AnalogTimer' folder. For more information about the Greensock Tweening engine, take a look at www.greensock.com. Open the analogTimer.fla file that you saved in the 'AnalogTimer' folder.

Inside the Timeline, you will see nine layers. It's important to get familiar with the movieclips associated with these layers because we will be working on them through the Document class that we will create later.

  1. The first is the 'labels' layer, this layer is where we have 4 static texts "H", "M","S", & ".". They represent hours, minutes, seconds, and 1/10th seconds which describe the digits to their immediate right.
  2. The second layer named 'container' only has an empty movieclip named 'analogContainer'. This movieclip will house the odometer engine which will be instantiated from the Odometer class which we will also create later.
  3. The third layer is the 'mask' layer. This layer contains an instance of mask named 'digitMask'. We don't have to convert this into a mask inside the flash file and worry about it's order of position since we will assign it as a mask via actionscript later on. To see the 'digitMask' movieclip, you can hide the container layer above it.
  4. The fourth layer named 'start' houses an instance of playButton also named 'playButton'.
  5. The fifth layer named 'pause' and has a movieclip instance of the pauseButton also named 'pauseButton'. This movieclip sits exactly below 'playButton'. To see it, hide the layer for 'playButton'.
  6. The sixth layer is named 'reset', it only contains a movieclip instance of the resetButton named 'resetButton'.
  7. The seventh layer named 'minimize' has a movieclip instance of minimizeButton named 'minimizeButton'.
  8. The eighth layer is named 'close' and has a movieclip instance of closeButton named 'closeButton'.
  9. The last layer named 'bg' has an instance of backGround named 'background'.

The analogTimer.fla should be set to 40 fps with the dimensions of 243 x 22 pixels.

Again, if you are using Flash CS3, make sure to uncheck the 'Automatically declare stage instances' from Publish Settings>Flash>ActionScript Settings.

Still within the Flash tab, click the player dropdown menu, choose Adobe AIR 1.5, and click 'OK'.

Press Settings from the Player option; for the window style, choose Custom Chrome (transparent).

Save the changes.


Step 20: The Time Enumeration Class

This project will have a total of 5 classes including the CustomTimer class. Let's start with the smallest to keep things clear.

Create a new Class and name it Time. This is a very simple class, all it does is hold public static constants that represent values of time. Now whenever I need a value of time, e.g. 1 hour (3600000 milliseconds), all I have to do is access Time.ONE_HOUR which is a lot more readable than its numerical value. This Class will be used in conjuction with 2 major classes later.

Add the following lines of code:

package 
{ 
   public class Time 
   { 
	   public static const ONE_HUNDRETH_SECOND:uint =	10; 
	   public static const ONE_TENTH_SECOND:uint 	=	100; 
	   public static const ONE_SECOND:uint 		 	=	1000; 
	   public static const TEN_SECONDS:uint 		=	ONE_SECOND * 10;//10000 
	   public static const ONE_MINUTE:uint 		 	=	TEN_SECONDS * 6;//60000 
	   public static const TEN_MINUTES:uint 		=	ONE_MINUTE * 10;//600000 
	   public static const ONE_HOUR:uint 			=	TEN_MINUTES * 6;//3600000 
	   public static const TEN_HOURS:uint 			=	ONE_HOUR * 10;//36000000 
   } 
}

Note: all the classes that we will create should be saved into the same folder ('AnalogTimer').


Step 21: Testing The Time Class

Open up a new Flash file, name it timeTest.fla, and save it inside the AnalogTimer folder. It should be alongside the Time.as class.

Inside the script window, type in:

var t:Number = Time.ONE_MINUTE; 
 trace (t); // you should get a result of 60000

If the result is correct, go ahead and close the file.


Step 22: The Digit Class

Create another class and call it Digit.as. This class is also fairly simple. It takes two required parameters, the first is $index which has an unsigned integer value and second, $textFormat which is assiged a textFormat instance and creates the numbers you see that are animated when the timer is started. Also, the numbers are positioned in the center of each instance.

Add the following lines of code:

package 
{ 
   import flash.display.Graphics; 
   import flash.display.Sprite; 
   import flash.text.TextField; 
   import flash.text.TextFieldAutoSize; 
   import flash.text.TextFormat; 
 
   public class Digit extends Sprite 
   { 
		public function Digit ($index:uint, $format:TextFormat) 
		{ 
			var textField:TextField = new TextField; 
			textField.autoSize = TextFieldAutoSize.CENTER; 
			textField.defaultTextFormat = $format; 
			textField.embedFonts = true; 
			textField.text = String ($index); 
			textField.selectable = false; 
			textField.x = textField.width * .5; 
			addChild (textField); 
		} 
	} 
}

Step 23: Testing The Digit Class

Open up a new Flash file and name it digitTest.fla and save it inside the AnalogTimer folder. It should be alongside the Digit.as class.

Right-Click the library tab and select New Font. Choose the installed 'Inconsolata' font and name it 'Inconsolata'. Check the box for Export for Actionscript and click 'OK' twice.

Inside the script window, type in:

var format:TextFormat = new TextFormat ('Inconsolata', 50, 0xFF0000, true); 
var d:Digit = new Digit (4, format); 
d.x = 100; 
d.y = 100; 
addChild (d);

You should see a red '4' on the stage. When you're done, go ahead and close the file.


Step 24: Odometer Class Imports and Variables

This is the main animation engine. You should visualize this class as one independent mechanical section for the AnalogTimer application. When you look at the final preview, you will see seven Odometers.

Create a new class and name it Odometer.as.

Add the following code:

package 
{ 
	import com.greensock.easing.Linear; 
	import com.greensock.TweenLite; 
	import flash.display.Sprite; 
	import flash.events.Event; 
	import flash.text.TextFormat; 
 
	public class Odometer extends Sprite 
	{ 
		private var _format:TextFormat; 
		private var _digits:Array; 
		private var _index:uint; 
		private var _startY:Number = -30; 
		private var _centerY:Number = -10; 
		private var _endY:Number = 10; 
		private var _limit:uint; 
		private var _speed:Number; 
		private var _customTimer:CustomTimer; 
		private var _currentTarget:Sprite; 
		private var _previousTarget:Sprite; 
		private var _rollSpeed:Number; 
 
	} 
}

Inside the class brackets are 12 private variables.

The _format variable holds a TextFormat instance that will be passed to the Digit class. When you saw the preview, did you notice the 1/10th second's color is red? That's what we need this for.

The _digits variable is an array that will hold all the Digit classes that we will instantiate later.

_index is an unsigned integer that we increment by 1 every time the CustomTimer dispatches a 'TICK' event. It points into the Digit instance in the_digits array which becomes the next target for animation.

_startX, _centerX, and _endX are assigned positions for the Digits. _startX is where the numbers animate coming from the top. _centerX is where they pause showing the current value of the time it represents, and _endY is where they animate to coming from _centerX.

The _limit variable, an unsigned integer, gets either a value of 10 or 6, depending on what speed of CustomTimer we assign for this Odometer instance. I'll explain more later when we get to its assignment.

_speed is a Number variable that is assigned the delay value of the CustomTimer that is passed in as a parameter.

_customTimer is assigned the CustomTimer we pass in from the constructor. It triggers the animation for the Odometer.

_currentTarget and _previousTarget are Sprite variables that are assigned with Digit classes. They are used when pause and resume are called.

The _rollSpeed variable of type Number, is assigned the proper speed the Digit class is animated from _startX to _centerX, to _endX.


Step 25: Odometer Class Constructor Method

As with all constructors' methods, we only put everything that needs to be prepared at instantiation inside of it. Add the following lines of code right below the _rollSpeed variable:

public function Odometer ($customTimer:CustomTimer) 
{ 
	_customTimer = $customTimer; 
	_speed = _customTimer.delay; 
 
	setRollSpeed (); 
	setLimit (); 
 
	addEventListener (Event.ADDED_TO_STAGE, init); 
	addEventListener (Event.REMOVED_FROM_STAGE, removeRef); 
}

As soon as an instance is created, _customTimer is assigned a reference to the CustomTimer passed as a parameter. It then assigns the value of _customTimer's delay to _speed. The setRollSpeed() and setLimit() functions are then called and finally, event listeners are added for Event.ADDED_TO_STAGE and Event.REMOVED_FROM_STAGE.


Step 26: Odometer Class The setRollSpeed Method

This method checks to see if the _speed variable is less than or equal to Time.ONE_TENTH_SECOND (100 milliseconds). If it is, _rollSpeed is set to Time.ONE_TENTH_SECOND. If _speed is anything but less than or equal to Time.ONE_TENTH_SECOND, _rollSpeed is set to Time.ONE_SECOND / 2 (500 milliseconds). It then converts _rollSpeed to a fractional value required for TweenLite. Ex. Time.ONE_TENTH_SECOND is 100. We need to divide it with Time.ONE_SECOND to convert it to '0.1'.

Add the following lines of code right after the constructor method:

 
private function setRollSpeed():void 
{ 
	if (_speed <= Time.ONE_TENTH_SECOND) 
	{ 
		_rollSpeed = Time.ONE_TENTH_SECOND; 
	} 
	else 
	{ 
		_rollSpeed = Time.ONE_SECOND / 2; 
	} 
 
	_rollSpeed /= Time.ONE_SECOND;//convert to values compatible with TweenLite 
}

Step 27: Odometer Class The setLimit Method

It's a simple method, all it does is set _limit according to the value of _speed. A good example is Time.TEN_MINUTES, it represents the "tens" column in the minutes, and as you know, these only go from 0-5. You'll see how we use this when we get to the startTweens() method.

Add the following lines of code below the setRollSpeed method:

 
private function setLimit():void 
{ 
	switch (_speed) 
	{ 
		case Time.ONE_HUNDRETH_SECOND: 
		case Time.ONE_TENTH_SECOND: 
		case Time.ONE_SECOND: 
		case Time.ONE_MINUTE: 
		case Time.ONE_HOUR: 
		case Time.TEN_HOURS: _limit = 10; 
		break; 
 
		case Time.TEN_SECONDS: 
		case Time.TEN_MINUTES: _limit = 6; 
		break; 
	} 
}

Step 28: Odometer Class The init Method

This method is called when the instance gets added to the stage. First it removes the Event.ADDED_TO_STAGE listener and then adds listeners for the _customTimer's events - TICK, TIMER_PAUSED, and TIMER_RESUMED. Then it calls the createOdometer() method.

Add the following lines of code:

 
private function init(e:Event):void 
{ 
	removeEventListener (Event.ADDED_TO_STAGE, init); 
 
	_customTimer.addEventListener (CustomTimer.TICK, roll); 
	_customTimer.addEventListener (CustomTimer.TIMER_RESUMED, onResume); 
	_customTimer.addEventListener (CustomTimer.TIMER_PAUSED, onPause); 
 
	createOdometer (); 
}

Step 29: Odometer Class The createOdometer Method

Within this method, _digits is assigned a new Array instance. _format is then assigned a TextFormat. If _speed is equal to Time.ONE_TENTH_SECOND, _textFormat is assigned RED as the color and BLACK if not. We then loop to the value of _limit and call createDigits() passing the current iteration as the parameter.

Add the following lines of code:

 
private function createOdometer():void 
{ 
	_digits = []; 
 
	if (_speed == Time.ONE_TENTH_SECOND) _format = new TextFormat ('Inconsolata', 17, 0xFF0000, true); 
	else _format = new TextFormat ('Inconsolata', 17, 0, true);//0xFF8000 
 
	for (var i:uint = 0; i < _limit; i++) createDigits (i); 
}

Step 30: Odometer Class The createDigits Method

As you already know, every time this method is called, it comes with a different $index value ranging from 0 to the value of _limit (either 6 or 10, refer to step 24). It checks to see if $index is equal to 0; if it is, digit is positioned on _centerY. If $index isn't equal to 0, digit is positioned on _startY. digit is then added inside Odometer and pushed into the _digits array for reference later.

Add the following lines of code:

 
private function createDigits ($index:uint):void 
{ 
	var digit:Digit = new Digit ($index, _format); 
	if ($index == 0) digit.y = _centerY; 
	else digit.y = _startY; 
 
	addChild (digit); 
	_digits.push (digit); 
}

Step 31: Odometer Class The roll Method

As you may recall, this is the method called everytime a TICK event is dispatched by _customTimer. _index is incremented by 1 and then checked if the current value is greater than or equal to the length of _digits. If it is, _index is assigned 0 again. A local variable named target of type Digit is then assigned the Digit that resides within the current _index of the _digits array. When the class is instantiated, _index, of type uint is assigned no value which defaults to 0. This means that the first animation you will see when the application starts is 0 moving out from _centerY to _endY and 1 moving in from startY to _centerY. The target is then positioned on _startY so everytime target is animated, it starts from the right position. After all the setup, startTweens () is called passing target as the parameter.

Add the following lines of code:

 
private function roll (e:Event):void 
{ 
	_index++; 
	if (_index >= _digits.length) _index = 0; 
	var target:Digit = _digits[_index]; 
	target.y = _startY; 
 
	startTweens (target); 
}

Step 32: Odometer Class The startTweens Method

Everytime this method is called, a local variable named previous is created and _currentTarget is assigned the $target parameter. It then loops through all the Digit instances stored inside _digits. Once the $target has matched the current Digit in _digits, _limit is tested to see if its value is 10 or not. If _limit is 10 and the current iteration (i) is equal to 0, previous is assigned 9, if the current iteration isn't equal to 0, previous is assigned one less than i.

On the other hand, if _limit is not equal to 10 (meaning it's equal to 6), if the current iteration is equal to 0, previous is assigned 5, if not, previous is assigned i - 1. _previousTarget is then assigned _digits[previous].

We need _currentTarget & _previousTarget assigned here so that any time the application is paused, if we ever need to resume, we know which Digits were animating.

Note also that once _previousTarget has been located in _digits, we break out of the loop to save resources.

TweenLite is then instantiated to animate both _currentTarget & _previousTarget.

Add the following code:

 
private function startTweens ($target:Digit):void 
{ 
	var previous:uint; 
	_currentTarget = $target; 
 
	for (var i:uint = 0; i < _digits.length; i++) 
	{ 
		if (_digits[i] == $target) 
		{ 
			if (_limit == 10) previous = i == 0 ? 9 : i - 1; 
			else previous = i == 0 ? 5 : i - 1; 
			_previousTarget = _digits[previous]; 
			break; 
		} 
	} 
 
	TweenLite.to 
	( 
	_currentTarget, //digit to animate 
	_rollSpeed, //dutation 
	{ y:_centerY, ease: Linear.easeNone } //will move the current target from top to center position 
	); 
	TweenLite.to 
	( 
	_previousTarget, //digit to animate 
	_rollSpeed, //duration 
	{ y:_endY, ease: Linear.easeNone } ); //will move the previous target from center to bottom position 
}

Step 33: Odometer Class The onPause & onResume Methods

The onPause() method simply stops all the tweens of all the Digits that are currently animating by looping through all the Digits in _digits and calling the TweenLite.killTweensOf() method, assigning digit as the parameter.

The onResume() method checks three conditions - whether _currentTarget has been assigned a value, whether _currentTarget's y position is less than _centerY, and whether _currentTarget's y position is greater than or equal to _startY. If those conditions are all true, TweenLite is then called to animate _currentTarget from its current position to _centerY.

_previousTarget is then also checked for 3 conditions, if it has been assigned, if its y position is less than _endY and greater than _startY. If all those conditions are met, _previousTarget is animated from its current position to _endY.

Add the following lines of code:

 
private function onPause (e:Event):void 
{ 
	for each (var digit:Digit in _digits) TweenLite.killTweensOf (digit); 
} 
 
private function onResume (e:Event):void 
{ 
	if (_currentTarget && _currentTarget.y < _centerY && _currentTarget.y >= _startY) 
	TweenLite.to (_currentTarget, _rollSpeed, { y:_centerY, ease: Linear.easeNone } ); 
	if (_previousTarget && _previousTarget.y < _endY && _previousTarget.y > _startY) 
	TweenLite.to (_previousTarget, _rollSpeed, { y:_endY, ease: Linear.easeNone } ); 
}

Step 34: Odometer Class The removeRef Method

This method is called whenever the class is removed from the stage. It pretty much takes care of clean up. Whenever the app is reset, it disposes of the old Digits and CustomTimers and starts anew.

Add the following lines of code:

 
private function removeRef (e:Event):void 
{ 
	removeEventListener (Event.REMOVED_FROM_STAGE, removeRef);//remove own listener 
 
	//remove all listeners for the _customTimer 
	_customTimer.removeEventListener (CustomTimer.TICK, roll); 
	_customTimer.removeEventListener (CustomTimer.TIMER_RESUMED, onResume); 
	_customTimer.removeEventListener (CustomTimer.TIMER_PAUSED, onPause); 
 
	_customTimer.quit ();//refer back to step 6 
	_customTimer = null;//release the CustomTimer object for garbage collection 
}

The Complete Odometer Class should now look exactly like below:

package 
{ 
	import com.greensock.easing.Linear; 
	import com.greensock.TweenLite; 
	import flash.display.Sprite; 
	import flash.events.Event; 
	import flash.text.TextFormat; 
 
	public class Odometer extends Sprite 
	{ 
		private var _format:TextFormat; 
		private var _digits:Array; 
		private var _index:uint = 0; 
		private var _startY:Number = -30; 
		private var _centerY:Number = -10; 
		private var _endY:Number = 10; 
		private var _limit:uint; 
		private var _speed:Number; 
		private var _customTimer:CustomTimer; 
		private var _currentTarget:Sprite; 
		private var _previousTarget:Sprite; 
		private var _rollSpeed:Number; 
 
		public function Odometer ($customTimer:CustomTimer) 
		{ 
			_customTimer = $customTimer; 
			_speed = _customTimer.delay; 
 
			setRollSpeed (); 
			setLimit (); 
 
			addEventListener (Event.ADDED_TO_STAGE, init); 
			addEventListener (Event.REMOVED_FROM_STAGE, removeRef); 
		} 
 
		private function setRollSpeed():void 
		{ 
			if (_speed <= Time.ONE_TENTH_SECOND) 
			{ 
				_rollSpeed = Time.ONE_TENTH_SECOND; 
			} 
			else 
			{ 
				_rollSpeed = Time.ONE_SECOND / 2; 
			} 
 
			_rollSpeed /= Time.ONE_SECOND; 
		} 
 
		private function setLimit():void 
		{ 
			switch (_speed) 
			{ 
				case Time.ONE_HUNDRETH_SECOND: 
				case Time.ONE_TENTH_SECOND: 
				case Time.ONE_SECOND: 
				case Time.ONE_MINUTE: 
				case Time.ONE_HOUR: 
				case Time.TEN_HOURS: _limit = 10; 
				break; 
 
				case Time.TEN_SECONDS: 
				case Time.TEN_MINUTES: _limit = 6; 
				break; 
			} 
		} 
 
		private function init(e:Event):void 
		{ 
			removeEventListener (Event.ADDED_TO_STAGE, init); 
 
			_customTimer.addEventListener (CustomTimer.TICK, roll); 
			_customTimer.addEventListener (CustomTimer.TIMER_RESUMED, onResume); 
			_customTimer.addEventListener (CustomTimer.TIMER_PAUSED, onPause); 
 
			createOdometer (); 
		} 
 
		private function createOdometer():void 
		{ 
			_digits = []; 
 
			if (_speed == Time.ONE_TENTH_SECOND) _format = new TextFormat ('Inconsolata', 17, 0xFF0000, true); 
			else _format = new TextFormat ('Inconsolata', 17, 0, true); 
 
			for (var i:uint = 0; i < _limit; i++) createDigits (i); 
		} 
		private function createDigits ($index:uint):void 
		{ 
			var digit:Digit = new Digit ($index, _format); 
			if ($index == 0) digit.y = _centerY; 
			else digit.y = _startY; 
 
			addChild (digit); 
			_digits.push (digit); 
		} 
 
		private function roll (e:Event):void 
		{ 
			_index++; 
			if (_index >= _digits.length) _index = 0; 
			var target:Digit = _digits[_index]; 
			target.y = _startY; 
 
			startTweens (target); 
		} 
 
		private function startTweens ($target:Digit):void 
		{ 
			var previous:uint; 
			_currentTarget = $target; 
 
			for (var i:uint = 0; i < _digits.length; i++) 
			{ 
				if (_digits[i] == $target) 
				{ 
					if (_limit == 10) previous = i == 0 ? 9 : i - 1; 
					else previous = i == 0 ? 5 : i - 1; 
					_previousTarget = _digits[previous]; 
				} 
			} 
 
			TweenLite.to 
			( 
			_currentTarget, 
			_rollSpeed, 
			{ y:_centerY, ease: Linear.easeNone } 
			); 
			TweenLite.to 
			( 
			_previousTarget, 
			_rollSpeed, 
			{ y:_endY, ease: Linear.easeNone } ); 
		} 
 
		private function onPause (e:Event):void 
		{ 
			for each (var digit:Digit in _digits) TweenLite.killTweensOf (digit); 
		} 
 
		private function onResume (e:Event):void 
		{ 
			if (_currentTarget && _currentTarget.y < _centerY && _currentTarget.y >= _startY) 
			TweenLite.to (_currentTarget, _rollSpeed, { y:_centerY, ease: Linear.easeNone } ); 
			if (_previousTarget && _previousTarget.y < _endY && _previousTarget.y > _startY) 
			TweenLite.to (_previousTarget, _rollSpeed, 
			{ y:_endY, ease: Linear.easeNone } ); 
		} 
 
		private function removeRef (e:Event):void 
		{ 
			removeEventListener (Event.REMOVED_FROM_STAGE, removeRef); 
 
			_customTimer.removeEventListener (CustomTimer.TICK, roll); 
			_customTimer.removeEventListener (CustomTimer.TIMER_RESUMED, onResume); 
			_customTimer.removeEventListener (CustomTimer.TIMER_PAUSED, onPause); 
 
			_customTimer.quit (); 
			_customTimer = null; 
		} 
 
	} 
}

Step 35: Odometer Class Testing

Open up a new Flash file and name it odometerTest.fla and save it inside the AnalogTimer folder. It should be along with the Odometer.as class.

Add an Inconsolata font exactly as you did in Step 23.

Inside the script window, type:

 
var c:CustomTimer = new CustomTimer (Time.ONE_SECOND); 
var o:Odometer = new Odometer (c); 
 
o.x = o.y = 100; 
addChild (o); 
 
c.start ();

You should see seconds rolling on the stage. When you're done, go ahead and close the file.

Inside the Odometer Class, on createOdometer() method, change the Font assigned to the TextFormat from 'Inconsolata' to 'MGOpen'.

The createOdometer() method should now look like this:

private function createOdometer():void 
{ 
	_digits = []; 
 
	if (_speed == Time.ONE_TENTH_SECOND) _format = new TextFormat ('MgOpen', 17, 0xFF0000, true); 
	else _format = new TextFormat ('MgOpen', 17, 0, true); 
 
	for (var i:uint = 0; i < _limit; i++) createDigits (i); 
}

Save the class before moving on.


Step 36: AnalogTimer Class Imports and Variables

The final class, this will also be the document class for our analogTimer.fla.

The first 8 public variables are references to the stage assets (see Step 19). Right below those is an [embed] metatag for the font I've included, with the source download assigned to a private variable named MgOpen of type Class. This font was extracted to the 'AnalogTimer' folder earlier during set up.

The _timerArray variable is an array the class will use to reference all the CustomTimer instances.

The _adjustment variable holds an unsigned integer value of 8 that we will use to position each Odometer instance on the stage.

Create a new class named AnalogTimer.as and add the following lines of code:

package 
{ 
import com.greensock.TweenLite; 
   import flash.display.MovieClip; 
   import flash.display.Sprite; 
   import flash.events.Event; 
   import flash.events.MouseEvent; 
   import flash.text.Font; 
 
   public class AnalogTimer extends Sprite 
   { 
   	public var playButton:MovieClip; 
   	public var resetButton:MovieClip; 
   	public var pauseButton:MovieClip; 
   	public var minimizeButton:MovieClip; 
   	public var closeButton:MovieClip; 
 
   	public var analogContainer:MovieClip; 
   	public var digitMask:MovieClip; 
   	public var background:MovieClip; 
 
   	[Embed(source="MgOpenModernaBold.ttf", //need to move this to the same folder as the fla 
   	fontName = "MgOpen", 
   	fontWeight = 'bold', 
   	advancedAntiAliasing="true", 
   	mimeType = "application/x-font")] private var MgOpen:Class; 
	private var _timerArray:Array; 
   	private var _adjustment:uint = 8; 
	} 
}

Step 37: AnalogTimer Class buildApp Method

This method is called as soon as the AnalogTimer class is instantiated. It accurately describes its pupose.

The playButton is added to the stage to ensure that it is on top of the pauseButton. Event listeners are then added to the background, playButton, minimizeButton, & closeButton. The alpha property of _pauseButton is set to 0 to make it invisible, and _playButton's alpha is set to 1.

A new array is then assigned to _timerArray.

The analogContainer is then checked to see if it has more than one child. Remember, we have one graphic object as its child: the gradient rectangle which we use as background for the digits. If analogContainer's children is more than 1, we loop backwards and remove all of them except for the gradient rectangle object. This happens when reset is called.

We then create a local array variable named sequence. Inside this are smaller arrays with Time values and references to the children of digitMask. We loop through sequence array and for every iteration, we create a CustomTimer instance with the value of the current iteration in the sequence array in position 0: sequence[current_iteration][0]. We then create an instance of Odometer and assign the CustomTimer isntance as its parameter. The Odometer instance is then added as a child to the analogContainer movieclip on the stage and positioned based on the current iteration in sequence in position 1, then _adjustment is applied. The digitMask is then set as the mask for analogContainer.

Add the following lines of code right below the last private variable:

public function AnalogTimer() 
{ 
	buildApp (); 
} 
 
private function buildApp():void 
{ 
	addChild (playButton); 
	background.addEventListener (MouseEvent.MOUSE_DOWN, appPress); 
	background.addEventListener (MouseEvent.MOUSE_UP, appRelease); 
	playButton.addEventListener (MouseEvent.CLICK, startTimer); 
	minimizeButton.addEventListener (MouseEvent.CLICK, minimizeTimer); 
	closeButton.addEventListener (MouseEvent.CLICK, closeTimer); 
 
	pauseButton.alpha = 0; 
	playButton.alpha = 1; 
 
	_timerArray = new Array; 
 
	if (analogContainer.numChildren > 1) 
	{ 
		for (var h:uint = analogContainer.numChildren; h > 1; h--) analogContainer.removeChildAt (h - 1); 
	} 
 
	var sequence:Array = 
	[ 
	[Time.ONE_TENTH_SECOND, digitMask.oneTenthSecond], 
	[Time.ONE_SECOND, digitMask.oneSecond], 
	[Time.TEN_SECONDS, digitMask.tenSeconds], 
	[Time.ONE_MINUTE, digitMask.oneMinute], 
	[Time.TEN_MINUTES, digitMask.tenMinutes], 
	[Time.ONE_HOUR, digitMask.oneHour], 
	[Time.TEN_HOURS, digitMask.tenHours] 
	]; 
 
	for (var i:uint = 0; i < sequence.length; i++) 
	{ 
		var ct:CustomTimer = new CustomTimer (sequence[i][0]); 
		_timerArray.push (ct); 
 
		var odometer:Odometer = new Odometer (ct); 
		analogContainer.addChild (odometer); 
 
		odometer.x = sequence[i][1].x - _adjustment; 
		odometer.y = sequence[i][1].y + _adjustment; 
	} 
	analogContainer.mask = digitMask; 
}

Step 38: AnalogTimer Class AIR Application Drag Functionality

These three methods work together to drag the application. When stage.nativeWindow.startMove() is triggered, the whole AIR app will move around the desktop. They should be self-explanatory.

Add the following lines of code:

private function appPress(e:MouseEvent):void 
{ 
	addEventListener (MouseEvent.MOUSE_MOVE, drag); 
} 
 
private function appRelease(e:MouseEvent):void 
{ 
	removeEventListener (MouseEvent.MOUSE_MOVE, drag); 
} 
 
private function drag (e:MouseEvent):void 
{ 
	stage.nativeWindow.startMove (); 
}

(The appPress and appRelease event listeners were added to the buildApp method in Step 37.)


Step 39: AnalogTimer Class startTimer() Method

When the playButton is clicked, playTimer() is called and all the CustomTimer instances in _timerArray are told to start.

Add the following lines of code:

private function startTimer(e:MouseEvent):void 
{ 
	playTimer (); 
	for each (var timer:CustomTimer in _timerArray) timer.start (); 
}

Step 40: AnalogTimer Class playTimer() Method

Here, it removes the event listener for playButton to start the CustomTimer. The resetButton is then assigned a listener to reset the application and the tweenTarget method is called assigning playButton, pauseButton and the enablePauseButton method as parameters.

Add the following lines of code:

private function playTimer():void 
{ 
	playButton.removeEventListener (MouseEvent.CLICK, startTimer); 
	resetButton.addEventListener (MouseEvent.CLICK, resetTimer); 
	tweenTarget (playButton, pauseButton, enablePauseButton); 
}

Step 41: AnalogTimer Class tweenTarget and nextTween Methods

This methods should be positioned as the last two methods for this class. We go over it now to explain what happens when tweenTarget() is called.

Inside tweenTarget(), the first parameter is tweened to an alpha of 0. When the tween completes, nextTween is called sending the second and third parameters. The overwrite property is set to false to ensure the tween completes before nextTween is called.

Inside nextTween, the first parameter is added to the stage to make sure that it is currently on top for visibility. TweenLite is then called to animate it to an alpha of 1. The $function parameter is then called when nextTween completes.

You can feel the effect of this if you try to click like crazy on the start and pause buttons. The animation needs to complete before you can click the button again. You'll see what goes on the enablePauseButton() method soon.

Add the following lines of code:

private function tweenTarget ($target1:MovieClip, $target2:MovieClip, $function:Function = null):void 
{ 
	TweenLite.to ($target1, .125, { alpha:0, onComplete:nextTween, onCompleteParams:[$target2, $function], overwrite:false } ); 
} 
 
private function nextTween($target:MovieClip, $function:Function = null):void 
{ 
	addChild ($target); 
	TweenLite.to ($target, .125, { alpha:1, onComplete: $function, overwrite:false} ); 
}

Step 42: AnalogTimer Class pauseTimer Method

Here, the listener for pauseTimer is removed since the application has already paused. The tweenTarget method is then called again with pauseButton, playButton, & enablePlaybutton() as parameters. It then loops for every CustomTimer instance in _timerArray and calls pause() on them.

Add the following lines of code:

private function pauseTimer(e:MouseEvent):void 
{ 
	pauseButton.removeEventListener (MouseEvent.CLICK, pauseTimer); 
	tweenTarget (pauseButton, playButton, enablePlayButton); 
	for each (var timer:CustomTimer in _timerArray) { timer.pause (); } 
}

Step 43: AnalogTimer Class resumeTimer Method

This method calls playTimer() and then loops for every CustomTimer and calls resume().

Add the following lines of code:

private function resumeTimer(e:MouseEvent):void 
{ 
	playTimer (); 
	for each (var timer:CustomTimer in _timerArray) { timer.resume (); } 
}

Step 44: AnalogTimer Class enablePlayButton & enablePauseButton Methods

These methods are called when nextTween() completes. They simply add event listeners to the buttons to enable pause and resume. Again, this is the reason why the animation needs to complete before you can click the alternate button.

Add the following lines of code:

private function enablePlayButton():void 
{ 
	playButton.addEventListener (MouseEvent.CLICK, resumeTimer); 
} 
 
private function enablePauseButton():void 
{ 
	pauseButton.addEventListener (MouseEvent.CLICK, pauseTimer); 
}

Step 45: AnalogTimer Class resetTimer Method

Inside this method is what goes on when the resetButton (if active) is clicked. All listeners for the resetButton, pauseButton, and playButton are removed.

It then applies animation to the resetButton, playButton, and analogContainer. Notice the soft fade in and out of the numbers, playButton, resetButton and pauseButton whenever you reset the application.

After all that, the buildApp() method is called to create a new set of Odometers and CustomTimers.

Add the following lines of code:

private function resetTimer(e:MouseEvent):void 
{ 
	resetButton.removeEventListener (MouseEvent.CLICK, resetTimer); 
	pauseButton.removeEventListener (MouseEvent.CLICK, pauseTimer); 
	playButton.removeEventListener (MouseEvent.CLICK, resumeTimer); 
	tweenTarget (resetButton, resetButton); 
	tweenTarget (playButton, playButton); 
	tweenTarget (analogContainer, analogContainer); 
 
	buildApp (); 
}

Step 46: AnalogTimer Class minimizeTimer & closeTimer Methods

These methods are responsible for minimizing and closing the AIR application. Again, they are simple and straight forward.

Add the following lines of code:

private function minimizeTimer(e:MouseEvent):void 
{ 
	stage.nativeWindow.minimize (); 
} 
 
private function closeTimer(e:MouseEvent):void 
{ 
	stage.nativeWindow.close (); 
}

That's it! We're done! Your AnalogTimer Class should now look like this:

package 
{ 
	import com.greensock.TweenLite; 
   import flash.display.MovieClip; 
   import flash.display.Sprite; 
   import flash.events.Event; 
   import flash.events.MouseEvent; 
   import flash.text.Font; 
 
   public class AnalogTimer extends Sprite 
   { 
   	public var playButton:MovieClip; 
   	public var resetButton:MovieClip; 
   	public var pauseButton:MovieClip; 
 
   	public var minimizeButton:MovieClip; 
   	public var closeButton:MovieClip; 
 
   	public var analogContainer:MovieClip; 
   	public var digitMask:MovieClip; 
   	public var background:MovieClip; 
 
   	[Embed(source="MgOpenModernaBold.ttf", 
   	fontName = "MgOpen", 
   	fontWeight = 'bold', 
   	advancedAntiAliasing="true", 
   	mimeType = "application/x-font")] private var MgOpen:Class; 
	private var _timerArray:Array; 
   	private var _adjustment:uint = 8; 
 
   	public function AnalogTimer() 
   	{ 
   		buildApp (); 
   	} 
 
   	private function buildApp():void 
   	{ 
   		addChild (playButton); 
   		background.addEventListener (MouseEvent.MOUSE_DOWN, appPress); 
   		background.addEventListener (MouseEvent.MOUSE_UP, appRelease); 
   		playButton.addEventListener (MouseEvent.CLICK, startTimer); 
   		minimizeButton.addEventListener (MouseEvent.CLICK, minimizeTimer); 
   		closeButton.addEventListener (MouseEvent.CLICK, closeTimer); 
 
   		pauseButton.alpha = 0; 
   		playButton.alpha = 1; 
 
   		_timerArray = new Array; 
 
   		if (analogContainer.numChildren > 1) 
   		{ 
   			for (var h:uint = analogContainer.numChildren; h > 1; h--) analogContainer.removeChildAt (h - 1); 
   		} 
 
   		var sequence:Array = 
   		[ 
   		[Time.ONE_TENTH_SECOND, digitMask.oneTenthSecond], 
   		[Time.ONE_SECOND, digitMask.oneSecond], 
   		[Time.TEN_SECONDS, digitMask.tenSeconds], 
   		[Time.ONE_MINUTE, digitMask.oneMinute], 
   		[Time.TEN_MINUTES, digitMask.tenMinutes], 
   		[Time.ONE_HOUR, digitMask.oneHour], 
   		[Time.TEN_HOURS, digitMask.tenHours] 
   		]; 
 
   		for (var i:uint = 0; i < sequence.length; i++) 
   		{ 
   			var ct:CustomTimer = new CustomTimer (sequence[i][0]); 
   			_timerArray.push (ct); 
 
   			var odometer:Odometer = new Odometer (ct); 
   			analogContainer.addChild (odometer); 
 
   			odometer.x = sequence[i][1].x - _adjustment; 
   			odometer.y = sequence[i][1].y + _adjustment; 
   		} 
   		analogContainer.mask = digitMask; 
   	} 
 
   	private function appPress(e:MouseEvent):void 
   	{ 
   		addEventListener (MouseEvent.MOUSE_MOVE, drag); 
   	} 
 
   	private function appRelease(e:MouseEvent):void 
   	{ 
   		removeEventListener (MouseEvent.MOUSE_MOVE, drag); 
   	} 
 
   	private function drag (e:MouseEvent):void 
   	{ 
   		stage.nativeWindow.startMove (); 
   	} 
 
   	private function startTimer(e:MouseEvent):void 
   	{ 
   		playTimer (); 
   		for each (var timer:CustomTimer in _timerArray) timer.start (); 
   	} 
 
   	private function playTimer():void 
   	{ 
   		playButton.removeEventListener (MouseEvent.CLICK, startTimer); 
   		resetButton.addEventListener (MouseEvent.CLICK, resetTimer); 
   		tweenTarget (playButton, pauseButton, enablePauseButton); 
   	} 
 
   	private function pauseTimer(e:MouseEvent):void 
   	{ 
   		pauseButton.removeEventListener (MouseEvent.CLICK, pauseTimer); 
   		tweenTarget (pauseButton, playButton, enablePlayButton); 
   		for each (var timer:CustomTimer in _timerArray) { timer.pause (); } 
   	} 
 
   	private function resumeTimer(e:MouseEvent):void 
   	{ 
   		playTimer (); 
   		for each (var timer:CustomTimer in _timerArray) { timer.resume (); } 
   	} 
 
   	private function enablePlayButton():void 
   	{ 
   		playButton.addEventListener (MouseEvent.CLICK, resumeTimer); 
   	} 
 
   	private function enablePauseButton():void 
   	{ 
   		pauseButton.addEventListener (MouseEvent.CLICK, pauseTimer); 
   	} 
 
   	private function resetTimer(e:MouseEvent):void 
   	{ 
   		resetButton.removeEventListener (MouseEvent.CLICK, resetTimer); 
   		pauseButton.removeEventListener (MouseEvent.CLICK, pauseTimer); 
			playButton.removeEventListener (MouseEvent.CLICK, resumeTimer); 
   		tweenTarget (resetButton, resetButton); 
   		tweenTarget (playButton, playButton); 
   		tweenTarget (analogContainer, analogContainer); 
 
   		buildApp (); 
   	} 
 
   	private function minimizeTimer(e:MouseEvent):void 
   	{ 
   		stage.nativeWindow.minimize (); 
   	} 
 
  		private function closeTimer(e:MouseEvent):void 
   	{ 
   		stage.nativeWindow.close (); 
   	} 
 
   	private function tweenTarget ($target1:MovieClip, $target2:MovieClip, $function:Function = null):void 
   	{ 
   		TweenLite.to ($target1, .125, { alpha:0, onComplete:nextTween, onCompleteParams:[$target2, $function], overwrite:false } ); 
   	} 
 
   	private function nextTween($target:MovieClip, $function:Function = null):void 
   	{ 
  			addChild ($target); 
   		TweenLite.to ($target, .125, { alpha:1, onComplete: $function, overwrite:false} ); 
   	} 
 
   } 
  }

Go ahead and assign AnalogTimer as the document class for analogTimer.fla. If everything went well, you should have something like the preview with the minimize and close buttons enabled.

Hit Shift + F12, the Digital Signature window will pop up. Click 'Create' for the certificate. Fill out all the fields and save it into the 'AnalogTimer' folder.

Type in the password you chose when you created the Self-signed Digital Certificate and click 'OK'. You now have an AIR application published and ready to install on your computer.


Conclusion

I hope this tutorial helped you understand some of the possibilities of use for the CustomTimer class. Thanks so much for reading!

Again, for any comments, concerns, or suggestions, please don't hesitate to post in the comments section.

One more thing, I used simple vectors for the GUI since we were focusing on the logic to build the application. Feel free to modify and improve it as you like. =)