Skip directly to content

blog.lyraspace.com

Subscribe to blog.lyraspace.com feed
Updated: 48 min 18 sec ago

I’m moving into the cloud

Tue, 03/08/2011 - 17:47

I’m saying goodbye to WordPress and moving into the cloud. I’ve already moved my main website onto a Drupal Gardens instance which is available to mock view here.

I’ll be doing the bulk of my blogging on that site from now on but I’m still unsure whether to maintain a separate blog for more technical geeky stuff on this domain. This site will certainly stay live for a little while but it’s possible I will look at setting up a Tumblr blog in the future. I’m already using a Posterous blog for leeprobert.com and this is great and has a nice iPhone app.

WordPress running on my Media Temple server started to become a real headache with attacks and spam. It does look like they’re getting on top of this but as I seem to move more and more of my life in the cloud it made sense to do the same with the sites.

Also, as my work moves further away from coding and more towards user experience and digital strategy it seems that anything I have to say will most likely be appropriate fare for my main site.

So, for the time being … goodbye blog dot lyraspace.

Pixel-perfect accuracy with FXG?

Mon, 08/16/2010 - 18:48

I’ve been running some tests in Flex rendering FXG rects with a stroke to see if there’s a way to get the strokes to render inside the bounds of the object. I need this precision for a project I’m working on and unfortunately it doesn’t look good. All of these tests were done using some simple FXG code …

  1. <s:Rect
  2.    
  3.    width="30"
  4.    height="30"
  5.    radiusX="4"
  6.    radiusY="4"
  7.    >
  8.    <s:fill>
  9.     <s:SolidColor color="#ffffff" />
  10.    </s:fill>
  11.    <s:stroke>
  12.     <s:SolidColorStroke color="#ff3333" alpha="1" pixelHinting="true" weight="2" />
  13.    </s:stroke>
  14.   </s:Rect>

I’m not too concerned about the rounded corners but the lack of precision is an issue if you intend to create dynamic skins and want some control over the positions and dimensions of your composition.

Take a look to see my results :


Uploaded with plasq‘s Skitch!
Uploaded with plasq‘s Skitch!
Uploaded with plasq‘s Skitch!

You may need to jump to Skitch for that last one but basically if you have four squares in a HGroup with a 4 pixel gap and the square is defined as 30 pixels in width then you would expect an overall width of 132 pixels. You will actually get 140. This could be a big problem. 8 pixels is a big discrepancy.

Flash Catalyst and the Designer/Developer workflow

Tue, 07/13/2010 - 10:56

Uploaded with plasq‘s Skitch!
Uploaded with plasq‘s Skitch!
Uploaded with plasq‘s Skitch!
Uploaded with plasq‘s Skitch!
Uploaded with plasq‘s Skitch!

I drew this up recently to illustrate a proposed workflow for a project I’m working on. This is similar to the workflow as proposed by Adobe but with an extra level of responsibility on the Interaction Designer. This being the requirement to adapt their Catalyst work in Flash Builder. This, in my opinion, is a more realistic overview of how most Flex framework projects actually play out nowadays.

Flash Catalyst is not quite powerful enough at the moment to produce flexible, bespoke skins. The chances are you’ll need to build components that aren’t just button skins. You need to write your own SkinnableComponent classes with custom skin parts that resize dynamically etc etc.

I’m afraid, if you want to be a shit-hot Interaction Designer and need to build amazing, re-usable prototypes that can quickly be integrated into the final architecture then you’re going to need to think outside of the Catalyst and get your hands dirty with Spark and the skinning component life-cycle.

Good luck!

There’s a PDF of the images on this post here.

Archiving and Unarchiving objects in Objective-C

Mon, 06/07/2010 - 09:59

Woohoo … my first Objective-C related post!

Ok, so this is nothing new but I thought I’d log a couple of things that caught me out as a NOOB to the whole iPhone development malarky.

First off archiving your objects is a way of saving them to a property list file that sits on your device and can be unarchived the next time your app loads as a way to persist data locally on the device. Very handy it is too. Now there are better ways to manage data in Obj-C … CoreData for a start. This is a much more powerful way to persist data to a SQL lite database and allows you loads more freedom in what you can actually archive. There are limitations to the property list method you see. You can only archive NS objects and simple scalars so your class structure needs to compensate for this. The ‘plist’ objects you can archive are … NSArray, NSDictionary, NSString, NSNumber, NSDate and NSData along with their mutable subclasses. The scalars are int, float, double etc.

I don’t want to write a massive chapter on this and there’s loads of information online about the technical ins and outs of what’s happening behind the scenes. What I do want to do is share the classes I’ve used in a recent project and explain in basics what’s happening.

Firstly we’ll create the object we want to be archived. Any object that needs to be archived and is a subclass of NSObject must adopt the NSCoding protocol. Take a look at this object …

DataModel Object for archiving

  1. /* INTERFACE */
  2.  
  3. #define kfavesArray @"favesArray"
  4. #define kCurrentFave @"currentFave"
  5. #define kCurrentFaveIndex @"currentFaveIndexPath"
  6.  
  7. #import <Foundation/Foundation.h>
  8.  
  9. @class FaveVO;
  10.  
  11.  
  12. @interface DataModel : NSObject <NSCoding>
  13. {
  14.         NSMutableArray *favesArray;
  15.         FaveVO *currentFave;
  16.         int faveIndex;
  17. }
  18.  
  19. @property (nonatomic, retain) NSMutableArray *favesArray;
  20. @property (nonatomic, retain) FaveVO *currentFave;
  21. @property int faveIndex;
  22.  
  23. -(void)addNewFaveLocation:(FaveVO *)fave;
  24. -(void)createNewFaveVOFromFaveVO:(FaveVO *)fave;
  25. -(void)setFaveLocation:(int)index;
  26. -(void)removeFaveLocation;
  27.  
  28. @end
  29.  
  30. /* IMPLEMENTATION */
  31.  
  32. #import "DataModel.h"
  33. #import "FaveVO.h"
  34.  
  35. @implementation DataModel
  36.  
  37. @synthesize favesArray;
  38. @synthesize currentFave;
  39. @synthesize faveIndex;
  40.  
  41. //------------------------------------------------------
  42. -(id)init
  43. {
  44.         if(self = [super init])
  45.         {
  46.                 favesArray = [[NSMutableArray alloc] init];
  47.         }
  48.        
  49.         return self;
  50. }
  51. //--------------------------------------------------
  52. -(void)addNewFaveLocation:(FaveVO *)fave
  53. {
  54.         if(!favesArray) favesArray = [[NSMutableArray alloc] init];
  55.        
  56.         [favesArray insertObject:fave atIndex:0];
  57.         currentFave = [favesArray objectAtIndex:0];
  58.         faveIndex = 0;
  59.        
  60.         NSLog(@"DataModel currentFave: %@ faveIndex: %i", currentFave,faveIndex);
  61. }
  62. //--------------------------------------------------
  63. -(void)setFaveLocation:(int)index
  64. {
  65.         currentFave = [favesArray objectAtIndex:index];
  66.         faveIndex = index;
  67.        
  68.         NSLog(@"DataModel currentFave: %@ faveIndex: %i", currentFave,faveIndex);
  69. }
  70. //--------------------------------------------------
  71. -(void)removeFaveLocation
  72. {
  73.         currentFave = nil;
  74.        
  75.         NSLog(@"DataModel currentFave: %@ faveIndex: %i", currentFave,faveIndex);
  76. }
  77. //--------------------------------------------------
  78. -(void)createNewFaveVOFromFaveVO:(FaveVO *)fave
  79. {
  80.         double lat = fave.lat;
  81.         double lon = fave.lon;
  82.        
  83.         FaveVO *newFaveVO = [[FaveVO alloc] initWithLabel:[NSString stringWithString:fave.label]
  84.                                                                                 streetAddress:[NSString stringWithString:fave.streetAddress]
  85.                                                                                                  city:[NSString stringWithString:fave.city]
  86.                                                                                                 state:[NSString stringWithString:fave.state]
  87.                                                                                                   zip:[NSString stringWithString:fave.zip]
  88.                                                                                                   lat:lat
  89.                                                                                                   lon:lon];
  90.         [favesArray addObject:newFaveVO];
  91.         [newFaveVO release];
  92. }
  93. //--------------------------------------------------
  94. -(void)dealloc
  95. {
  96.         [favesArray release];
  97.         [currentFave release];
  98.         [super dealloc];
  99. }
  100. //--------------------------------------------------
  101.  
  102.  
  103. //--------------------------------------------------
  104. #pragma mark NSCoding
  105. - (void)encodeWithCoder:(NSCoder *)encoder
  106. {
  107.         [encoder encodeObject:favesArray forKey:kfavesArray];
  108.         [encoder encodeObject:currentFave forKey:kCurrentFave];
  109.         [encoder encodeInt:faveIndex forKey:kCurrentFaveIndex];
  110. }
  111. //--------------------------------------------------
  112. - (id)initWithCoder:(NSCoder *)decoder
  113. {
  114.         NSLog(@"DataModel initWithCoder: %@", decoder);
  115.        
  116.         if (self = [super init])
  117.         {
  118.                 self.favesArray = [decoder decodeObjectForKey:kfavesArray];
  119.                
  120.                 self.faveIndex = [decoder decodeIntForKey:kCurrentFaveIndex];
  121.                 self.currentFave = [favesArray objectAtIndex:faveIndex];
  122.         }
  123.         return self;
  124. }
  125.  
  126. @end

This object also references another object called FaveVO this is an object that I’m feeding into an array and also adopts the NSCoding protocol so you can see that you can archive quite a complicated object overall.

The important stuff is after the line that says “#pragma mark NSCoding”. These methods (using Actionscript terminology but I don’t care) handle what happens when the encoder and decoder go to work on your object.

- (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:favesArray forKey:kfavesArray]; [encoder encodeObject:currentFave forKey:kCurrentFave]; [encoder encodeInt:faveIndex forKey:kCurrentFaveIndex]; }

The encodeObject message takes each of my class variables and copies them into an NSData object using the keys which I defined in the interface. These are unique identifiers that I can use when I want to decode them later like so :

- (id)initWithCoder:(NSCoder *)decoder { if (self = [super init]) { self.favesArray = [decoder decodeObjectForKey:kfavesArray]; self.faveIndex = [decoder decodeIntForKey:kCurrentFaveIndex]; self.currentFave = [favesArray objectAtIndex:faveIndex]; } return self; }

And that’s all you need to know about the Object you want to archive. Next comes the class that I’m using to do the archiving and unarchiving.

Here’s the class :

Archiving and Unarchiving DataModel Object

  1. /* INTERFACE */
  2.  
  3. #import <Foundation/Foundation.h>
  4.  
  5. #define kFilename       @"dataarchive.plist"
  6. #define kDataKey                @"Data"
  7.  
  8. @class DataModel;
  9. @interface DataManager : NSObject {
  10.  
  11.         DataModel *dataModel;
  12. }
  13.  
  14. @property (nonatomic, retain) DataModel *dataModel;
  15.  
  16. -(NSString *)dataFilePath;
  17. -(void)initDataModel;
  18. -(void)applicationWillTerminate:(NSNotification *)notification;
  19.  
  20. +(DataManager *)sharedDataManager;
  21.  
  22. @end
  23.  
  24. /* IMPLEMENTATION */
  25.  
  26. #import "DataManager.h"
  27. #import "SynthesizeSingleton.h"
  28. #import "DataModel.h"
  29. #import "FaveVO.h"
  30.  
  31. @implementation DataManager
  32.  
  33. SYNTHESIZE_SINGLETON_FOR_CLASS(DataManager);
  34.  
  35. @synthesize dataModel;
  36.  
  37. //-----------------------------------------------------------------------------------------------------
  38. - (NSString *)dataFilePath
  39. {
  40.         NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  41.         NSString *documentsDirectory = [paths objectAtIndex:0];
  42.         return [documentsDirectory stringByAppendingPathComponent:kFilename];
  43. }
  44. //-----------------------------------------------------------------------------------------------------
  45. -(void)initDataModel
  46. {
  47.         NSString *filePath = [self dataFilePath];
  48.        
  49.         if([[NSFileManager defaultManager] fileExistsAtPath:filePath])
  50.         {
  51.                 NSLog(@"FILE EXISTS");
  52.                
  53.                 NSData *data = [[NSMutableData alloc] initWithContentsOfFile:[self dataFilePath]];
  54.                 NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
  55.                
  56.                 DataModel *decodedDataModel = [unarchiver decodeObjectForKey:kDataKey];
  57.                
  58.                 dataModel = [[DataModel alloc] init];
  59.                
  60.                 NSEnumerator *enumerator;
  61.                 enumerator = [decodedDataModel.favesArray objectEnumerator];
  62.                
  63.                 FaveVO *decodedFaveVO;
  64.                
  65.                 while (decodedFaveVO = [enumerator nextObject])
  66.                 {
  67.                         [dataModel createNewFaveVOFromFaveVO:decodedFaveVO];
  68.                 }
  69.                
  70.                 int index = decodedDataModel.faveIndex;
  71.                 dataModel.faveIndex = index;
  72.                 dataModel.currentFave = [dataModel.favesArray objectAtIndex:index];
  73.                
  74.                 [unarchiver finishDecoding];
  75.                 [unarchiver release];
  76.                 [data release];
  77.         }
  78.         else
  79.         {
  80.                 NSLog(@"FILE DOES NOT EXIST");
  81.                
  82.                 dataModel = [[DataModel alloc] init];
  83.         }
  84.        
  85.         UIApplication *app = [UIApplication sharedApplication];
  86.         [[NSNotificationCenter defaultCenter] addObserver:self
  87.                                                                                          selector:@selector(applicationWillTerminate:)
  88.                                                                                                  name:UIApplicationWillTerminateNotification
  89.                                                                                            object:app];
  90.         
  91. }
  92. //-------------------------------------------------------------
  93. - (void)applicationWillTerminate:(NSNotification *)notification
  94. {
  95.         NSLog(@"Application will terminate");
  96.        
  97.         NSMutableData *data = [[NSMutableData alloc] init];
  98.         NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
  99.        
  100.         [archiver encodeObject:dataModel forKey:kDataKey];
  101.         [archiver finishEncoding];
  102.         [data writeToFile:[self dataFilePath] atomically:YES];
  103.        
  104.         [archiver release];
  105.         [data release];
  106. }
  107.  
  108.  
  109. //--------------------------------------------------
  110. -(void)dealloc
  111. {
  112.         [dataModel release];
  113.         [super dealloc];
  114. }
  115.  
  116.  
  117. @end

The initDataModel method looks for the property list and then uses the NSKeyedUnarchiver object to decode the objects. When the application terminates the objects are encoded using the NSKeyedArchiver object. The class listens for the ApplicationWillTerminate notification using the NSNotificationCenter object. This is similar to the Actionscript event model that uses listeners. The NSEnumerator object is used to step through an array of objects. The decoded FaveVO objects are then sent through to my DataModel class and passed to the createNewFaveVOFromFaveVO method which is where the objects are re-created as shiny new objects. Remember the NSKeyedUnarchiver object is released so you need to retain the objects you need … usually better to create new ones within the correct scope.

Another custom Flex preloader but with RSL support

Thu, 04/15/2010 - 13:41

This post has a great article on building a custom Flex preloader which also allows you to handle the progress of the runtime shared libraries that the Flex framework now loads into the users cache.

The DownloadProgressBar component allows you to view the progress of the runtime shared libraries but be aware that it can only tell you about the RSL as it loads and as there may be more than one it is practically impossible to get any indication of how many bytes in total the app is going to load. For this reason you may have seen erratic preloaders out there at the moment using all kinds of colourful language to tell you what it’s doing … “loading assets”, “modules”, “opening the champagne” etc. This is all done to keep the user occupied while each of the elements are loaded.

Then there’s the actual SWF file. Thankfully there are separate events to handle this so if you prefer you can still display the progress of the main SWF and then when it is completed change your display to inform the user that you are “initialising” which shouldn’t be too long after the main SWF has finished. Indeed your dependencies may have finished before the SWF.

I guess the only way to do this accurately is to know up front exactly how many bytes your application is going to load and then as you get the information from ProgressEvent.bytesLoaded from both the SWF and the RSL you can adjust your percentage calculation accordingly. If anyone does know how you can programmatically detect this I’d love to hear it.

Thanks to Bernhard Hirschmann and Jesse Warden for the work they’ve done and purely for my own reference here’s the class …

Custom Preloader for Flex with RSL support

  1. package
  2. {
  3.     import flash.display.MovieClip;
  4.     import flash.display.Sprite;
  5.     import flash.events.Event;
  6.     import flash.events.ProgressEvent;
  7.    
  8.     import mx.events.FlexEvent;
  9.     import mx.events.RSLEvent;
  10.     import mx.preloaders.DownloadProgressBar;
  11.  
  12.     /**
  13.      * This class extends the lightweight DownloadProgressBar class.  This class
  14.      * uses an embedded Flash 8 MovieClip symbol to show preloading.
  15.      * For handling both, SWF and RSL download, it separated its download progress
  16.      * by handling the appropriate events, i.e. RSLEvent
  17.      *
  18.      * @author jessewarden
  19.      * @author Bernhard Hirschmann (http://coding.bhirschmann.de)
  20.      */   
  21.     public class Preloader extends DownloadProgressBar
  22.     {
  23.        
  24.         /**
  25.          * The Flash 8 MovieClip embedded as a Class.
  26.          */       
  27.         [Embed(source="../assets/flash/preloader.swf", symbol="Preloader")]
  28.         private var FlashPreloaderSymbol:Class;
  29.        
  30.         private var clip:MovieClip;
  31.        
  32.     private var    isRslDownloading:Boolean = false;
  33.    
  34.         private var rslPercent:Number = 0;
  35.         private var swfPercent:Number = 0;
  36.        
  37.         private var rslBytesTotal:Number;
  38.         private var rslBytesLoaded:Number = 0;
  39.        
  40.         private var swfBytesTotal:Number;
  41.         private var swfBytesLoaded:Number = 0;
  42.        
  43.         public function Preloader()
  44.         {
  45.             super();
  46.            
  47.             // instantiate the Flash MovieClip, show it, and stop it.
  48.             // Remember, AS2 is removed when you embed SWF's,
  49.             // even "stop();", so you have to call it manually if you embed.
  50.             clip = new FlashPreloaderSymbol();
  51.             addChild(clip);
  52.             clip.gotoAndStop("start");
  53.         }
  54.        
  55.        public override function set preloader(preloader:Sprite):void
  56.     {                   
  57.       trace("starting...");
  58.  
  59.       // runtime shared library
  60.       preloader.addEventListener( RSLEvent.RSL_PROGRESS, onRSLDownloadProgress );
  61.       preloader.addEventListener( RSLEvent.RSL_COMPLETE, onRSLDownloadComplete );
  62.       preloader.addEventListener( RSLEvent.RSL_ERROR, onRSLError );
  63.  
  64.       // application
  65.       preloader.addEventListener( ProgressEvent.PROGRESS, onSWFDownloadProgress );   
  66.       preloader.addEventListener( Event.COMPLETE, onSWFDownloadComplete );
  67.      
  68.       // initialization
  69.       preloader.addEventListener( FlexEvent.INIT_PROGRESS, onFlexInitProgress );
  70.       preloader.addEventListener( FlexEvent.INIT_COMPLETE, onFlexInitComplete );
  71.      
  72.       clip.preloader.rsl_amount_txt.text = clip.preloader.app_amount_txt.text = "0%";
  73.      
  74.       centerPreloader();
  75.     }
  76.        
  77.     /**
  78.      * Makes sure that the preloader is centered in the center of the app.
  79.      *
  80.      */       
  81.     private function centerPreloader():void
  82.         {
  83.             x = (stageWidth / 2) - (clip.width / 2);
  84.             y = (stageHeight / 2) - (clip.height / 2);
  85.         }
  86.        
  87.     /**
  88.      * Updates the progress bar.
  89.      */
  90.     private function updateProgress():void
  91.     {
  92.       var p:Number = Math.round( (rslPercent + swfPercent) / 2 );
  93.         clip.preloader.gotoAndStop(p);
  94.     }
  95.    
  96.         /**
  97.          * As the RSL (runime shared library) (frame 2 usually) downloads, this event gets called.
  98.          * You can use the values from this event to update your preloader.
  99.          * @param event
  100.          *
  101.          */       
  102.         private function onRSLDownloadProgress( event:ProgressEvent ):void
  103.     {
  104.         isRslDownloading = true;
  105.        
  106.         rslBytesTotal = event.bytesTotal;
  107.         rslBytesLoaded = event.bytesLoaded;
  108.         rslPercent = Math.round( (rslBytesLoaded / rslBytesTotal) * 100);
  109.         trace("onRSLDownloadProgress: rslBytesLoaded " + rslBytesLoaded);
  110.         trace("onRSLDownloadProgress: rslBytesTotal " + rslBytesTotal);
  111.         trace("onRSLDownloadProgress: " + rslPercent + "%");
  112.         clip.preloader.rsl_amount_txt.text = String(rslPercent) + "%";
  113.        
  114.         updateProgress();
  115.     }
  116.    
  117.     /**
  118.      * When the download of frame 2
  119.      * is complete, this event is called. 
  120.      * This is called before the initializing is done.
  121.      * @param event
  122.      *
  123.      */       
  124.     private function onRSLDownloadComplete( event:RSLEvent ):void
  125.     {
  126.         trace("onRSLDownloadComplete: 100% - bytes total: " + event.bytesTotal);
  127.            clip.preloader.gotoAndStop(100);
  128.         clip.preloader.rsl_amount_txt.text = "100%";
  129.         rslPercent = 100;
  130.     }
  131.  
  132.     private function onRSLError( event:RSLEvent ):void
  133.     {
  134.         trace("onRSLError: " + event.errorText + " - " + event.url);
  135.         clip.preloader.status_txt.text = event.errorText;
  136.     }
  137.  
  138.         /**
  139.          * As the SWF (frame 2 usually) downloads, this event gets called.
  140.          * You can use the values from this event to update your preloader.
  141.          * @param event
  142.          *
  143.          */       
  144.         private function onSWFDownloadProgress( event:ProgressEvent ):void
  145.     {
  146.         swfBytesTotal = event.bytesTotal;
  147.         swfBytesLoaded = event.bytesLoaded;
  148.        
  149.         if ( isRslDownloading ) {
  150.           // as soon as RSL starts downloading the SWF data are added by the RSL values
  151.           swfBytesTotal -= rslBytesTotal;
  152.           swfBytesLoaded -= rslBytesLoaded;
  153.         }
  154.         swfPercent = Math.round( (swfBytesLoaded / swfBytesTotal) * 100);
  155.         trace("onSWFDownloadProgress: " + swfPercent + "%");
  156.         trace("onSWFDownloadProgress: swfBytesLoaded " + swfBytesLoaded);
  157.         trace("onSWFDownloadProgress: swfBytesTotal " + swfBytesTotal);
  158.         clip.preloader.app_amount_txt.text = String(swfPercent) + "%";
  159.  
  160.         updateProgress();
  161.     }
  162.    
  163.     /**
  164.      * When the download of frame 2
  165.      * is complete, this event is called. 
  166.      * This is called before the initializing is done.
  167.      * @param event
  168.      *
  169.      */       
  170.     private function onSWFDownloadComplete( event:Event ):void
  171.     {
  172.         trace("onSWFDownloadComplete: 100%");
  173.            clip.preloader.gotoAndStop(100);
  174.         clip.preloader.app_amount_txt.text = "100%";
  175.         swfPercent = 100;
  176.     }
  177.    
  178.     /**
  179.      * When Flex starts initilizating your application.
  180.      * @param event
  181.      *
  182.      */       
  183.     private function onFlexInitProgress( event:FlexEvent ):void
  184.     {
  185.         //trace("onFlexInitProgress: Initializing...");
  186.         try {
  187.           clip.preloader.gotoAndStop(100);
  188.           clip.preloader.status_txt.text = "Initializing...";
  189.         }
  190.         catch (e:Error) {
  191.         }
  192.     }
  193.    
  194.     /**
  195.      * When Flex is done initializing, and ready to run your app,
  196.      * this function is called.
  197.      *
  198.      * You're supposed to dispatch a complete event when you are done.
  199.      * I chose not to do this immediately, and instead fade out the
  200.      * preloader in the MovieClip.  As soon as that is done,
  201.      * I then dispatch the event.  This gives time for the preloader
  202.      * to finish it's animation.
  203.      * @param event
  204.      *
  205.      */       
  206.     private function onFlexInitComplete( event:FlexEvent ):void
  207.     {
  208.         trace("onFlexInitComplete");
  209.         clip.addFrameScript(21, onDoneAnimating);
  210.         clip.gotoAndPlay("fade out");
  211.     }
  212.    
  213.     /**
  214.      * If the Flash MovieClip is done playing it's animation,
  215.      * I stop it and dispatch my event letting Flex know I'm done.
  216.      * @param event
  217.      *
  218.      */       
  219.     private function onDoneAnimating():void
  220.     {
  221.         trace("onDoneAnimating");
  222.         clip.stop();
  223.         dispatchEvent( new Event( Event.COMPLETE ) );
  224.     }
  225.        
  226.     }
  227. }