Faking abstract classes in AS3: a slideshow app

Here’s a quick little exercise in writing an AS3 application. It’s a basic image slide show I wrote as a “code only” app in Flash CS4. There’s nothing in the Library, nothing on the stage. The .fla is only used to specify a Document Class (BasicSlideShow) and compile the SWF. The code can also be compiled via Flex Builder 3, FlashDevelop, FDT or Flash Builder with minimal change (mostly the metadata stuff like [SWF (width=”600″, height=”400″, backgroundColor=”#9999cc”, frameRate=”31″)]).

Faking an abstract class in AS3.0.

AS3.0 only supports Interfaces, so that’s what I started out with at first. However, soon enough I found myself repeating code (always inefficient and error prone). So I switched to a simulated abstract class to combine the benefits of an Interface with the code reuse utility offered by these puppies:

package com.timshaya.model
{
	import gs.TweenLite;
	import flash.display.Sprite;
	import flash.net.URLRequest;
	import flash.net.navigateToURL;
	import flash.events.MouseEvent;

	//DO NOT implement, subclass instead
	public class AbstractSlide extends Sprite
	{
		protected var _path:String;
		protected var _link:String;

		public function get path():String { return _path; }
		public function set path(val:String):void { _path = val; }
		public function get link():String { return _link; }
		public function set link(val:String):void { _link = val; }

		public function AbstractSlide(path2:String="", link2:String=""){}

		//faking an abstract method in AS3.0
        public function setClickArea():void
		{
			throw new Error("AbstractSlide shouldn't be implemented. Subclass it insteaad and override this method: setClickArea");
		}

		//faking an abstract method in AS3.0
		public function doLink(e:MouseEvent):void
		{
			throw new Error("AbstractSlide shouldn't be implemented. Subclass it insteaad and override this method: doLink");
		}

		//faking an abstract method in AS3.0
		public function animateSlideIn(spd:Number):void
		{
			throw new Error("AbstractSlide shouldn't be implemented. Subclass it insteaad and override this method: animateSlideIn");
		}

		public function fadeInSlide(speed:Number):void
		{
			this.alpha = 0;
			TweenLite.to(this, speed, {alpha:1});
                //using this static method enables TweenLite's automatic memory management
		}
	}
}

Here’s a subclass that uses AbstractSlide:

package com.timshaya.model
{
	import flash.display.Sprite;
	import flash.net.URLRequest;
	import flash.net.navigateToURL;
	import flash.events.MouseEvent;

	public class SimpleSlide extends AbstractSlide
	{
		public function SimpleSlide(path2:String = "", link2:String = "")
		{
			this.path = path2;
			this.link = link2;
			this.setClickArea();
		}

		public override function setClickArea():void
		{
			this.buttonMode = true;
			this.useHandCursor = true;
			this.addEventListener(MouseEvent.CLICK, doLink, false, 0, true);
		}

		public override function doLink(e:MouseEvent):void
		{
			var url:String = this.link; //grab url from getter method
			var req:URLRequest = new URLRequest(url);

			try {
				navigateToURL(req, '_self');
			} catch (e:Error) {
				trace("Something's wrong with the URL!");
			}
		}

		public override function animateSlideIn(spd:Number):void
		{
			fadeInSlide(spd);
		}
	}
}

The bulk of the business logic is in this class (the Document Class in this case):

package
{
	import com.timshaya.model.events.SlideRotator;
	import com.timshaya.model.SimpleSlide;

	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.ProgressEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.net.URLLoaderDataFormat;
	import flash.display.Loader;

	public class BasicSlideShow extends Sprite
	{
		private var xmlPath:String;

		private var firstTime:Boolean = true;
		private var slideShow:SlideRotator;
		private var slideContainer:Sprite;
		private var currntSlide:int;
		private var currntImg:Sprite;
		private var myImages:Array = new Array();
		private var myImg:Sprite;
		private var loader:URLLoader;

		public function set theXmlPath(val:String):void { xmlPath = val; }
		public function get theXmlPath():String { return xmlPath; }

		public function BasicSlideShow(defaultPath:String = "slideshow.xml")
		{
			this.theXmlPath = defaultPath;
			buildImgList();
		}

		public function buildImgList():void
		{
			currntSlide = 0;
			slideContainer = new Sprite();
			slideContainer.x = slideContainer.y = 0;
			addChild(slideContainer);

			loadAllImages(); //load external images
		}

		public function loadAllImages():void
		{
			//load XML list of images (with paths & urls)
			var xmlURL:URLRequest = new URLRequest(this.theXmlPath);
			loader = new URLLoader();
            loader.dataFormat = URLLoaderDataFormat.TEXT;  
			loader.addEventListener(Event.COMPLETE, onXMLDoneLoading, false, 0, true);
			loader.load(xmlURL);
		}

		public function onXMLDoneLoading(e:Event):void
		{
			loader.removeEventListener(Event.COMPLETE, onXMLDoneLoading); //free up memory slot

			var myXml = new XML(e.target.data);
			for(var g:int=0; g < myXml.image.length(); g++)
			{
				trace("g = " + g);
				myImg = new SimpleSlide();
				myImages.push(myImg);
              // make myImages as long as the num of images in XML file
			}

			for each (var img:Sprite in myImages) //add img containers to main display object
			{
				img.visible = false;
				slideContainer.addChild(img);
			}

			for(var i:int=0; i < myXml.image.length(); i++)
			{
				SimpleSlide(myImages[i]).path = myXml.image[i].path.@src;
				SimpleSlide(myImages[i]).link = myXml.image[i].url.@src;

				var imgLoader = new Loader(); //load the actual JPEGs
				imgLoader.load(new URLRequest(myImages[i].path));
				imgLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, imageLoaded, false, 0, true);
				imgLoader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, imageLoadProgress, false, 0, true);
				myImages[i].addChild(imgLoader);
			}

			doneLoading();
		}

		public function imageLoaded(e:Event):void { trace("imageLoaded"); }

		public function imageLoadProgress(e:ProgressEvent):void
		{
			trace((e.target.bytesLoaded / e.target.bytesTotal)*100 + "%"); 
            // cheezy; good for demo purposes
		}

		public function doneLoading():void
		{
			slideShow = new SlideRotator(); //begin main slideshow timer
			slideShow.addEventListener(SlideRotator.CHANGE_SLIDE, notifyAll, false, 0, true);
		}

		public function nextSlide():void
		{
		    for each (var img:Sprite in myImages) //hide all images
				img.visible = false;

			currntImg = myImages[currntSlide];//select next image to show
			currntImg.visible = true;
			SimpleSlide(currntImg).animateSlideIn(1.33);
		}

		public function notifyAll(e:Event):void
		{
			trace("BasicSlideShow.notifyAll says: SlideRotator dispatch tells us to change slide");

			if(currntSlide < myImages.length-1){  //increment currntSlide
				if(firstTime) {
					currntSlide = 0;
					firstTime = false;
				} else {
					currntSlide += 1;
				}
			} else {
				currntSlide = 0;
			}
			trace("currntSlide = " + currntSlide);

			nextSlide();

			//updateNavbar();
		}

	}
}

NOTE: the image loading code can and should be cleaned up.

 

Most of the custom event flow is in SlideRotator, which extends EventDispatcher:

package com.timshaya.model.events
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.TimerEvent;
	import flash.utils.Timer;

	public class SlideRotator extends EventDispatcher
	{
		public static const TIMER_DELAY:int = 5000;
		public static const CHANGE_SLIDE:String = "changeSlide";

		private var timeCount:Timer;

		public function SlideRotator()
		{
			timeCount = new Timer(TIMER_DELAY);
			timeCount.addEventListener(TimerEvent.TIMER, onTimer, false, 0, true);
			timeCount.start();
		}

		public function onTimer(e:TimerEvent):void
		{
			changeSlide();
		}

		public function changeSlide():void
		{
			dispatchEvent(new Event(SlideRotator.CHANGE_SLIDE));
		}

		public function pauseTimer():void
		{
			timeCount.stop();
		}

	    public function restartTimer():void
		{
			timeCount.start();
		}
	}
}

Here’s the XML source file:

<?xml version="1.0" encoding="UTF-8"?>
<slideshow>
	<image type="1">
		<path src="img/3.jpg"/>
		<url src="http://www.ft.com"/>
	</image>
	<image type="1">
		<path src="img/4.jpg"/>
		<url src="http://www.google.com"/>
	</image>
	<image type="1">
		<path src="img/b1.jpg"/>
		<url src="http://www.yahoo.com"/>
	</image>
	<image type="1">
		<path src="img/b2.jpg"/>
		<url src="http://www.guardian.co.uk"/>
	</image>
	<image type="1">
		<path src="img/b3.jpg"/>
		<url src="http://www.gmail.com"/>
	</image>
	<image type="1">
		<path src="img/Slot2_Flash_template1.jpg"/>
		<url src="http://www.nytimes.com"/>
	</image>
	<image type="1">
		<path src="img/c3.jpg"/>
		<url src="http://www.yahoo.com"/>
	</image>
	<image type="1">
		<path src="img/d3.jpg"/>
		<url src="http://www.amazon.com"/>
	</image>
	<image type="1">
		<path src="img/d3.jpg"/>
		<url src="http://www.amazon.com"/>
	</image>
</slideshow>

You can put any number of images in the “img” directory, BasicSlideShow.as counts the number of images via the XML file.

To build upon this exercise, you can extend AbstractSlide and make your own CustomSlide. For example, if you need a slide with 2 links instead of 1, you can use an XML image tag of type=”2″ and do some basic refactoring to make sure BasicSlideShow catches it. Yep, you can also write an AbstractSlideShow, so instead of refactoring, you’d be adding another implementation while keeping the old one, in case you need it later. Ofcourse, you can add your own NavBar, CustomBtn classes, Pause/Play buttons, etc.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s