Flash + Spry = Fly

I was playing with Dreamweaver the other day, just to see what kind of non Flash platform technologies are available these days, when something caught my eye. That “something” was the Collapsible Panel widget – part of the Spry/ajax framework that comes with Dreamweaver. It seemed to me that with just a minimal amount of ajax style javascript, one could add new content to the panel on the fly – even from a Flash or Flex built .swf file. So after a little bit of playing around, this here is what I came up. While I don’t intend on writing a full fledged tutorial, I thought I’d share how it was done. Bear in mind that although I put together a simple image gallery, you could also use the Collapsible Panel to display .pdf files, .doc files, maps, etc. Essentially, anything you would normally display in popup window can, instead, be kept tucked away in the same html page eliminating the need for those annoying popups.

To get started, let’s take a look at the generic .html document created by Dreamweaver when you insert a Collapsible Panel into a blank page (I’ll call it index.html from here out). It should look something like this:





Untitled Document





Tab
Content

The first thing we want to do is customize the file a little bit. Give the document a decent title. In addition, give the Collapsible Panel a meaningful instance name. Since I was using mine to display image files, I called it “imagePanel”. This has to be changed towards the bottom of the html file in the line that instantiates the panel (both the var name and the argument passed to the class) and also in the id attribute of the div immediately following the opening body tag. Also, add some meaningful content to the CollapsiblePanelTab and CollapsiblePanelContent divs (e.g. for mine I used “- Click here to display/hide selected image -” and “This text will be replaced by selected image” – okay, so maybe the second item wasn’t that meaningful, but you can add what you want). Finally, and perhaps most importantly, add an id attribute to the “CollapsiblePanelContent” div. I just used the simple id “panelContent”. This is what we’ll be using to add content to the div later, so it’s a step you really don’t want to overlook if you’re playing along at home. After these changes your index.html should look something similar to this:





My Title





-Click here to display/hide selected image-
This text will be replaced by selected image.

Since we already have the html file open, let’s go ahead and write the markup to add a .swf file. I used SWFObject 2.0 to embed my .swf (when using SWFObject, don’t forget to copy the swfobject.js file to the correct directory!), so that’s the method I’ll be showing here. I knew in advance I’d call the .swf “main.swf” and also knew I’d be targetting the version 9 Flash Player. Also, it’s always good to throw in a little bit of CSS styling to get rid of unwanted padding around your content – nothing too fancy or new to most folks here. My final index.html file wound up looking like this:





Flash + Spry = Fly  |  another fine mess from onebyonedesign.com














- Click here to display/hide selected image -
This text will be replaced by selected image.
This content requires a more up to date Flash Player and javascript.

Once that’s done, you’re done with the .html file for good. You can set that aside and dive right into the Flash stuff, so go ahead fire that up. Now in this case (as is usual when I work with Flash), the only thing I used Flash for was publishing and setting movie properties – the document class, background color, frame rate, etc. The real content of the .swf was contained in the document class, Main.as, which I created and edited using FlashDevelop. This is the biggest part of the file here:

package {
	
	import caurina.transitions.Tweener;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Loader;
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.external.ExternalInterface;
	import flash.filters.DropShadowFilter;
	import flash.net.URLRequest;
	import flash.utils.Dictionary;
	
	public class Main extends Sprite {
		
		private var _pics:Array = [ { thumb:"thumbs/doll1.jpg", big:"images/doll1.jpg" }, { thumb:"thumbs/doll2.jpg", big:"images/doll2.jpg" }, { thumb:"thumbs/doll3.jpg", big:"images/doll3.jpg" }, { thumb:"thumbs/doll4.jpg", big:"images/doll4.jpg" } ];
		private var _numPics:int;
		private var _currentPic:int;
		private var _imageHolder:Sprite;
		private var _largePics:Dictionary;
		
		private var _shadow:DropShadowFilter = new DropShadowFilter(2, 90, 0x000000, .7, 2, 2, 1, 1);
		
		public function Main():void {
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			stage.addEventListener(Event.RESIZE, stageHandler);
			stage.showDefaultContextMenu = false;
			
			_largePics = new Dictionary(true);
			
			_currentPic = 0;
			_numPics = _pics.length;

			_imageHolder = new Sprite();
			addChild(_imageHolder);
			
			stageHandler(null);
			
			init();
		}
		
		private function init():void {
			beginThumbLoad()
		}
		
		private function beginThumbLoad():void {
			loadThumb(_pics[_currentPic].thumb);
		}
		
		private function loadThumb(imgPath:String):void {
			var loader:Loader = new Loader();
			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onImageLoad);
			loader.load(new URLRequest(imgPath));
		}
		
		private function onImageLoad(event:Event):void {
			var l:Loader = event.currentTarget.loader;
			var bmpData:BitmapData = new BitmapData(l.width, l.height);
			bmpData.draw(l);
			l = null;
			var img:Bitmap = new Bitmap(bmpData, "auto", true);
			img.x -= img.width * .5;
			img.y -= img.height * .20;
			var container:Sprite = new Sprite();
			container.buttonMode = true;
			container.useHandCursor = true;
			container.filters = [_shadow];
			container.addChild(img);
			container.x = _currentPic * 105;
			_largePics[container] = _pics[_currentPic].big;
			_imageHolder.addChild(container);
			centerImages();
			container.addEventListener(MouseEvent.MOUSE_OVER, onOver);
			container.addEventListener(MouseEvent.MOUSE_OUT, onOut);
			container.addEventListener(MouseEvent.CLICK, imageHandler);
			if (_currentPic++ < _numPics - 1)
				loadThumb(_pics[_currentPic].thumb);
		}
		
		private function onOver(event:MouseEvent):void {
			var targ:Sprite = event.currentTarget as Sprite;
			var shadow:DropShadowFilter = event.currentTarget.filters[0];
			_imageHolder.addChild(targ);
			Tweener.addTween(targ, { scaleX:1.5, scaleY:1.5,  transition:"easeOutQuad", time:.25 } );
			Tweener.addTween(shadow, { blurX:16, blurY:16, distance:12, transition:"easeOutQuad", time:.25, onUpdate:adjustShadow, onUpdateParams:[targ, shadow] } );
		}
		
		private function onOut(event:MouseEvent):void {
			var targ:Sprite = event.currentTarget as Sprite;
			var shadow:DropShadowFilter = event.currentTarget.filters[0];
			Tweener.addTween(targ, { scaleX:1, scaleY:1, transition:"easeOutQuad", time:.25 } );
			Tweener.addTween(shadow, { blurX:2, blurY:2, distance:2, transition:"easeOutQuad", time:.25, onUpdate:adjustShadow, onUpdateParams:[targ, shadow] } );
		}
		
		private function adjustShadow(targ:Sprite, shadow:DropShadowFilter) {
			targ.filters = [shadow];
		}
		
		private function centerImages():void {
			_imageHolder.y = 25;
			Tweener.addTween(_imageHolder, { x:(stage.stageWidth * .5 - _imageHolder.width * .5) + 50, transition:"easeOutElastic", time:1} );
		}
		
		private function stageHandler(event:Event):void {
			_imageHolder.x = stage.stageWidth * .5 - _imageHolder.width * .5 + 50;
			_imageHolder.y = 25;
		}
		
		private function imageHandler(event:MouseEvent):void {
			// Stuff to swap out image in Collapsible Pane will go here.
		}
	}
}

I won't dwell too much on this. It's all fairly basic stuff. It just loads some thumbnail images, centers them on each load, and adds some groovy rollover and rollout effects using Tweener. The paths to both the thumbnail images and the corresponding full sized pictures are stored inside an array for the sake of simplicity. They could easily be loaded in via xml or from a database or however you like to load that sort of data. The actual .jpg files themselves are kept in two directories ("images" for the larger pictures and "thumbs" for the thumbnails) which are located in the same directory as main.swf and index.html.

Now comes the fun part.

Finally, to wrap this up, we have to add the script that will be run when a user clicks on one of the thumbnail instances. That is, the script that will actually replace the content in our Collapsible Panel's content div. As I implied earlier, all that is really needed to perform this little swapperoo is a simple ajaxian javascript function. All this function needs to do is target the Collapsible Panel Content div, create a new img (image) node, set the img node's src attribute as the path to the selected picture, delete whatever node the content div currently has, then append the newly created img node to the content div. Written out, it would probably look something like:

function swapImage(image) {
	// target our panel's content div
	var targ = document.getElementById("panelContent");
	// create an  node and set it's src attribute to argument passed
	var node = document.createElement("img");
	node.setAttribute("src", image);
	// remove whatever node our target currently contains
	targ.removeChild(targ.firstChild);
	// add the image node to the target
	targ.appendChild(node);
}

Now we could just insert that javascript function into the head of our index.html file and be done with it, but that's like Squaresville, dads. The cool thing these days is .swf Javascript Injection. For those unfamiliar with the concept, I'll give a quick rundown. To understand javascript injection, you have to understand how the Actionscript ExternalInterface class works. The static ExternalInterface.call() method accepts one or more String arguments - the first is a string version of the function inside the .swf's container (in this case, html) to call, all others (if present) are arguments passed to that function. So, for quick example, let's say we want to pop up an alert box that said "Hello, World" using the javascript alert() function. We could simply say ExternalInterface.call("alert", "Hello, World"); . Doesn't get much easier than that. But now things get interesting. Instead of calling a named javascript function, you could also pass an anonymous function that will be run immediately. So, our Hello World example could also be written like this: ExternalInterface.call("function() {alert('Hello World');}"); . But what if we didnt' want to say "Hello World" or wanted to say different things at different times? Well, just like a named function, an anonymous function passed to the ExternalInterface.call() method can accept arguments as well. Take a look at this example:

var javaScriptFunction:String = "function(msg){alert(msg);}";
var message:String = "Goodbye, cruel world.";
ExternalInterface.call(javaScriptFunction, message);

Big fat soggy deal, you may be saying, we could have done the same thing with Actionscript 2. Well, you're right. But what Actionscript 3 brings to the table is the ability to write inline XML right in our script. And what does that mean? Well, complicated javascript functions in Actionscript 2 could lead to some extremely complex and convoluted String instances. Really better to leave the js inside the html. In AS 3 however, we can insert our javascript into some CDATA which is, in turn, included in an XML instance and we have script that looks like actual script. That script can be passed to the ExternalInterface.call() method using the XML.toString() method. Probably best to look at an example at this point, so here is the final Main.as class:

package {
	
	import caurina.transitions.Tweener;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Loader;
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.external.ExternalInterface;
	import flash.filters.DropShadowFilter;
	import flash.net.URLRequest;
	import flash.utils.Dictionary;
	
	public class Main extends Sprite {
		
		private var _pics:Array = [ { thumb:"thumbs/doll1.jpg", big:"images/doll1.jpg" }, { thumb:"thumbs/doll2.jpg", big:"images/doll2.jpg" }, { thumb:"thumbs/doll3.jpg", big:"images/doll3.jpg" }, { thumb:"thumbs/doll4.jpg", big:"images/doll4.jpg" } ];
		private var _numPics:int;
		private var _currentPic:int;
		private var _imageHolder:Sprite;
		private var _largePics:Dictionary;
		
		private var _shadow:DropShadowFilter = new DropShadowFilter(2, 90, 0x000000, .7, 2, 2, 1, 1);
		
		private var _javaScriptCall:XML = 	
									
		
		
		public function Main():void {
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			stage.addEventListener(Event.RESIZE, stageHandler);
			stage.showDefaultContextMenu = false;
			
			_largePics = new Dictionary(true);
			
			_currentPic = 0;
			_numPics = _pics.length;

			_imageHolder = new Sprite();
			addChild(_imageHolder);
			
			stageHandler(null);
			
			init();
		}
		
		private function init():void {
			beginThumbLoad()
		}
		
		private function beginThumbLoad():void {
			loadThumb(_pics[_currentPic].thumb);
		}
		
		private function loadThumb(imgPath:String):void {
			var loader:Loader = new Loader();
			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onImageLoad);
			loader.load(new URLRequest(imgPath));
		}
		
		private function onImageLoad(event:Event):void {
			var l:Loader = event.currentTarget.loader;
			var bmpData:BitmapData = new BitmapData(l.width, l.height);
			bmpData.draw(l);
			l = null;
			var img:Bitmap = new Bitmap(bmpData, "auto", true);
			img.x -= img.width * .5;
			img.y -= img.height * .20;
			var container:Sprite = new Sprite();
			container.buttonMode = true;
			container.useHandCursor = true;
			container.filters = [_shadow];
			container.addChild(img);
			container.x = _currentPic * 105;
			_largePics[container] = _pics[_currentPic].big;
			_imageHolder.addChild(container);
			centerImages();
			container.addEventListener(MouseEvent.MOUSE_OVER, onOver);
			container.addEventListener(MouseEvent.MOUSE_OUT, onOut);
			container.addEventListener(MouseEvent.CLICK, imageHandler);
			if (_currentPic++ < _numPics - 1)
				loadThumb(_pics[_currentPic].thumb);
		}
		
		private function onOver(event:MouseEvent):void {
			var targ:Sprite = event.currentTarget as Sprite;
			var shadow:DropShadowFilter = event.currentTarget.filters[0];
			_imageHolder.addChild(targ);
			Tweener.addTween(targ, { scaleX:1.5, scaleY:1.5,  transition:"easeOutQuad", time:.25 } );
			Tweener.addTween(shadow, { blurX:16, blurY:16, distance:12, transition:"easeOutQuad", time:.25, onUpdate:adjustShadow, onUpdateParams:[targ, shadow] } );
		}
		
		private function onOut(event:MouseEvent):void {
			var targ:Sprite = event.currentTarget as Sprite;
			var shadow:DropShadowFilter = event.currentTarget.filters[0];
			Tweener.addTween(targ, { scaleX:1, scaleY:1, transition:"easeOutQuad", time:.25 } );
			Tweener.addTween(shadow, { blurX:2, blurY:2, distance:2, transition:"easeOutQuad", time:.25, onUpdate:adjustShadow, onUpdateParams:[targ, shadow] } );
		}
		
		private function adjustShadow(targ:Sprite, shadow:DropShadowFilter) {
			targ.filters = [shadow];
		}
		
		private function centerImages():void {
			_imageHolder.y = 25;
			Tweener.addTween(_imageHolder, { x:(stage.stageWidth * .5 - _imageHolder.width * .5) + 50, transition:"easeOutElastic", time:1} );
		}
		
		private function stageHandler(event:Event):void {
			_imageHolder.x = stage.stageWidth * .5 - _imageHolder.width * .5 + 50;
			_imageHolder.y = 25;
		}
		
		private function imageHandler(event:MouseEvent):void {
			var largeImage:String = _largePics[event.currentTarget];
			ExternalInterface.call(_javaScriptCall.toString(), largeImage);
		}
	}
}

And that is, as they say, it. The only thing left to do is compile the .swf and test the index.html file on a server, whether local or remote. Again, if all went well, you should wind up with something like this. If all didn't go well, double check your directory structure as that may be the most likely culprit. In this example, the main directory should contain main.swf, swfobject.js, index.html and three other directories - "images" which contains the full sized image files ("doll1.jpg", "doll2.jpg", "doll3.jpg", and "doll4.jpg" in this case), "thumbs" which contains the 100x100 pixel versions of the larger images (and have the same names) and "SpryAssets" which is created auto-magically by Dreamweaver and contains the files SpryCollapsiblePanel.css (which you can modify to change your panel's appearance or center content like I did) and SpryCollapsiblePanel.js.

Hope this might inspire some ideas. I'm definitely thinking the next time someone asks for a popup, I'm going to suggest a collapsible panel instead. And that's only one of the Spry framework widgets - there's still more to explore.

Date: