Where accuracy is required you can't rely on the Timer class alone to give you a precise passage of time. Especially if the Flash Player is doing a lot of other work. This is because the Timer classes interval is actually a request and not a guarantee. Let's take a look, start with a trivial implementation of a timer ticking once a second (1000
milliseconds):
private var startTime:uint;
private var ticks:int;
protected function start():void
{
var t:Timer = new Timer( 1000 );
t.addEventListener( TimerEvent.TIMER, timerTick );
t.start();
startTime = getTimer();
}
protected function timerTick( event:TimerEvent ):void
{
trace( 'Ideal: ' + (++ticks) + ' Actual: ' + (getTimer()-startTime)/1000 );
}
Using getTimer
(get familiar with getTimer) to measure the actual time we can see within 20 seconds the Timer class is a half a second behind. This drift will vary each time this is run:
Expected: 1 Actual: 1.043
Expected: 2 Actual: 2.083
Expected: 3 Actual: 3.082
…
Expected: 18 Actual: 18.417
Expected: 19 Actual: 19.457
Expected: 20 Actual: 20.5
That's where implementing a Stopwatch comes in handy for measuring time more precisely:
import flash.utils.getTimer;
public class Stopwatch
{
private var startStamp:Number;
private var stopStamp:Number;
private var runTime:Number;
private var _countdownDuration:Number;
private var started:Boolean;
private var stopped:Boolean;
private var paused:Boolean;
function Stopwatch( startNow:Boolean = true ):void
{
if ( startNow )
start();
}
public function start():void
{
runTime = 0;
startStamp = getTimer();
_countdownDuration = 0;
started = true;
stopped = false;
paused = false;
}
public function startCountdown( milliseconds:Number ):void
{
start();
_countdownDuration = milliseconds;
}
public function pause():void
{
if ( started && ! stopped )
{
runTime += getTimer() - startStamp;
paused = true;
}
}
public function resume():void
{
if ( started && paused )
{
startStamp = getTimer();
paused = false;
}
}
public function stop():void
{
if ( started && ! stopped )
{
if ( ! paused )
runTime += getTimer() - startStamp;
stopped = true;
paused = false;
}
}
public function set elapsed( value:uint ):void
{
runTime = value;
if ( running )
startStamp = getTimer();
}
public function get elapsed():uint
{
if ( running )
return ( getTimer() - startStamp ) + runTime;
return runTime;
}
public function get running():Boolean
{
return ( started && ! paused && ! stopped );
}
public function get countdownDuration():Number
{
return _countdownDuration;
}
public function set countdownDuration( value:Number ):void
{
_countdownDuration = value;
}
public function get remaining():int
{
if ( ! _countdownDuration )
return 0;
else if ( _countdownDuration - elapsed < 0 )
return 0;
return _countdownDuration - elapsed;
}
}
Extending the first example with Stopwatch you can effectively measure the passage of time very simply (just remember stopwatch.elapsed
is in milliseconds so we'll divide by 1000 for seconds):
private var stopwatch:Stopwatch;
protected function start():void
{
var t:Timer = new Timer( 1000 );
t.addEventListener( TimerEvent.TIMER, timerTick );
t.start();
stopwatch = new Stopwatch;
stopwatch.start();
}
protected function timerTick( event:TimerEvent ):void
{
trace( stopwatch.elapsed/1000 + ' seconds have elapsed',
(60 * 10) - stopwatch.elapsed/1000 + ' seconds remain' );
}
Since stopwatch.elapsed
is in milliseconds, you'll want to convert that quantity to different time increments. Following the Single Responsibility Principle we'll make a reusable general use class called StopwatchFormatter to help us consolidate these calculations and expose a readable API:
public class StopwatchFormatter
{
private var elapsed:Number;
public var paddedSize:int;
public var cappedDecimalLength:int;
function StopwatchFormatter( paddedSize:Number = 2, cappedDecimalLength:Number = 1, elapsed:Number = 0 )
{
this.elapsed = elapsed;
this.paddedSize = paddedSize;
this.cappedDecimalLength = cappedDecimalLength;
}
// INPUTS
public function setTimeAsGroup( hours:Number, minutes:Number = 0, seconds:Number = 0, milliseconds:Number = 0 ):StopwatchFormatter
{
elapsed = ( hours * 60 * 60 * 1000 ) + ( minutes * 60 * 1000 ) + ( seconds * 1000 ) + milliseconds;
return this;
}
public function set totalMilliseconds( value:Number ):void
{
elapsed = value;
}
public function set totalSeconds( value:Number ):void
{
elapsed = value * 1000;
}
public function set totalMinutes( value:Number ):void
{
elapsed = value * 1000 * 60;
}
public function set totalHours( value:Number ):void
{
elapsed = value * 1000 * 60 * 60;
}
// CLOCK LIKE
// (converting to int will drop the decimal place)
public function get milliseconds():int
{
return elapsed % 1000;
}
public function get seconds():int
{
return ( elapsed / 1000 ) % 60;
}
public function get minutes():int
{
return ( elapsed / 1000 / 60 ) % 60;
}
public function get hours():int
{
return ( elapsed / 1000 / 60 / 60 ) % 24;
}
// CLOCK PADDED (zeroes in the front)
// 5 becomes "05" , 10 becomes "10" where _paddedSize is 2
public function get millisecondsPadded():String
{
return frontPad( milliseconds );
}
public function get secondsPadded():String
{
return frontPad( seconds );
}
public function get minutesPadded():String
{
return frontPad( minutes );
}
public function get hoursPadded():String
{
return frontPad( hours );
}
// TOTAL
public function get totalMilliseconds():Number
{
return elapsed;
}
public function get totalSeconds():Number
{
return elapsed / 1000;
}
public function get totalMinutes():Number
{
return elapsed / 1000 / 60;
}
public function get totalHours():Number
{
return elapsed / 1000 / 60 / 60;
}
// TOTAL CAPPED
// 3.134 becomes 3.1 where _cappedDecimalLength is 1
public function get totalMillisecondsCapped():Number
{
return capped( totalMilliseconds );
}
public function get totalSecondsCapped():Number
{
return capped( totalSeconds );
}
public function get totalMinutesCapped():Number
{
return capped( totalMinutes );
}
public function get totalHoursCapped():Number
{
return capped( totalHours );
}
// TOTAL CAPPED + PADDED (zeroes in the back and one zero in the front for values less than 0)
// 3.101 becomes "3.10" where _cappedDecimalLength is 2
public function get totalSecondsCappedPadded():String
{
return capped( totalSeconds ).toFixed( cappedDecimalLength );
}
public function get totalMinutesCappedPadded():String
{
return capped( totalMinutes ).toFixed( cappedDecimalLength );
}
public function get totalHoursCappedPadded():String
{
return capped( totalHours ).toFixed( cappedDecimalLength );
}
// UTILITY FUNCTIONS
private function frontPad( n:int ):String
{
var s:String = n.toString();
if ( s.length < paddedSize )
{
var i:int = 0;
var len:int = paddedSize - s.length;
for ( ; i < len; i++ )
{
s = "0" + s;
}
}
return s;
}
private function capped( input:Number ):Number
{
if ( cappedDecimalLength == 0 )
return Math.floor( input );
var decimalFactor:Number = Math.pow( 10, cappedDecimalLength );
return Math.floor( input * decimalFactor ) / decimalFactor;
}
}
Pulling it all together with these two classes and a Timer we have a trivial way to countdown:
private var stopwatch:Stopwatch;
private var time:StopwatchFormatter;
protected function start():void
{
var t:Timer = new Timer( 1000 );
t.addEventListener( TimerEvent.TIMER, timerTick );
t.start();
stopwatch = new Stopwatch;
stopwatch.startCountdown( new StopwatchFormatter().setTimeAsGroup( 1, 10, 30 ).totalMilliseconds );
time = new StopwatchFormatter;
}
protected function timerTick( event:TimerEvent ):void
{
time.totalMilliseconds = stopwatch.elapsed;
var elapsed:String = time.hoursPadded + ':' + time.minutesPadded + ':' + time.secondsPadded + ':' + time.millisecondsPadded;
time.totalMilliseconds = stopwatch.remaining;
var remainig:String = time.hoursPadded + ':' + time.minutesPadded + ':' + time.secondsPadded + ':' + time.millisecondsPadded;
trace( 'Elapsed:', elapsed, "Remaining:", remainig );
}