Today we're going to build a Photo Slider widget which can be placed on social networking sites like Orkut or MySpace. This widget will be based on Google's Open Social API and contains a Flash application, which will show all photos from your public photo albums in a slideshow.
"Plug-in classes" will handle the transitions of the slideshow and this tutorial will include writing two of those "plug-ins".
Step 1: Things we Need
In order to build and test the widget we'll need a couple of things. I've listed them below:
- Flash CS4
- JavaScript/PHP/XML editor (Eclipse/Aptana)
- Some hosting space (with PHP support)
- An account on a Social Networking site which supports OpenSocial (at least version 0.8, for this tutorial I'll use Orkut).
- Developer status on that Social Networking Site.
For the XML/ JavaScript /PHP part of this tutorial I'll Use Aptana, since that's my editor of choice, but any editor will do of course. I've chosen Orkut because, well, Orkut is Google and OpenSocial is... Google. We need a recent version of the OpenSocial API for this tutorial (Orkut supports version 0.9).
Step 2: Preparations
Before we get started, we'll need to make sure that everything is configured and set up just the way we want it. To test this widget we'll need developer status on Orkut. Go to http://sandbox.orkut.com/SandboxSignup.aspx and fill in the form. You'll get an email confirming your new status. Being a developer gives you the possibility to add widgets to your profile, which aren't yet approved by Orkut. For security reasons these widgets are only available to you. Later on I'll show you in more detail how to add a widget to Orkut.
Next, since we need a publicly available photo album, add some photos to a new album and make it public.

Step 3: Gadget.xml
We're going to build this widget from the bottom up, so the first thing we're going to do is write the widget's XML file. If this is your first encounter with OpenSocial, I'd advise you to go to http://www.opensocial.org/ and read up about it. In short, OpenSocial is a framework developed by Google which can be implemented by Social Network sites making it easier for developers to build applications for them.
A widget generally consists of an XML file and some additional assets. This XML file and the additional assets need to be placed somewhere on a server, that's why we need the Web space. The XML file for this widget will consist of two parts; the XML specs and the JavaScript code, which will retrieve the photos from your albums on your profile and pass them on to the Flash application.
Step 4: Gadget.xml Setup
The layout of the XML file is no rocket science. It contains 2 nodes, ModulePrefs and Content, which are important to us. In this step I'll discuss the first one, ModulePrefs.
This node defines variables which are necessary to describe the widget. I don't think I need to explain anything about the first ones I hope, but if you're confused, please visit the OpenSocial website for more information.
The last one however, is of more importance. With this node we state that we require at least version 0.8 of the OpenSocial API or else it won't work. For this application it's really the only requirement we'll need, but there are many more you can define here. Again, if you want to know more, OpenSocial.org is the place to be.
<?xml version="1.0" encoding="UTF-8"?> <Module> <ModulePrefs title="FlashTuts Widgets" author="Gerb Sterrenburg" author_email="gerbster@gmail.com" description="FlashTuts Widget" height="300" width="400" scaling="false" scrolling="false" screenshot="http://85.17.135.135/~gerbster/projects/FlashTutsWidget/screenshot.png" thumbnail="http://85.17.135.135/~gerbster/projects/FlashTutsWidget/thumbnail.png"> <Require feature="opensocial-0.8"/> </ModulePrefs> <Content type="html"> <![CDATA[ <script type="text/javascript"> // Fetches all of the viewer's albums that are publicly viewable (i.e. // "shared with everyone" var masterAlbumArray = new Array(); function fetchAlbums() { var req = opensocial.newDataRequest(); var idspec = opensocial.newIdSpec({ "userId" : "OWNER", "groupId" : "SELF" }); req.add(req.newFetchAlbumsRequest(idspec), 'ownerAlbums'); req.send(fetchAlbumsHandler); }; // Callback function, executed when orkut finishes fetching the viewer's // public albums function fetchAlbumsHandler(resp) { var ownerAlbumsResp = resp.get('ownerAlbums'); if (!ownerAlbumsResp.hadError()) { var ownerAlbums = ownerAlbumsResp.getData(); // every album is an object inside the main object 'viewerAlbums' // we're going to create one big album which we're gonna feed to // the flash application ownerAlbums.each( function(album) { addAlbumToMasterAlbum(album.getId()); } ); } }; // Fetches all photos from the album with the passed ID function addAlbumToMasterAlbum(albumId) { var req = opensocial.newDataRequest(); var idspec = opensocial.newIdSpec({ "userId" : "OWNER", "groupId" : "SELF" }); req.add(req.newFetchMediaItemsRequest(idspec, albumId), 'albumPhotos'); req.send(addAlbumToMasterAlbumHandler); }; // Callback function, executed when orkut finishes fetching the // requested media items function addAlbumToMasterAlbumHandler(resp) { var albumPhotosResp = resp.get('albumPhotos'); if (!albumPhotosResp.hadError()) { var albumPhotos = albumPhotosResp.getData(); albumPhotos.each( function(photo) { masterAlbumArray.push(photo.getField('url')); } ); // now that all the Photo's are in // the masterAlbum, launch the Flash App launchFlashApp(); } }; // show the flash app function launchFlashApp() { var content = '<embed src="http://85.17.135.135/~gerbster/projects/FlashTutsWidget/widget.swf" width="400" height="300" quality="high" bgcolor="000000" pluginspage="http://www.macromedia.com/go/getflashplayer" allowScriptAccess="always" flashvars="" type="application/x-shockwave-flash">'; document.getElementById("flashTutsWidgetDiv").innerHTML = content; }; function retrieveMasterAlbum() { return masterAlbumArray; }; // Execute fetchAlbums function when gadget loads gadgets.util.registerOnLoadHandler(fetchAlbums); </script> <div id="flashTutsWidgetDiv" style="width:380;height:400"><img src="http://85.17.135.135/~gerbster/projects/FlashTutsWidget/loader.gif" /> Loading...</div> ]]> </Content> </Module>
Step 5: Gadget.xml JavaScript
Now we're getting to the really interesting part of the XML file, the content node. As mentioned above, OpenSocial is an API written in JavaScript to give developers access to all kinds of data inside a Social Network site.
For this widget we're going to use some functionality concerning photo albums and media items. I'll give you the code first and them we'll go through it step by step.
// Fetches all of the viewer's albums that are publicly viewable (i.e. // "shared with everyone" var masterAlbumArray = new Array(); function fetchAlbums() { var req = opensocial.newDataRequest(); var idspec = opensocial.newIdSpec({ "userId" : "OWNER", "groupId" : "SELF" }); req.add(req.newFetchAlbumsRequest(idspec), 'ownerAlbums'); req.send(fetchAlbumsHandler); }; // Callback function, executed when orkut finishes fetching the viewer's // public albums function fetchAlbumsHandler(resp) { var ownerAlbumsResp = resp.get('ownerAlbums'); if (!ownerAlbumsResp.hadError()) { var ownerAlbums = ownerAlbumsResp.getData(); // every album is an object inside the main object 'viewerAlbums' // we're going to create one big album which we're gonna feed to // the flash application ownerAlbums.each( function(album) { addAlbumToMasterAlbum(album.getId()); } ); } }; // Fetches all photos from the album with the passed ID function addAlbumToMasterAlbum(albumId) { var req = opensocial.newDataRequest(); var idspec = opensocial.newIdSpec({ "userId" : "OWNER", "groupId" : "SELF" }); req.add(req.newFetchMediaItemsRequest(idspec, albumId), 'albumPhotos'); req.send(addAlbumToMasterAlbumHandler); }; // Callback function, executed when orkut finishes fetching the // requested media items function addAlbumToMasterAlbumHandler(resp) { var albumPhotosResp = resp.get('albumPhotos'); if (!albumPhotosResp.hadError()) { var albumPhotos = albumPhotosResp.getData(); albumPhotos.each( function(photo) { masterAlbumArray.push(photo.getField('url')); } ); // now that all the Photo's are in // the masterAlbum, launch the Flash App launchFlashApp(); } }; // show the flash app function launchFlashApp() { var content = '<embed src="http://85.17.135.135/~gerbster/projects/FlashTutsWidget/widget.swf" width="400" height="300" quality="high" bgcolor="000000" pluginspage="http://www.macromedia.com/go/getflashplayer" allowScriptAccess="always" flashvars="" type="application/x-shockwave-flash">'; document.getElementById("flashTutsWidgetDiv").innerHTML = content; }; function retrieveMasterAlbum() { return masterAlbumArray; }; // Execute fetchAlbums function when gadget loads gadgets.util.registerOnLoadHandler(fetchAlbums);
The general idea behind this piece of code is not very complex. We'll define an array, which we fill with the URL's of all the photos in the publically accessible albums. If this is done, we'll show the Flash application, which will then retrieve the array we've just made.
Step 6: Retrieving Albums
To retrieve all the photo albums from the profile, we'll need two functions. One that will perform the call and one that will handle the result when it comes back from the call.
Caller:
function fetchAlbums() { var req = opensocial.newDataRequest(); var idspec = opensocial.newIdSpec({ "userId" : "OWNER", "groupId" : "SELF" }); req.add(req.newFetchAlbumsRequest(idspec), 'ownerAlbums'); req.send(fetchAlbumsHandler); };
Handler:
// Callback function, executed when orkut finishes fetching the viewer's // public albums function fetchAlbumsHandler(resp) { var ownerAlbumsResp = resp.get('ownerAlbums'); if (!ownerAlbumsResp.hadError()) { var ownerAlbums = ownerAlbumsResp.getData(); // every album is an object inside the main object 'viewerAlbums' // we're going to create one big album which we're gonna feed to // the flash application ownerAlbums.each( function(album) { addAlbumToMasterAlbum(album.getId()); } ); } };
I don't want to go to into too much depth here concerning OpenSocial, since it is not the main subject of this tutorial. But to give you some clues about what's happening here, I'll explain it in general terms.
In the OpenSocial environment you'll have 2 roles, namely 'owner' and 'viewer'. Needless to say, the owner is the actual owner of the profile, then the viewer, well that's the person who's visiting. If you're going to request some data, you'll need to specify whose data you want; the viewer's or the owner's.
Next, if you perform a request, you'll need to specify a handler. This handler will be executed when the data comes back from the server. So here we request all the albums belonging to the owner's profile and when the data comes back from the server, we'll fetch all the photos from them.
Step 7: Retrieving the Photos
This step is very similar to the previous one. For every album we get back, we'll call a function to retrieve the items in it. Next, those items will be added to our array, which will be retrieved by the flash application later on.
// Fetches all photos from the album with the passed ID function addAlbumToMasterAlbum(albumId) { var req = opensocial.newDataRequest(); var idspec = opensocial.newIdSpec({ "userId" : "OWNER", "groupId" : "SELF" }); req.add(req.newFetchMediaItemsRequest(idspec, albumId), 'albumPhotos'); req.send(addAlbumToMasterAlbumHandler); };
Handler:
// Callback function, executed when orkut finishes fetching the // requested media items function addAlbumToMasterAlbumHandler(resp) { var albumPhotosResp = resp.get('albumPhotos'); if (!albumPhotosResp.hadError()) { var albumPhotos = albumPhotosResp.getData(); albumPhotos.each( function(photo) { masterAlbumArray.push(photo.getField('url')); } ); // now that all the Photo's are in // the masterAlbum, launch the Flash App launchFlashApp(); } };
Note: we're only getting the URL from the photo object, but there's more. For a full list of all the properties you can retrieve, like comments and statistics, go to opensocial.org.
Step 8: Placing the Flash App on the Screen
The last line of the handler function described above calls for the function "launchFlashApp".
launchFlashApp();
This does exactly what the name implies, it places the flash application inside the div tag defined at the bottom of the content node. When it's loaded, the flash application will call the function "retrieveMasterAlbum".
// show the flash app function launchFlashApp() { var content = '<embed src="http://85.17.135.135/~gerbster/projects/FlashTutsWidget/widget.swf" width="400" height="300" quality="high" bgcolor="000000" pluginspage="http://www.macromedia.com/go/getflashplayer" allowScriptAccess="always" flashvars="" type="application/x-shockwave-flash">'; document.getElementById("flashTutsWidgetDiv").innerHTML = content; };
The last line of this javascript part will call the fetchAlbums function when the widget is fully loaded.
// Execute fetchAlbums function when gadget loads gadgets.util.registerOnLoadHandler(fetchAlbums);
Step 9: Proxy.php
Now that the XML file is ready, we'll need to do one more thing before we can start with the flash part of the tutorial. The security restrictions of the flash player prevent it from accessing the bitmap data of an image loaded from another domain. Since we need that bitmap data for the effects we're going to add later on, we need a solution for this problem. The solution comes in the form of a little piece of PHP. This script will act as a proxy between the social media site and our flash application.
It's really simple, just pass a URL to this script and it will retrieve and display the content of that URL. This way, the flash application will think that the image comes from our own server and we don't have to worry about the security restriction.
<?php $post_data = $HTTP_RAW_POST_DATA; $header[] = "Content-type: text/xml"; $header[] = "Content-length: ".strlen($post_data); preg_match("/url=(.*)/",$_SERVER['REQUEST_URI'],$params); $ch = curl_init( $params[1] ); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_HTTPHEADER, $header); if ( strlen($post_data)>0 ) { curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); } $response = curl_exec($ch); if (curl_errno($ch)) { print curl_error($ch); } else { curl_close($ch); header("Content-type: text/xml; Content-length: ".strlen($response)); print $response; } ?>
Since this tutorial is not about PHP I've taken the liberty of using an existing script. It's written by Abdul Qabiz and it does exactly what we want, so no need to reinvent the wheel. More information about this script can be found here..
..and more information about the security restriction can be found here.
Step 10: Flash - Introduction
As mentioned above, the flash application will show all the photos in a slideshow and "plug-in classes" will perform the transitions. The first thing we need to do is to set up a new Flash project and assign its base class. I don't want to go into the details too much, because I presume this is not the first flash application you've built.

Step 11: Flash - Base Class
If you downloaded the source files of this project, you'll notice that in the root directory (the one that contains the .fla file), there's a folder called "widget" and inside that folder there's a file called "Widget.as". This is going to be our base class.
Below, you'll see the full code of the base class. Again, I won't discuss every detail of it, but in the next steps I'll highlight the most important aspects.
package widget { import flash.display.*; import flash.events.*; import flash.geom.*; import flash.net.URLRequest; import flash.system.*; import gs.TweenMax; import widget.transitions.*; import widget.OpenSocialPhotoLoader; public class Widget extends MovieClip { private var myProxy:String = "http://85.17.135.135/~gerbster/projects/FlashTutsWidget/proxy.php?url="; //private var myProxy:String = ""; private var myOSPL:OpenSocialPhotoLoader; private var photoArray:Array; private var myPhotoLoader:Loader; public var myPhotoHolder:MovieClip; private var fadeInOutTransition:FadeInOut; private var cubes3DTransition:Cubes3D; private var transitionsArray:Array = new Array(); private var currentPhotoIndex:Number = 0; private var transitionIndex:Number = NaN; private var photoTime:Number = 5; public function Widget():void { Security.allowDomain("orkut.com"); myPhotoHolder = new MovieClip(); myPhotoHolder.name = "photoHolder"; addChild(myPhotoHolder); initPhotoArray(); initTransitions(); if(photoArray.length > 0) { loadPhoto(currentPhotoIndex); } } private function initPhotoArray() { myOSPL = new OpenSocialPhotoLoader(); addChild(myOSPL); myOSPL.loadAlbum(); photoArray = myOSPL.masterAlbum; } private function initTransitions() { fadeInOutTransition = new FadeInOut(); cubes3DTransition = new Cubes3D(); transitionsArray.push(fadeInOutTransition); transitionsArray.push(cubes3DTransition); } private function loadPhoto(index:Number):void { if(index >= photoArray.length) { index = 0; } if(index == -1) { index = photoArray.length; } myPhotoLoader = new Loader(); var photoUrl:String = photoArray[index]; var photoUrlReq:URLRequest = new URLRequest(myProxy + photoUrl); myPhotoLoader.load(photoUrlReq); // if the photo is loaded, call showNextPhoto which will do the transition myPhotoLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, showNextPhoto); // increment currentPhotoIndex and call this function again in 5 seconds currentPhotoIndex = ++index; TweenMax.delayedCall( photoTime, loadPhoto, [currentPhotoIndex]); } private function showNextPhoto(e:Event) { // we want our photo's nicely scaled and centered var scaleValue:Number = calculateScaleValue(e.target.loader.contentLoaderInfo.width, e.target.loader.contentLoaderInfo.height); var translateX:Number = (stage.stageWidth - (e.target.loader.contentLoaderInfo.width * scaleValue)) / 2; var translateY:Number = (stage.stageHeight - (e.target.loader.contentLoaderInfo.height * scaleValue)) / 2; // ...so we use a matrix var resizeMatrix:Matrix = new Matrix(); resizeMatrix.scale(scaleValue, scaleValue); resizeMatrix.translate(translateX, translateY); var newPhotoBitmapData:BitmapData = new BitmapData(stage.stageWidth, stage.stageHeight, false, 0x000000); newPhotoBitmapData.draw(e.target.loader, resizeMatrix, null, null, null, true); // if there's a photo on stage do a transition if(myPhotoHolder.numChildren > 0) { var oldPhotoBitmapData:BitmapData = new BitmapData(stage.stageWidth, stage.stageHeight, false, 0x000000); oldPhotoBitmapData.draw(myPhotoHolder.getChildAt(0), null, null, null, null, true); // remove the old transition object if(myPhotoHolder.getChildAt(0) is ITransition) { transitionsArray[transitionIndex].destroyTransition(); } // remove the old transition object myPhotoHolder.removeChild(myPhotoHolder.getChildAt(0)); // pick a random transition transitionIndex = Math.round(Math.random() * (transitionsArray.length - 1)); myPhotoHolder.addChild(transitionsArray[transitionIndex]); transitionsArray[transitionIndex].doTransition(oldPhotoBitmapData, newPhotoBitmapData); } else { var newPhoto:Bitmap = new Bitmap(newPhotoBitmapData); myPhotoHolder.addChildAt(newPhoto, 0); } } private function calculateScaleValue(oldWidth:Number, oldHeight:Number):Number { var photoRatio:Number = oldHeight / oldWidth; var screenRatio:Number = stage.stageHeight / stage.stageWidth; if(photoRatio < screenRatio) { return stage.stageWidth / oldWidth; } else { return stage.stageHeight / oldHeight; } } } }
Step 12: Flash - Imports
Before we can get anything to run, we need to import some classes. The first five are native flash classes and aren't very exciting. Next we need to import TweenMax. TweenMax is a commonly used tween class, but we're going to use it here because is has a delayedTask function. We'll discuss this in more detail later on. Finally we need to import the transition classes and the OpenSocialPhotoLoader class.
import flash.display.*; import flash.events.*; import flash.geom.*; import flash.net.URLRequest; import flash.system.*; import gs.TweenMax; import widget.transitions.*; import widget.OpenSocialPhotoLoader;
Step 13: Flash - Constructor
As you probably know, the constructor will be called upon when the application starts, so here we'll do all the initializations. First we'll tell this application that Orkut.com is a safe domain. We need to do this or else we won't be able to call the javascript function to get the photo array. Next we'll add an empty MovieClip to the stage, which will act as a container for the photo's we're about to show. Thirdly, we'll call two init functions. These functions initialize the transitions and retrieve the photo array from javascript. The last 3 lines of code will check if there any photos in the array and if so, show the first photo.
public function Widget():void { Security.allowDomain("orkut.com"); myPhotoHolder = new MovieClip(); myPhotoHolder.name = "photoHolder"; addChild(myPhotoHolder); initPhotoArray(); initTransitions(); if(photoArray.length > 0) { loadPhoto(currentPhotoIndex); } }
Step 14: Flash - InitPhotoArray
The initPhotoArray function will make a new instance of the OpenSocialPhotoLoader class, add to the stage, then call the loadAlbum function inside that class and finally assign the masterAlum variable inside that class to our local photoArray variable (which we declared at the top of the class). In the next step we'll discuss the OpenSocialPhotoLoader class in more detail.
private function initPhotoArray() { myOSPL = new OpenSocialPhotoLoader(); addChild(myOSPL); myOSPL.loadAlbum(); photoArray = myOSPL.masterAlbum; }
Step 15: Flash - OpenSocialPhotoLoader
To get the obvious question out of the way, why use a separate class to load the photos? Well, if you want to reuse this application in a different environment, you won't need to completely rewrite the base class, just build a new Loader class and implement it. Hail OOP!
This class is very simple though. It contains a loadAlbum function, which will use the external interface class from the AS3 library to call the retrieveMasterAlbum function from javascript. This function returns an object which contains all the URL's. Finally, all the items in that object will be added to a local Array.
package widget { import flash.external.ExternalInterface; import flash.display.MovieClip; public class OpenSocialPhotoLoader extends MovieClip { private var rawMasterAlbum:Object; public var masterAlbum:Array = new Array(); public function OpenSocialPhotoLoader() { } public function loadAlbum():void { if (ExternalInterface.available) { try { rawMasterAlbum = ExternalInterface.call("retrieveMasterAlbum"); for each (var value:* in rawMasterAlbum) { masterAlbum.push(value.toString()); } } catch (error:SecurityError) { } catch (error:Error) { } } else { } } } }
Since calling a function from javascript is prone to all kinds of errors, we'll use a try catch construction here.
Step 16: Flash - initTransitions
Let's get back to our base class. After initializing the photo array, we're going to initialize the various transitions we'll build later on. In the top of this class we've declared two transitions and a transition array. What we do here is very simple; make an instance of the transition class and push it into the array.
private function initTransitions() { fadeInOutTransition = new FadeInOut(); cubes3DTransition = new Cubes3D(); transitionsArray.push(fadeInOutTransition); transitionsArray.push(cubes3DTransition); }
Why we do this, I'll tell you later on.
Step 17: Flash - loadPhoto
The last 3 lines of the constructor were responsible for "kick starting" the application. If there are any photos in the array, loadPhoto is called with currentPhotoIndex as its argument. The variable currentPhotoIndex is declared at the top of the class and it holds the index number of the current photo on the screen. It's declared with default value 0. Let's examine the function and then discuss it.
private function loadPhoto(index:Number):void { if(index >= photoArray.length) { index = 0; } if(index == -1) { index = photoArray.length; } myPhotoLoader = new Loader(); var photoUrl:String = photoArray[index]; var photoUrlReq:URLRequest = new URLRequest(myProxy + photoUrl); myPhotoLoader.load(photoUrlReq); // if the photo is loaded, call showNextPhoto which will do the transition myPhotoLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, showNextPhoto); // increment currentPhotoIndex and call this function again in 5 seconds currentPhotoIndex = ++index; TweenMax.delayedCall( photoTime, loadPhoto, [currentPhotoIndex]); }
The first two if statements will check if the currentPhotoIndex is out of bounds and if so, correct it. The next step will be actually loading the picture. We use our proxy script here to bypass the flash security restriction. The myProxy string is also declared at the top of the class. We'll add an eventListener to this loader, which will execute the showNextPhoto function in the event that the photo is fully loaded.
As a final step we'll increase the currentPhotoIndex and use the TweenMax delayedCall function to re-call this function in 5 seconds. More information about the delayedCall function can be found on the homepage of TweenMax: http://blog.greensock.com/tweenmaxas3/.
Step 18: Flash - ShowNextPhoto
This function is the most important function of them all. In short, it will resize and position the loaded photo correctly, hand it down to the transitions classes, then finally make sure it's visible on the screen. This function is called when a photo is fully loaded. I'll break this function down into 4 parts.
Editor's Note: As occasionally happens, something within this showNextPhoto function didn't want to play nicely with our syntax highlighter. As it won't display on the page, I've made the function available for download. Sorry for any inconvenience, Ian.
The first thing we're going to do is to rescale the image and position it correctly. No matter what the dimensions of the photo are, it's being centered and placed on a black background. To do this we use a matrix. Although it may seem scary at first, using a matrix is very efficient and not that difficult. More information on matrixes can be found in the AS3 API on adobe.com. We use a small function called calculateScaleValue to determine the scale factor we need for resizing the photo.
if(myPhotoHolder.numChildren > 0)
Next we'll determine if there are any pictures on the screen yet. We need to do this because the first picture needs to be shown directly without a transition (there's nothing to transition from).
if(myPhotoHolder.numChildren > 0) { var oldPhotoBitmapData:BitmapData = new BitmapData(stage.stageWidth, stage.stageHeight, false, 0x000000); oldPhotoBitmapData.draw(myPhotoHolder.getChildAt(0), null, null, null, null, true); // remove the old transition object if(myPhotoHolder.getChildAt(0) is ITransition) { transitionsArray[transitionIndex].destroyTransition(); } // remove the old transition object myPhotoHolder.removeChild(myPhotoHolder.getChildAt(0)); // pick a random transition transitionIndex = Math.round(Math.random() * (transitionsArray.length - 1)); myPhotoHolder.addChild(transitionsArray[transitionIndex]); transitionsArray[transitionIndex].doTransition(oldPhotoBitmapData, newPhotoBitmapData); }
However, if there's a picture already, we first need to remove the old transition object, then pick a new one from the transitions array by using the random function, add this to the screen and finally order the transition class to "doTransition". As arguments we feed the old, current bitmap data and the new bitmap data. Note here that we use the ITransition interface to check if the child on stage is a transition class or not. More information about this interface will come in a few steps.
else { var newPhoto:Bitmap = new Bitmap(newPhotoBitmapData); myPhotoHolder.addChildAt(newPhoto, 0); }
If not, we just put the photo on the screen.
Step 19: Flash - The Transition Plugins
As mentioned earlier, the transition effects are being handled by plug-ins. The plug-ins are classes, which implement an interface called ITransition and will handle the visual transition from the old photo to the new. I've made two of these classes, one simple fade in/fade out transition and a complex one, which shows 3d rotating cubes. In the next few steps we'll discuss this interface and the 2 transitions.
Step 20: Flash - The interface
If this is your first encounter with an interface, I'd advise you to search the web and read up about it. In short, an interface is an OOP concept and it defines some specifications for the various plug-in classes. We'll use it here to make sure that our transition classes include certain functions. The code of this interface is very simple:
package widget.transitions { import flash.display.*; public interface ITransition { function doTransition(oldPhotoBitmapData:BitmapData, newPhotoBitmapData:BitmapData):void; function destroyTransition():void } }
Both our transition classes will implement this interface, so both will need to have a doTransition and a destroyTransition function or else we'll get compiler errors. Next to that, we'll use this interface to check if an object inside the photo container on the stage is a transition class.
Step 21: Flash - The FadeInOut class
The first of our two transition classes is a fade in fade out effect. It's a simple class, which extends the MovieClip class (because we're adding it as a child to our photo container in the base class) and it implements the ITransition interface. It's doTransition function will place both photos on the screen, make the new one transparent and with the use of TweenMax fade it in and out . This takes 1 second, which is defined by the variable playtime in the top of the class.
package widget.transitions { import flash.display.*; import gs.TweenMax; import gs.easing.*; public class FadeInOut extends MovieClip implements ITransition { // playtime in seconds public var playTime:Number = 1; public function FadeInOut() { } public function doTransition(oldPhotoBitmapData:BitmapData, newPhotoBitmapData:BitmapData):void { var oldPhoto:Bitmap = new Bitmap(oldPhotoBitmapData); this.addChild(oldPhoto); var newPhoto:Bitmap = new Bitmap(newPhotoBitmapData); newPhoto.alpha = 0; this.addChild(newPhoto); TweenMax.to(oldPhoto, playTime, {alpha: 0, ease:Quad.easeIn}); TweenMax.to(newPhoto, playTime, {alpha: 1, ease:Quad.easeOut}); } public function destroyTransition():void { // no need to destroy anything. } } }
Since there is no real need to do anything after the transition is done the destroyTransition function is left empty. We could use it to remove the old photo from the stage but since that has no effect on the performance we just leave it this way. Remember we have to implement it, because of the interface we use.
Step 22: Flash - The Cubes3D class
The second transition class is somewhat more complex then the previous one. It will show a grid of rotating cubes. These cubes will have both photos as textures. To create this effect we'll use papervision3d.
Again, the class will extend the MovieClip and implements the ITransition interface. In addition to the doTranstion and destroyTransition functions there are some other functions. I'll discuss them in the next several steps. The complete code of this transition class looks like this:
package widget.transitions { import flash.display.*; import flash.events.* import flash.geom.*; import org.papervision3d.materials.*; import org.papervision3d.materials.utils.*; import org.papervision3d.objects.primitives.Cube; import org.papervision3d.objects.DisplayObject3D; import org.papervision3d.cameras.Camera3D; import org.papervision3d.render.BasicRenderEngine; import org.papervision3d.scenes.Scene3D; import org.papervision3d.view.Viewport3D; import gs.TweenMax; import gs.easing.*; public class Cubes3D extends MovieClip implements ITransition { private var viewport:Viewport3D; // The Viewport private var renderer:BasicRenderEngine; // Rendering engine private var scene:Scene3D; // A Scene private var camera:Camera3D; private var cubeSize:Number = 100; private var horizontalAmount:Number = 4; private var verticalAmount:Number = 3; public function Cubes3D() { } public function doTransition(oldPhotoBitmapData:BitmapData, newPhotoBitmapData:BitmapData):void { init3d(); for(var v:int = 0; v < verticalAmount; v++) { for(var h:int = 0; h < horizontalAmount; h++) { var oldPhotoPart:BitmapData = new BitmapData(cubeSize, cubeSize); oldPhotoPart.copyPixels(oldPhotoBitmapData ,new Rectangle(h*cubeSize, v*cubeSize, cubeSize, cubeSize), new Point(0,0)); var newPhotoPart:BitmapData = new BitmapData(cubeSize, cubeSize); newPhotoPart.copyPixels(newPhotoBitmapData ,new Rectangle(h*cubeSize, v*cubeSize, cubeSize, cubeSize), new Point(0,0)); var frontMaterial:BitmapMaterial = new BitmapMaterial(oldPhotoPart); var backMaterial:BitmapMaterial = new BitmapMaterial(newPhotoPart); var ml:MaterialsList = new MaterialsList({ front: frontMaterial, right: new ColorMaterial(0x000000, 0), left: new ColorMaterial(0x000000, 0), back: backMaterial, top: new ColorMaterial(0x000000, 0), bottom: new ColorMaterial(0x000000, 0) }); var cube:Cube = new Cube(ml, cubeSize, cubeSize, cubeSize, 4, 4, 4); cube.name = "cube_"+v+"_"+h; cube.x = -(h * cubeSize) + (stage.stageWidth / 2) - (cubeSize / 2); cube.y = -(v * cubeSize) + (stage.stageHeight / 2) - (cubeSize / 2); scene.addChild(cube); } } // cubes are loaded, so start rotating! for(var i:int = 0; i < verticalAmount; i++) { for(var j:int = 0; j < horizontalAmount; j++) { var cubeNumber:Number = i*verticalAmount + j; viewport.getChildLayer(scene.getChildByName("cube_"+i+"_"+j), true).layerIndex = cubeNumber; but not all at once, so use a delayedcall TweenMax.delayedCall(cubeNumber/10, rotateCube, ["cube_"+i+"_"+j]); } } } private function rotateCube(cubeName:String) { TweenMax.to(scene.getChildByName(cubeName), 0.75, {rotationY: -180, ease:Linear.easeNone, onComplete: sortLayers, onCompleteParams: [cubeName]}); } private function sortLayers(cubeName:String) { viewport.getChildLayer(scene.getChildByName(cubeName), true).layerIndex = 1; } private function init3d() { viewport = new Viewport3D(stage.stageHeight, stage.stageWidth, true); renderer = new BasicRenderEngine(); scene = new Scene3D(); camera = new Camera3D(); var cameraPoint:DisplayObject3D = new DisplayObject3D(); cameraPoint.x = 0; cameraPoint.y = 0; cameraPoint.z = 0; camera.z = 2450; camera.zoom = 120; camera.focus = 20; camera.target = cameraPoint; addEventListener(Event.ENTER_FRAME, render); addChild(viewport); } private function render(e:Event):void { renderer.renderScene(scene, camera, viewport); } public function destroyTransition():void { removeEventListener(Event.ENTER_FRAME, render); removeChild(viewport); viewport = null; renderer = null; scene = null; camera = null; cameraPoint = null; } } }
Step 23: Flash - Cubes3D - init3d
The first step of the doTransition function is to call the init3d function. This function initializes the various components needed by papervision3d. No rocket science here. To flatten the 3d effect, we place the camera at some distance and use the zoom property to zoom in on the cubes. Next we'll add an event listener which renders the whole scene.
private function init3d() { viewport = new Viewport3D(stage.stageHeight, stage.stageWidth, true); renderer = new BasicRenderEngine(); scene = new Scene3D(); camera = new Camera3D(); var cameraPoint:DisplayObject3D = new DisplayObject3D(); cameraPoint.x = 0; cameraPoint.y = 0; cameraPoint.z = 0; camera.z = 2450; camera.zoom = 120; camera.focus = 20; camera.target = cameraPoint; addEventListener(Event.ENTER_FRAME, render); addChild(viewport); }
Step 24: Flash - Cubes3D - doTransition
When papervision3d is correctly initialized, the doTransition function continues and it will place a total of 12 cubes in the 3d scene. These cubes all have a part of the old photo and the new photo on their front and back. These pieces fit together as a puzzle. To identify the individual cubes they all get a name that gives away their position in the grid.
public function doTransition(oldPhotoBitmapData:BitmapData, newPhotoBitmapData:BitmapData):void { init3d(); for(var v:int = 0; v < verticalAmount; v++) { for(var h:int = 0; h < horizontalAmount; h++) { var oldPhotoPart:BitmapData = new BitmapData(cubeSize, cubeSize); oldPhotoPart.copyPixels(oldPhotoBitmapData ,new Rectangle(h*cubeSize, v*cubeSize, cubeSize, cubeSize), new Point(0,0)); var newPhotoPart:BitmapData = new BitmapData(cubeSize, cubeSize); newPhotoPart.copyPixels(newPhotoBitmapData ,new Rectangle(h*cubeSize, v*cubeSize, cubeSize, cubeSize), new Point(0,0)); var frontMaterial:BitmapMaterial = new BitmapMaterial(oldPhotoPart); var backMaterial:BitmapMaterial = new BitmapMaterial(newPhotoPart); var ml:MaterialsList = new MaterialsList({ front: frontMaterial, right: new ColorMaterial(0x000000, 0), left: new ColorMaterial(0x000000, 0), back: backMaterial, top: new ColorMaterial(0x000000, 0), bottom: new ColorMaterial(0x000000, 0) }); var cube:Cube = new Cube(ml, cubeSize, cubeSize, cubeSize, 4, 4, 4); cube.name = "cube_"+v+"_"+h; cube.x = -(h * cubeSize) + (stage.stageWidth / 2) - (cubeSize / 2); cube.y = -(v * cubeSize) + (stage.stageHeight / 2) - (cubeSize / 2); scene.addChild(cube); } }
Next, we'll start rotating them. Again we use the delayedCall function of TweenMax to add a little delay in order to get a sweeping effect.
// cubes are loaded, so start rotating! for(var i:int = 0; i < verticalAmount; i++) { for(var j:int = 0; j < horizontalAmount; j++) { var cubeNumber:Number = i*verticalAmount + j; viewport.getChildLayer(scene.getChildByName("cube_"+i+"_"+j), true).layerIndex = cubeNumber; // but not all at once, so use a delayedcall TweenMax.delayedCall(cubeNumber/10, rotateCube, ["cube_"+i+"_"+j]); } } }
When the rotation is done, we'll call the sortLayers function to make sure the cubes are layered correctly.
private function sortLayers(cubeName:String) { viewport.getChildLayer(scene.getChildByName(cubeName), true).layerIndex = 1; }
Step 25: Flash - Cubes3D - destroyTransition
In contrast to the fade in/out class we'll need to do something with destroyTransition function here. We'll use it to remove the event listener that handles the rendering and the various other papervision3d components. Why? Well if we don't do this the performance of the application will drop dramatically every time this transition is used. Since the transition object isn't destroyed in the base class when it finishes, it will create a new papervision3d environment every time it's called, causing serious performance issues.
public function destroyTransition():void { removeEventListener(Event.ENTER_FRAME, render); removeChild(viewport); viewport = null; renderer = null; scene = null; camera = null; cameraPoint = null; }
Step 26: Putting it all online.
Now that we have everything in place, it's time to put it all online. Make sure that all the paths inside the widget.xml file and the path to proxy script inside the base class are correct.
After uploading then testing the proxy script and flash application it's time to add your widget to your Orkut profile. To do this, navigate to Orkut.com, login and navigate to: http://sandbox.orkut.com/. Make sure that you have developer status. While you're in the sandbox, you have the ability to add your own applications. To do this, click on "edit" next to "Apps" in the left column and you will see an extra input field. Fill in the URL of your widget.xml file, press "add application" and you're done.
If everything is correct you'll see a very nice photo slider!


Final Note.
As a final word, I want to give you some tips concerning building and testing OpenSocial applications. To make things somewhat easier, the OpenSocial community just released a tool called OpenSocial Development Environment (OSDE) in which you can simulate a social network site (it's a plug-in for Eclipse). However, to this day it only supports OpenSocial API version 0.7, which was not enough for this tutorial.
To debug the javascript part of an application you can use a built-in function called Gadgets.util.log, which is in essence just a wrapper for console.log (firebug / firefox).
In order to test your flash application, it can be handy to use a different PhotoLoader class. One that just loads an array of pictures from your HD, because believe me, you don't want to upload the xml file and flash app everytime you want to test if an transition works properly :-)
Thanks for reading, I hope you learned something!
Envato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post