Following my tutorials on PureMVC and the YouTube Player API I thought it would be a good idea to show you how to use the YouTube GData API for searching for and playing videos. So I've created a little tutorial that combines your new PureMVC and YouTube Player skills into a nice and simple search and play gadget.
Before We Start...
This is a tutorial for intermediate to advanced Actionscripters and it would be handy if you first read my PureMVC and YouTube Player API tutorials.
Step 1: Why Use PureMVC?
You may be wondering why I've chosen to use PureMVC. Well, I think it's great! And it's a great tool that allows you to build something small and then expand it into a big application - it's all about scalability. So while we create this app, it's worth noting that this can be used as a gadget to be included in multiple places such as iGoogle, blogs and even gadget ads.
Step 2: The Set Up
Fire up your favourite IDE, whether it's Flex Builder, FDT, FlashDevelop or TextMate and create a new Actionscript project. We're also going to be creating a SWC so it's handy if you've got a copy of Flash (you can download a trail from Adobe's web site).
Additionally, we're going to be using GreenSock's TweenLite class and not forgetting the PureMVC codebase.
Note: In the the first few steps we're going to be setting up a skeleton for PureMVC. An explanation of the first steps is available on my PureMVC tutorial.
Step 3: Creating the Base App
Just like with any PureMVC app, we need to set up our base app. I'm going to start by just creating a simple background for the app and then running the start up command within the facade, so create a new file called "App.as" within "src/":
package
{
import com.flashtuts.ApplicationFacade;
import flash.display.GradientType;
import flash.display.Sprite;
import flash.geom.Matrix;
import flash.text.Font;
[SWF( width='600', height='400', frameRate='30', backgroundColor='#000000' )]
public class App extends Sprite
{
[Embed( systemFont='Arial', fontName='Arial', mimeType='application/x-font' )]
private var arialFont:Class;
public function App()
{
init();
}
private function init():void
{
var mat:Matrix = new Matrix();
var bg:Sprite = new Sprite();
mat.createGradientBox( stage.stageWidth, stage.stageHeight, Math.PI * .5 );
bg.graphics.beginGradientFill( GradientType.LINEAR, [ 0x333333, 0x000000 ], [ 1, 1 ], [ 0, 255 ], mat );
bg.graphics.drawRect( 0, 0, stage.stageWidth, stage.stageHeight );
bg.graphics.endFill();
addChild( bg );
Font.registerFont( arialFont );
ApplicationFacade.getInstance().startup( this );
}
}
}
Step 4: Creating the Facade
We now simply create the facade, nothing special, just like the facade we created in the PureMVC tutorial, so create a new file called "ApplicationFacade.as" inside "src/com/flashtuts/":
package com.flashtuts
{
import com.flashtuts.controller.StartupCommand;
import org.puremvc.as3.interfaces.IFacade;
import org.puremvc.as3.patterns.facade.Facade;
import org.puremvc.as3.patterns.observer.Notification;
public class ApplicationFacade extends Facade implements IFacade
{
public static const NAME:String = 'ApplicationFacade';
public static const STARTUP:String = NAME + 'StartUp';
public static function getInstance():ApplicationFacade
{
return (instance ? instance : new ApplicationFacade()) as ApplicationFacade;
}
override protected function initializeController():void
{
super.initializeController();
registerCommand( STARTUP, StartupCommand );
}
public function startup(stage:Object):void
{
sendNotification( STARTUP, stage );
}
override public function sendNotification(notificationName:String, body:Object=null, type:String=null):void
{
trace( 'Sent ' + notificationName );
notifyObservers( new Notification( notificationName, body, type ) );
}
}
}
Step 5: Creating the Startup Command
Now we'll create our startup command and make sure that we register our proxy and application mediator. Create a file called "StartupCommand.as" in "src/com/flashtuts/controller/":
package com.flashtuts.controller
{
import com.flashtuts.model.DataProxy;
import com.flashtuts.view.ApplicationMediator;
import org.puremvc.as3.interfaces.ICommand;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.command.SimpleCommand;
public class StartupCommand extends SimpleCommand implements ICommand
{
override public function execute(notification:INotification):void
{
facade.registerProxy( new DataProxy() );
facade.registerMediator( new ApplicationMediator( notification.getBody() as App ) );
}
}
}
Step 6: Creating the Proxy
We now need to set up our proxy, so let's create a file called "DataProxy.as" in "src/com/flashtuts/model/":
package com.flashtuts.model
{
import com.flashtuts.model.vo.DataVO;
import org.puremvc.as3.interfaces.IProxy;
import org.puremvc.as3.patterns.proxy.Proxy;
public class DataProxy extends Proxy implements IProxy
{
public static const NAME:String = 'DataProxy';
public function DataProxy()
{
super( NAME, new DataVO() );
} new Proxy
public function get vo():DataVO
{
return data as DataVO;
}
}
}
We'll be coming back to the proxy later in the tutorial as we'll be using it to load the data from GData and store it in the VO.
Step 7: Creating the VO
We're going to use the VO to store data from our GData query so that if the user searches for a keyword more than once, we'll just load the data from the VO rather than creating another request to the GData API. Create a file called "DataVO.as" in "src/com/flashtuts/model/vo/":
package com.flashtuts.model.vo
{
import flash.utils.Dictionary;
public class DataVO
{
public var gdataURL:String = 'http://gdata.youtube.com/feeds/api/videos?orderby=published&max-results=15&v=2&q=';
public var queryResults:Dictionary = new Dictionary();
}
}
Have a quick read of the GData API documentation you'll see that the var "gdataURL" shows that we're going to get 15 results and we've left the URL open so that we can put the query string at the back of the request's URL.
Step 8: Creating the Application Mediator
The final part of our PureMVC skeleton is our application mediator. We'll create it and then get it to register our first view and mediator, the "ProgressView" and "ProgressViewMediator", so create a file called "ProgressViewMediator.as" within "src/com/flashtuts/view/":
package com.flashtuts.view
{
import org.puremvc.as3.interfaces.IMediator;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.mediator.Mediator;
public class ApplicationMediator extends Mediator implements IMediator
{
public static const NAME:String = 'ApplicationMediator';
public function ApplicationMediator(viewComponent:Object=null)
{
super( NAME, viewComponent );
}
override public function onRegister():void
{
}
override public function listNotificationInterests():Array
{
return [
];
}
override public function handleNotification(notification:INotification):void
{
var name:String = notification.getName();
var body:Object = notification.getBody();
switch ( name )
{
}
}
}
}
Step 9: Creating our Progress View
We'll simply start with a similar progress view to the one we made in the PureMVC tutorial, so create the file "ProgressView.as" within "src/com/flashtuts/view/component/":
package com.flashtuts.view.component
{
import flash.display.Sprite;
import gs.TweenLite;
public class ProgressView extends Sprite
{
public static const NAME:String = 'ProgressView';
public static const SHOW:String = NAME + 'Show';
public static const HIDE:String = NAME + 'Hide';
private var asset:LoaderAsset;
public function ProgressView()
{
init();
}
private function init():void
{
asset = new LoaderAsset();
asset.stop();
asset.x = 275;
asset.y = 175;
addChild( asset );
}
public function show():void
{
asset.play();
TweenLite.to( this, .5, { autoAlpha: 1 } );
}
public function hide():void
{
asset.stop();
TweenLite.to( this, .5, { autoAlpha: 0 } );
}
}
}
You'll notice that we're using a new class called "LoaderAsset". This, in fact, is a movie clip that we'll create in the Flash IDE and then use within our progress view instead of just showing a percentage.
Step 10: Creating the SWC
Fire up Flash IDE (if you haven't got the Flash IDE, you can download the trial from Adobe's web site) and create a new AS3 file:
Before we continue, we need to make sure Flash knows that we want to target Flash Player 9 and that we're exporting a SWC, so select 'Flash Player 9' from the drop down at the top and tick the box next to 'Export SWC' under the heading 'SWF Settings':
Now we'll create a simple loader. I've chosen to go for a spinning one, but it's up to you what you do. Create a circle and punch a hole in it:
Once you've put a hole in your circle, apply a radial gradient, move it to the right of the circle. Then copy half of the circle, paste it on top of the current circle and apply a solid fill to it. Done. Now, next thing is to make it rotate, so let's create a tween:
I've got my tween running for 15 frames, you can have yours fast, the same or slower. Now we simply set it to rotate once:
The last thing to do is wrap this all up in a movie clip. Once you've done that, open up the properties window for that movie clip and set it to export for Actionscript:
As you can see I've called it 'LoaderAsset', this linkage name will be the class name you'll use in your view, so back to our "ProgressView". Make sure you add your asset in the same way I have in the class:
package com.flashtuts.view.component
{
import flash.display.Sprite;
import gs.TweenLite;
public class ProgressView extends Sprite
{
public static const NAME:String = 'ProgressView';
public static const SHOW:String = NAME + 'Show';
public static const HIDE:String = NAME + 'Hide';
private var asset:LoaderAsset;
public function ProgressView()
{
init();
}
private function init():void
{
asset = new LoaderAsset();
asset.stop();
asset.x = 275;
asset.y = 175;
addChild( asset );
}
public function show():void
{
asset.play();
TweenLite.to( this, .5, { autoAlpha: 1 } );
}
public function hide():void
{
asset.stop();
TweenLite.to( this, .5, { autoAlpha: 0 } );
}
}
}
Step 11: Setting up the Progress View Mediator
Our progress view mediator will be similar to the one we created in the PureMVC tutorial with the exception that there's no update as we just have a spinning wheel rather than a percentage sign. Create the file "ProgressViewMediator.as" in "src/com/flashtuts/view/":
package com.flashtuts.view
{
import com.flashtuts.view.component.ProgressView;
import org.puremvc.as3.interfaces.IMediator;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.mediator.Mediator;
public class ProgressViewMediator extends Mediator implements IMediator
{
public static const NAME:String = 'ProgressViewMediator';
private var progressView:ProgressView;
public function ProgressViewMediator(viewComponent:Object=null)
{
super( NAME, viewComponent );
}
override public function onRegister():void
{
progressView = new ProgressView();
progressView.hide();
viewComponent.addChild( progressView );
}
override public function listNotificationInterests():Array
{
return [
ProgressView.SHOW,
ProgressView.HIDE
];
}
override public function handleNotification(notification:INotification):void
{
var name:String = notification.getName();
var body:Object = notification.getBody();
switch ( name )
{
case ProgressView.SHOW:
progressView.show();
break;
case ProgressView.HIDE:
progressView.hide();
break;
}
}
}
}
You're free to use whatever loader you want, but there's a problem with YouTube's GData. You see when Flash Player makes a call, most servers send the content-length of the request to the loader, thus allowing Flash Player to know how many bytes it's going to load and allowing us to work out a percentage. However, YouTube GData doesn't seem to send this content-length and whenever you try to work out a percentage, Flash Player finds itself dividing the 'bytesLoaded' with zero. Shame.
Step 12: Creating the Search View
Next thing we need to do is create our search view. This will contain a search box where the user will enter their query and then a button for them to press which runs the query. Additionally, we'll be using an event listener on the search box so that if the user were to hit 'enter' the query would still be run. Create a file called "SearchView.as" within "src/com/flashtuts/view/component/":
package com.flashtuts.view.component
{
import flash.display.Sprite;
import flash.events.DataEvent;
import flash.events.FocusEvent;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.text.TextFieldType;
import flash.text.TextFormat;
import flash.ui.Keyboard;
import mx.utils.StringUtil;
public class SearchView extends Sprite
{
public static const NAME:String = 'SearchView';
public static const SHOW:String = NAME + 'Show';
public static const HIDE:String = NAME + 'Hide';
public static const SEARCH_RUN:String = NAME + 'SearchRun';
public static const SEARCH_RESULTS:String = NAME + 'SearchResults';
public static const SEARCH_FIELD_STRING:String = 'Keywords...';
private var searchField:TextField;
public function SearchView()
{
init();
}
private function init():void
{
var textFormat:TextFormat = new TextFormat();
var boxBg:Sprite = new Sprite();
var boxCopy:TextField = new TextField();
var boxButton:Sprite = new ButtonAsset();
textFormat.color = 0x000000;
textFormat.font = 'Arial';
textFormat.size = 10;
boxBg.graphics.beginFill( 0xFFFFFF );
boxBg.graphics.drawRoundRectComplex( 0, 0, 300, 35, 0, 0, 5, 5 );
boxBg.graphics.endFill();
boxBg.x = 150;
addChild( boxBg );
boxCopy.autoSize = TextFieldAutoSize.LEFT;
boxCopy.defaultTextFormat = textFormat;
boxCopy.embedFonts = true;
boxCopy.text = 'Search for:';
boxCopy.x = 10;
boxCopy.y = ( boxBg.height / 2 ) - ( boxCopy.height / 2 );
boxBg.addChild( boxCopy );
searchField = new TextField();
searchField.border = true;
searchField.borderColor = 0x666666;
searchField.defaultTextFormat = textFormat;
searchField.embedFonts = true;
searchField.multiline = false;
searchField.text = SEARCH_FIELD_STRING;
searchField.type = TextFieldType.INPUT;
searchField.width = 185;
searchField.height = 16;
searchField.x = boxCopy.x + boxCopy.width + 10;
searchField.y = ( boxBg.height / 2 ) - ( searchField.height / 2 );
searchField.addEventListener( FocusEvent.FOCUS_IN, handleSearchFieldFocusIn );
searchField.addEventListener( FocusEvent.FOCUS_OUT, handleSearchFieldFocusOut );
searchField.addEventListener( KeyboardEvent.KEY_UP, handleSearchFieldKeyUp );
boxBg.addChild( searchField );
boxButton.buttonMode = true;
boxButton.x = boxCopy.x + boxCopy.width + searchField.width + 20;
boxButton.y = ( boxBg.height / 2 ) - ( boxButton.height / 2 );
boxButton.addEventListener( MouseEvent.CLICK, searchRun );
boxBg.addChild( boxButton );
}
private function handleSearchFieldFocusIn(e:FocusEvent):void
{
if ( searchField.text === SEARCH_FIELD_STRING )
{
searchField.text = '';
}
}
private function handleSearchFieldFocusOut(e:FocusEvent):void
{
if ( StringUtil.trim( searchField.text ) == '' )
{
searchField.text = SEARCH_FIELD_STRING;
}
}
private function handleSearchFieldKeyUp(e:KeyboardEvent):void
{
if ( e.keyCode === Keyboard.ENTER )
{
searchRun();
}
}
private function searchRun(e:*=null):void
{
var query:String = StringUtil.trim( searchField.text );
if ( query != '' && query != SEARCH_FIELD_STRING )
{
dispatchEvent( new DataEvent( SEARCH_RUN, true, false, query ) );
}
}
}
}
This code should be nothing new to an ActionScripter, but here's a quick run through: we first set the text format, then create the background box (I'm using Sprite.drawRoundRectComplex() to draw the box), I then set up the label, create the text input box and then import an asset from the SWC for the search button.
As I mentioned before, I've added an event listener to the text input box and to the submit button. This means that the user is able to hit enter or click on the submit button to run the query. The 'handleSearchFieldKeyUp()' function checks to make sure if the enter key has been hit, if it has, it then runs the search. This is also good practice for usability issues as people are often used to hitting enter rather than having to click a button.
Step 13: Creating the Search View Mediator
The search view mediator won't have too much there as it's not listening to any events, rather it's sending them to the facade. Start then, by creating a file called "SearchViewMediator.as" inside "src/com/flashtuts/view/":
package com.flashtuts.view
{
import com.flashtuts.view.component.SearchView;
import flash.events.DataEvent;
import org.puremvc.as3.interfaces.IMediator;
import org.puremvc.as3.patterns.mediator.Mediator;
public class SearchViewMediator extends Mediator implements IMediator
{
public static const NAME:String = 'SearchViewMediator';
private var searchView:SearchView;
public function SearchViewMediator(viewComponent:Object=null)
{
super( NAME, viewComponent );
}
override public function onRegister():void
{
searchView = new SearchView();
searchView.addEventListener( SearchView.SEARCH_RUN, sendEvent );
viewComponent.addChild( searchView );
}
private function sendEvent(e:DataEvent):void
{
sendNotification( SearchView.SEARCH_RUN, { query: e.data } );
}
}
}
If you look at the view, you'll notice that it'll dispatch a "DataEvent" up to the mediator. Within our mediator we listen to that and then send a notification to your facade. This notification will be picked up by a command which will interact with the proxy and then send an event with the results.
We've reached the stage when our view will be sending our query to the facade, so we now need to create a command to pass this query to our proxy and then update our proxy's code. It'll then run the query, store it in the VO, pass it back to a mediator and then the view.
Step 14: Creating the Data Command
We're going to start by updating the facade so that it will pass the notification "SearchView.SEARCH_RUN" to our data command. Open up "ApplicationFacade.as" and simply add another "registerCommand" within the facade's "initializeController()" function:
package com.flashtuts
{
import com.flashtuts.controller.DataCommand;
import com.flashtuts.controller.StartupCommand;
import com.flashtuts.view.component.SearchView;
import org.puremvc.as3.interfaces.IFacade;
import org.puremvc.as3.patterns.facade.Facade;
import org.puremvc.as3.patterns.observer.Notification;
public class ApplicationFacade extends Facade implements IFacade
{
public static const NAME:String = 'ApplicationFacade';
public static const STARTUP:String = NAME + 'StartUp';
public static function getInstance():ApplicationFacade
{
return (instance ? instance : new ApplicationFacade()) as ApplicationFacade;
}
override protected function initializeController():void
{
super.initializeController();
registerCommand( STARTUP, StartupCommand );
registerCommand( SearchView.SEARCH_RUN, DataCommand ); // < here!
}
public function startup(stage:Object):void
{
sendNotification( STARTUP, stage );
}
override public function sendNotification(notificationName:String, body:Object=null, type:String=null):void
{
trace( 'Sent ' + notificationName );
notifyObservers( new Notification( notificationName, body, type ) );
}
}
}
Now we need to create the "DataCommand.as" file in "src/com/flashtuts/controller/":
package com.flashtuts.controller
{
import com.flashtuts.model.DataProxy;
import com.flashtuts.view.component.SearchView;
import org.puremvc.as3.interfaces.ICommand;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.command.SimpleCommand;
public class DataCommand extends SimpleCommand implements ICommand
{
override public function execute(notification:INotification):void
{
var name:String = notification.getName();
var body:Object = notification.getBody();
switch ( name )
{
case SearchView.SEARCH_RUN:
proxy.searchRun( body.query );
break;
}
}
private function get proxy():DataProxy
{
return facade.retrieveProxy( DataProxy.NAME ) as DataProxy;
}
}
}
This is quite a simple controller, it's listening in on one notification then just passing it to the proxy. Again, we reference the proxy using a get function, this means that we're using the facade's instance rather than re-declaring the proxy.
Step 15: Finishing the Proxy
Now that our view bubbles up the event to the facade and the facade passes it to our data command, we need our proxy to do the dirty work, so let's load some XML.
Unless you've worked with GData before, it's worth noting that the XML it passes back is in Atom format and it's got lots of namespaces within it. In AS2 using namespaces was a bit of a hack, as was the rest of the language, but now in AS3 there are two ways:
-
The proper way
This is the proper way but it's a pain in the bum as you need to declare each namespace in the AS3 before loading in the XML, like so:var rss:Namespace = new Namespace("http://purl.org/rss/1.0/"); var rdf:Namespace = new Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#"); var dc:Namespace = new Namespace("http://purl.org/dc/elements/1.1/");Only then can you access the XML's variables like so:var item:String = items[i].dc::date;
So you see that you put the declared namespace and two colons (this :) in front of the tag's/attribute's name. This may look painful - and it is, but you can find out more on Adobe's LiveDocs. -
The easy way
This way is much easier (and lazier I guess) but it gets the job done and isn't messy or a hack. A namespace is simply a method to associate tags (or attributes) to a URL reference, so for example GData references OpenSearch, YouTube's and Google's own schemas and more. The easy way around is instead of declaring namespaces and prefixing them to tag's/attribute's names, you simply add an asterisk (this *) like so:var item:String = items[i].*::date;
Easy!
So now we understand how to handle the XML we're getting back, let's go about finishing the proxy. Launch "DataProxy.as" in "src/com/flashtuts/model/" and here's the code for it:
package com.flashtuts.model
{
import com.flashtuts.model.vo.DataVO;
import com.flashtuts.view.component.ProgressView;
import com.flashtuts.view.component.SearchView;
import flash.events.Event;
import flash.events.ProgressEvent;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.utils.Dictionary;
import org.puremvc.as3.interfaces.IProxy;
import org.puremvc.as3.patterns.proxy.Proxy;
public class DataProxy extends Proxy implements IProxy
{
public static const NAME:String = 'DataProxy';
private var queryDic:Dictionary = new Dictionary();
public function DataProxy()
{
super( NAME, new DataVO() );
}
public function searchRun(query:String):void
{
if ( vo.queryResults[ query ] )
{
dataReady( query );
}
else
{
dataLoad( query );
}
}
private function dataLoad(query:String):void
{
var request:URLRequest = new URLRequest( vo.gdataURL + query );
var loader:URLLoader = new URLLoader();
sendNotification( ProgressView.SHOW );
queryDic[ loader ] = query;
loader.addEventListener( Event.COMPLETE, handleSearchComplete );
loader.load( request );
}
private function handleSearchComplete(e:Event):void
{
var data:XML = new XML( e.target.data );
vo.queryResults[ queryDic[ e.target ] ] = data..*::entry;
dataReady( queryDic[ e.target ] );
}
private function dataReady(query:String):void
{
sendNotification( SearchView.SEARCH_RESULTS, { entries: vo.queryResults[ query ] } );
sendNotification( ProgressView.HIDE );
}
public function get vo():DataVO
{
return data as DataVO;
}
}
}
We start off with creating the public function that our data command will trigger "searchRun()". As you see it accepts one parameter, the query string:
public function searchRun(query:String):void
{
if ( vo.queryResults[ query ] )
{
dataReady( query );
}
else
{
dataLoad( query );
}
}
Now to make the application run faster. I'm using the principle that once a query has been run, the data is stored in the VO and there's no need for that query to run again. So using AS3's Dictionary class, we're able to check to see if the results are already in the VO. If they're not, we need to get them, otherwise send them to our mediator. We'll come on to the "dataReady()" function a bit further down:
If the query hasn't yet been run, we need to load some XML:
private function dataLoad(query:String):void
{
var request:URLRequest = new URLRequest( vo.gdataURL + query );
var loader:URLLoader = new URLLoader();
sendNotification( ProgressView.SHOW );
queryDic[ loader ] = query;
loader.addEventListener( Event.COMPLETE, handleSearchComplete );
loader.load( request );
}
private function handleSearchComplete(e:Event):void
{
var data:XML = new XML( e.target.data );
vo.queryResults[ queryDic[ e.target ] ] = data..*::entry;
dataReady( queryDic[ e.target ] );
}
You'll see that we're suffixing the query on to the 'gdataURL' string stored in our VO. We then reference the query string using the proxy's dictionary so that when data's ready, we can store it in the VO under that query and won't need to load it again. Then we add the event listeners and make the call. Once we've got the data back, we get the query from the dictionary, store it in the VO and then run the "dataReady()" function:
private function dataReady(query:String):void
{
sendNotification( SearchView.SEARCH_RESULTS, { entries: vo.queryResults[ query ] } );
sendNotification( ProgressView.HIDE );
}
This function simply gets the entries and sends the notification back to the facade. Now we can display our videos.
Step 16: Creating the Results View
Once we have the entries, it's then easy for us to load the thumbnails for those entries and display them in a grid. Let's start by creating a file called "ResultsView.as" inside "src/com/flashtuts/component/":
package com.flashtuts.view.component
{
import flash.display.Loader;
import flash.display.Sprite;
import flash.events.DataEvent;
import flash.events.MouseEvent;
import flash.net.URLRequest;
import flash.utils.Dictionary;
public class ResultsView extends Sprite
{
public static const NAME:String = 'ResultsView';
public static const SHOW:String = NAME + 'Show';
public static const HIDE:String = NAME + 'Hide';
public static const CLICKED:String = NAME + 'Clicked';
private var idsDic:Dictionary = new Dictionary();
private var thumbnailHeight:Number = 90;
private var thumbnailSpacing:Number = 10;
private var thumbnailWidth:Number = 100;
public function buildThumbnails(entries:Object):void
{
var ix:Number = 0;
var iy:Number = 0;
var thumbnails:Sprite = new Sprite();
var thumbnail:Sprite;
if ( numChildren > 0 )
{
for ( var i:Number = 0; i < numChildren; i++ )
{
removeChildAt( i );
}
buildThumbnails( entries );
}
else
{
for each ( var entry:XML in entries )
{
thumbnail = createThumbnail( entry );
thumbnail.x = ( thumbnailWidth + thumbnailSpacing ) * ix;
thumbnail.y = ( thumbnailHeight + thumbnailSpacing ) * iy;
if ( ix > 3 )
{
ix = 0;
iy++;
}
else
{
ix++;
}
thumbnails.addChild( thumbnail );
}
thumbnails.x = 25;
thumbnails.y = 70;
addChild( thumbnails );
}
}
private function createThumbnail(entry:XML):Sprite
{
var thumbnail:Sprite = new Sprite();
var mask:Sprite = new Sprite();
var request:URLRequest = new URLRequest( entry..*::thumbnail[ 0 ].@url.toString() );
var loader:Loader = new Loader();
idsDic[ thumbnail ] = entry..*::videoid.toString();
mask.graphics.beginFill( 0x000000 );
mask.graphics.drawRoundRect( 0, 0, thumbnailWidth, thumbnailHeight, 5, 5 );
mask.graphics.endFill();
thumbnail.addChild( mask );
loader.load( request );
thumbnail.addChild( loader );
loader.mask = mask;
thumbnail.buttonMode = true;
thumbnail.mouseChildren = false;
thumbnail.addEventListener( MouseEvent.CLICK, handleThumbnailClicked );
return thumbnail;
}
private function handleThumbnailClicked(e:MouseEvent):void
{
dispatchEvent( new DataEvent( CLICKED, true, false, idsDic[ e.target ] ) );
}
}
}
This is simply a class that just takes the YouTube video ids and then loads the corresponding thumbnail. We first of all start by removing all children of the view and when that's done we run through a loop that then loads the thumbnail, masks it (so we have nice rounded corners) and adds an event listener to the sprite. You'll notice that I'm using a dictionary again here called "idsDic". This will simply store a reference of the sprite being added to the stage so that when it's been clicked on, we know the corresponding YouTube video id to load.
Finally you'll see that the class dispatches a "DataEvent" containing the video id back to its mediator.
Step 17: Creating the Results View Mediator
This mediator will have two simple jobs:
- To (re-)build the thumbnails when the query has been run
- To pass the video id to our YouTube player
So let's begin by creating the file "ResultsViewMediator.as" in "src/com/flashtuts/view/":
package com.flashtuts.view
{
import com.flashtuts.view.component.PlayerView;
import com.flashtuts.view.component.ResultsView;
import com.flashtuts.view.component.SearchView;
import flash.events.DataEvent;
import org.puremvc.as3.interfaces.IMediator;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.mediator.Mediator;
public class ResultsViewMediator extends Mediator implements IMediator
{
public static const NAME:String = 'ResultsViewMediator';
private var resultsView:ResultsView;
public function ResultsViewMediator(viewComponent:Object=null)
{
super( NAME, viewComponent );
}
override public function onRegister():void
{
resultsView = new ResultsView();
resultsView.addEventListener( ResultsView.CLICKED, sendEvent );
viewComponent.addChild( resultsView );
}
override public function listNotificationInterests():Array
{
return [
SearchView.SEARCH_RESULTS
];
}
override public function handleNotification(notification:INotification):void
{
var name:String = notification.getName();
var body:Object = notification.getBody();
switch ( name )
{
case SearchView.SEARCH_RESULTS:
resultsView.buildThumbnails( body.entries );
break;
}
}
private function sendEvent(e:DataEvent):void
{
sendNotification( PlayerView.PLAY, { videoId: e.data } );
}
}
}
As I said before, this mediator listens in on 'SearchView.SEARCH_RESULTS' (sent out by the proxy) and sends 'PlayerView.PLAY' once a thumbnail has been created. Now we can get on to creating the player view that will house the YouTube player.
Step 18: Creating the Player View
Before you start with this, you should really read up on the YouTube Player API tutorial I wrote as we'll be using it here. In that tutorial we created a set of classes and a player wrapper. Now copy that player wrapper and paste it in to "src/assets/swf/". As for our classes, I've put them in a simple reusable directory called "src/com/flashtuts/lib/", therefore I've got the class "YouTubePlayer.as" in "src/com/flashtuts/lib/display/" and "YouTubePlayerEvent.as" in "src/com/flashtuts/lib/events/". This'll mean that you could easily reuse these classes time and time again.
Once you're happy with the location of your YouTube player classes, we'll begin with the player view, so create a new file called "PlayerView.as" inside "src/com/flashtuts/view/component/":
package com.flashtuts.view.component
{
import com.flashtuts.lib.display.YouTubePlayer;
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.filters.GlowFilter;
public class PlayerView extends Sprite
{
public static const NAME:String = 'PlayerView';
public static const PLAY:String = NAME + 'Play';
public static const CLOSE:String = NAME + 'Close';
private var player:YouTubePlayer;
public function PlayerView()
{
init();
}
private function init():void
{
var bg:Sprite = new Sprite();
var closeButton:CloseButtonAsset = new CloseButtonAsset();
bg.graphics.beginFill( 0x000000, .8 );
bg.graphics.drawRect( 0, 0, 600, 400 );
bg.graphics.endFill();
addChild( bg );
player = new YouTubePlayer();
player.autoPlay = true;
player.playerWidth = 550;
player.playerHeight = 350;
player.x = 25;
player.y = 25;
addChild( player );
closeButton.buttonMode = true;
closeButton.filters = [ new GlowFilter( 0x000000, 1, 10, 10 ) ];
closeButton.x = 560;
closeButton.y = 15;
closeButton.addEventListener( MouseEvent.CLICK, close );
addChild( closeButton );
visible = false;
}
public function play(videoId:String):void
{
player.init( videoId );
visible = true;
}
public function close(e:*=null):void
{
player.stop();
visible = false;
}
}
}
As you can see, I start with creating the YouTube player, all within the "init()" function and leave the class to be invisible. I've also used another SWC asset called "CloseButtonAsset". Then we have our two main functions "play()" and "stop()" and they do exactly what it says on the tin.
Now that we're happy with our player view, we can get on with the mediator.
Step 19: Creating Player View Mediator
Again this mediator won't be as complex as you may think. All it's going to do is listen to the notification 'PlayerView.PLAY' and show the player:
package com.flashtuts.view
{
import com.flashtuts.view.component.PlayerView;
import org.puremvc.as3.interfaces.IMediator;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.mediator.Mediator;
public class PlayerViewMediator extends Mediator implements IMediator
{
public static const NAME:String = 'PlayerViewMediator';
private var playerView:PlayerView;
public function PlayerViewMediator(viewComponent:Object=null)
{
super( NAME, viewComponent );
}
override public function onRegister():void
{
playerView = new PlayerView();
viewComponent.addChild( playerView );
}
override public function listNotificationInterests():Array
{
return [
PlayerView.PLAY
];
}
override public function handleNotification(notification:INotification):void
{
var name:String = notification.getName();
var body:Object = notification.getBody();
switch ( name )
{
case PlayerView.PLAY:
playerView.play( body.videoId );
break;
}
}
}
}
Step 20: Deferring the Initialising of the Mediators
We're nearly at the end, but if you've been paying attention you'll see that we haven't yet added the results view mediator or the player view mediator to our application yet. This is because they're not needed at the start of the application, for example, the results view mediator is only needed once a query is being run and the player view mediator is only needed once the results are in and ready to be clicked on. Our application mediator will control all of this, so launch "ApplicationMediator.as" - we're going to update the "listNotificationInterests()" and "handleNotification()" functions like so:
package com.flashtuts.view
{
import com.flashtuts.view.component.SearchView;
import org.puremvc.as3.interfaces.IMediator;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.mediator.Mediator;
public class ApplicationMediator extends Mediator implements IMediator
{
public static const NAME:String = 'ApplicationMediator';
public function ApplicationMediator(viewComponent:Object=null)
{
super( NAME, viewComponent );
}
override public function onRegister():void
{
facade.registerMediator( new ProgressViewMediator( viewComponent ) );
facade.registerMediator( new SearchViewMediator( viewComponent ) );
}
override public function listNotificationInterests():Array
{
return [
SearchView.SEARCH_RUN,
SearchView.SEARCH_RESULTS
];
}
override public function handleNotification(notification:INotification):void
{
var name:String = notification.getName();
var body:Object = notification.getBody();
switch ( name )
{
case SearchView.SEARCH_RUN:
if ( !facade.hasMediator( ResultsViewMediator.NAME ) )
{
facade.registerMediator( new ResultsViewMediator( viewComponent ) );
}
break;
case SearchView.SEARCH_RESULTS:
if ( !facade.hasMediator( PlayerViewMediator.NAME ) )
{
facade.registerMediator( new PlayerViewMediator( viewComponent ) );
}
break;
}
}
}
}
You'll see that we're only registering the mediators at certain points during the user's interaction with the application.
And we've done it! Fire up the application and let's have a look..
Conclusion
So there you have it. Using the agility of PureMVC and the YouTube Player API tutorial you can now expand this little application to do many things or integrate the code into a bigger application or web site. The biggest help is Google's GData API service, ever since they've pushed it to YouTube, querying the service has become easier and easier. It's worth having a read of the GData docs as there are lots of things that can be done with Google's products, such as uploading videos to YouTube, using Picasa APIs and Calendar APIs.
Thanks for following along.















User Comments
( ADD YOURS )André August 5th
Nice work dude, but the demo returns an IOErrorEvent for me
Thanks for sharing, i will see this better latter, it´s too many code for now.
I am now working on my next tutorial, and it´s final result will be this:
( )http://cavallari.freehostia.com/sphere/
Ahmed Nuaman August 5th
Yep I see that, it’s the old S3 forbidden error, care to update perms please Ian?
( )André August 5th
Here the error was an IOError, not found… not a perms error, but it´s always important to look the permissions, good observation
Ian Yates August 5th
Got it. Sorry Ahmed, that one was buried deep..
André August 5th
Now i am getting the video working too, very nice work!!
Vlasnn August 5th
looks nice, it’s doing the search but i can not see the video.
( )Dario Gutierrez August 5th
Looks great the grid, Let me try it!
( )Mikko Saario August 5th
Great tut as always. Would be nice to see a tutorial about using puremvc multicore
( )Chris August 5th
I think thats too much code for that small project. PURE MVC should be used on big projects.
( )André August 6th
I had this kind of thought too, also i am start learning better the PURE MVC, but as he said, you can start a small project and it becomes a very big project, so i think even in small projects the PUREMVC will be very usefull too.
( )Joel August 5th
I think it would be silly to write a tutorial using a huge project. PureMVC works well on small projects, and the ‘extra’ code flows quickly once you aren’t thinking about it anymore.
Nice work!
( )Maurizio Liberato August 6th
Cool! Nice gadget and great tutorial!
( )floral August 6th
what the hell ???? !!!!
( )Monkey August 7th
“what the hell ???? !!!!”
Whats your problem?
Nice work as always Ahmed!
( )Adam August 10th
THANK YOU THANK YOU! between this and your last tutorial I think i have a pretty good handle on the framework. THE POSSIBILITIES! I’ll be using puremvc for this next big project I’m working on. Wish me luck.
Thanks again, I’ll definitely be keeping an eye on your blog.
( )Thaylin September 30th
Nice tutorial, but there seems to be an issue with the app.
( )It worked at first and it loaded the video but now when I click on an image it just freezes the browser. The console says it’s loading the swf but the activity monitor for Safari doesn’t say anything (probably because it’s frozen).
Any idea what could be causing this? I’ve looked over everything and I’m clueless. Maybe the as2 loader?