Chapter 14. BUILDING STUFF

Up to this point in the book, you have created quite a few projects using images, text, audio, video, and other media. We bet you're feeling pretty good about what you've accomplished (you should!), and, like many who have reached your skill level, you are wondering, "How does all of this hang together?"

In this chapter, we will bring together the various skills and knowledge you have developed, and use them to create some rather interesting projects. We are going to start slowly and show you how to build a preloader, and then move through a slide show, an MP3 player, and finish with a full-bore video player. Some of these are quite complicated projects, but if you have reached this point in the book, you are ready to develop some Flash "chops" and explore what you can do with your newfound skills.

Here's what we'll cover in this chapter:

  • Loading visual content from outside the SWF

  • Preloading a movie with heavy internal content

  • Creating a slide show using external images and XML

  • Creating a custom MP3 player

  • Creating a custom video controller

The following files are used in this chapter (located in Chapter14/ExerciseFiles_Ch14/Exercise/):

  • Loading.fla

  • toBeLoaded.swf

  • toBeLoaded.png

  • BeachTrip.fla

  • beach01.jpg–beach05.jpg

  • NASATrip.fla

  • 798_01.jpg−798_08.jpg

  • slideshow.xml

  • TinBangs.fla

  • controls.fla

  • playlist.xml (MP3 player version)

  • WhiteLies(Timekiller).mp3

  • YoungLions.mp3

  • YourSkyIsFalling.mp3

  • VideoPlayer.fla

  • playlist.xml (video player version)

  • InkSong.mp4

  • InspectorGadget.flv

  • PeterWolf.flv

The source files are available online from either of the following sites:

  • http://www.FoundationFlashCS4.com

  • http://www.friendsofED.com/download.html?isbn=9781430210931

Loading content

Flash has a potentially bad habit that drives people crazy. In cases where everything in a movie is packed into the first few frames—and especially in single-frame movies—the SWF can take an awfully long time to display. Why? Because Flash Player loads content one frame at a time, and when a SWF's first frame is heavy, the rest of the movie suffers. It's even more interesting in cases where Export for ActionScript is selected for library assets, because those items are included in the movie's first frame, in a behind-the-scenes way, even if you don't place them there yourself (Flash does it for you automatically). This should explain to you why, when you hit certain websites, you're slugged with an interminable wait, involving fingers drumming on the mouse or your desk and audible sighs as you wait for the movie to start.

One useful solution is to remove your heaviest assets—large images, audio, and video files—and use ActionScript to load them at runtime. This way, the rest of your content—the lighter stuff, including text and vector artwork—displays almost immediately, while the heavy stuff streams into the SWF from your server. Just keep in mind that even the light stuff may need a few seconds to load. But at least your audience will be looking at something, and even the mere perception of at least something happening works PR wonders.

Dynamic loading

The dynamic loading we're talking about is accomplished through the use of the Loader class, new to ActionScript 3.0 and introduced in Flash CS3. (To be sure, ActionScript 2.0 and earlier also supported dynamic loading, but different approaches were required for different kinds of external files. ActionScript 3.0 makes it more consistent, and therefore simpler.) You can use the Loader class to load SWF files or image files (JPG, GIF, or PNG) into a SWF through the use of the load() method, and then take action when the requested files arrive.

  1. To see how all of this works, open the Loading.fla file in the Exercise/Preloader folder for this chapter. You'll see there's a single movieclip on the stage; this movieclip is the only object in the library. We have given this symbol the instance name clip.

  2. Select the first frame of the scripts layer, open the Actions panel, and add the following code:

    var loader:Loader = new Loader();
    addChild(loader);
    //clip.addChild(loader);
    
    loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
    
    Dynamic loading
    completeHandler); function completeHandler(evt:Event):void { //clip.x = 50; //clip.y = 50; }; loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS,
    Dynamic loading
    progressHandler); function progressHandler(evt:ProgressEvent):void { trace(evt.bytesLoaded); }; var req:URLRequest("toBeLoaded.swf"); //var req:URLRequest("toBeLoaded.png"); loader.load(req);

The first line of code creates an instance of the Loader class and stores it in a variable named loader for easy reference. Because this Loader instance is about to load visual content, it's not enough to merely create the object. You also need to add it to a display list, which is accomplished by the second and third lines. Eventually, you'll need only one of the two addChild() lines (notice that the second one is currently commented out, which means it will be ignored by Flash Player), but here's what each does:

  • The first one (addChild(loader);) adds loader to the display list of the main timeline.

  • The second one (clip.addChild(loader);) adds loader instead to the display list of the clip movieclip.

In just a moment, you'll see the difference between these two display lists.

The next two code blocks demonstrate a pair of event handlers. The first one listens for, and responds to, an Event.COMPLETE event, meaning that the requested file has fully loaded. The second one handles ProgressEvent.PROGRESS, which is dispatched repeatedly while the requested file is loading. The innards of the completeHandler() function are commented out for the time being, but you'll see how they're used soon. In the progressHandler() function, a trace() function tells the Output panel how many bytes have been loaded for the requested file.

Note

Handling Loader events is a bit different from what you've seen elsewhere in this book. With movieclips and button symbols, for example, the addEventListener() method is referenced directly by way of the symbol's instance name. With Loader, that method is routed through a contentLoaderInfo property. What's that? Nothing more than an instance of the ContentLoaderInfo class, which is included automatically when you create your Loader instance. The ContentLoaderInfo class contains information about Loader's content, such as the events we're using here.

The following two lines tell loader which file to request: a SWF or a PNG. The PNG request is commented out at the moment. The last line uses the req variable to summon the file.

  1. Now test the movie. Two things will happen: you'll see a haloed guru in the container SWF's upper-left corner and you'll see a few numbers in the Output panel. Those numbers show how much of toBeLoaded.swf was loaded during that particular occurrence of ProgressEvent.PROGRESS. You'll see only a few numbers, because the requested SWF loads very quickly from your hard drive.

  2. Close the SWF and return to the Actions panel. Comment out lines 2 and 16:

    //addChild(loader);
    . . .
    //var req:URLRequest("toBeLoaded.swf"));

    And uncomment all the lines that were previously commented (just remove the double slashes at the beginning of each line other than 2 and 16).

At this point, you're adding loader to the display list of clip instead of the main timeline, you're telling clip to reposition itself when its content loads, and you're requesting the PNG file instead of the SWF.

  1. Test the movie again. Your results should look like Figure 14-1.

This time, the PNG is loaded (it looks identical; only the file format is different). The PNG is added to the stand in movie clip, which is positioned at 50 pixels to the right and 50 pixels down from the upper-left corner of the stage. The Output panel shows considerably more numbers than last time. In addition, a normally hidden pane is now showing in the upper half of Flash Player. Hang tight to see why that's happening, because it's a really neat tool.

Let's review the display list concept. In the first case, the requested file is loaded into the main timeline, which positions it in the stage's upper-left corner, unless you tell it otherwise with code. In the second case, the file is loaded into a movieclip, which you can position by hand. Positioning by hand means you can get a rough idea beforehand where your files will load—though you can certainly use ActionScript to reposition your movieclip containers, as shown in the completeHandler() function. That stand in container doesn't need to have anything in it, by the way. The outline and the words "stand-in" in the movieclip's text field (visible in the sample file) are there for illustrative purposes. Usually, your container will be an empty movieclip symbol.

A SWF is loaded into a movieclip, which changes position after the SWF loads.

Figure 14.1. A SWF is loaded into a movieclip, which changes position after the SWF loads.

Now, why is the Output panel showing so many numbers in Figure 14-1? That happens because Flash lets you simulate various modem speeds when you test downloads in the authoring environment. After you launch the SWF (Control

A SWF is loaded into a movieclip, which changes position after the SWF loads.

One loader, multiple SWFs

The beauty of the Loader class is that you need only one instance to load as many SWFs or images as you want, provided you're displaying only one per frame at a time. (If you plan to display more than one per frame, you'll need one Loader instance per SWF or image—just declare the necessary number of variables and give each its own name.) Let's see how you can reuse a single Loader instance numerous times.

  1. Open the BeachTrip.fla file from the Exercise/Preloader folder for this chapter. Inside, you'll see five layers in the main timeline, as shown in Figure 14-2: scripts, container (for the movieclip that holds the loaded images), text, buttons, and bg (for the background image stored in the library). Click around to get familiar with the FLA. The button symbols have the instance names btnPrev and btnNext. The text field in the center is dynamic, and it has the instance name tfLoadProgress. The container movieclip is empty, so it appears as a small circle. Click that circle, and you'll see that its instance name is container.

    With all the assets are in place, loading images will be easy.

    Figure 14.2. With all the assets are in place, loading images will be easy.

  2. Click info frame 1 of the scripts layer and type the following ActionScript:

    stop();
    
    var loader:Loader = new Loader();
    var req:URLRequest = new URLRequest();
    container.addChild(loader);
    
    // Loader events
    loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS,
    
    With all the assets are in place, loading images will be easy.
    progressHandler);
    function progressHandler(evt:ProgressEvent):void {
      var percent:int = Math.round(evt.bytesLoaded / evt.bytesTotal * 100);
      tfLoadProgress.text = percent.toString() + "%";
    }
    
    loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
    
    With all the assets are in place, loading images will be easy.
    completeHandler); function completeHandler(evt:Event):void { if (currentFrame != totalFrames) { enableButton(btnNext, true); } }

This is a multiframe movie, and we want it to stop dead in its tracks in frame 1. That explains the MovieClip.stop() method in the first line. Remember that you don't need an object reference prefix here, because Flash understands that the movieclip you're referring to is the main timeline. Next is the familiar Loader instance, again stored in a loader variable. In this case, the URLRequest instance is declared (req), and even instantiated (new URLRequest()), but not yet provided with a file name. That's going to happen in frames 2 through 6. To ensure that loaded images are placed in container's display list, loader is added as a child of container.

These three lines are followed by two event handlers, listening for ProgressEvent.PROGRESS and Event.COMPLETE, as before. The difference is that this progress event handler is sending its information to a dynamic text field (tfLoadProgress), rather than the Output panel. The complete handler is calling a custom function, enableButton(), which you'll write in just a moment. First, let's go over how this progress handler works.

In the previous exercise, you saw the ProgressEvent.bytesLoaded property referenced in terms of an evt parameter. That's happening again here. Additionally, you're making use of the bytesTotal property of the same ProgressEvent instance. An integer variable, percent, is declared and set to an arithmetic expression: evt.bytesLoaded / evt.byteTotal * 100. This calculates how many bytes have been loaded, divided by how many bytes there are total, multiplied by 100. This expression is rounded to the nearest integer by the Math.round() method. If there are 20,000 bytes total in the requested file and 10,000 of them have loaded, then the expression effectively means this:

10,000 / 20,000 * 100

Run the numbers, and you'll see that equates to 50, which is halfway (50%) loaded. Because percent is an int (integer) data type—as opposed to a string, as required by the text field—the Object.toString() method is invoked on percent as it is assigned to the text property of the tfLoadProgress text field. (All classes inherit from Object, so all objects inherit a toString() method.) The plus sign (+) concatenates the percent symbol (%) to the string value of percent, and that combined string is displayed in the text field. This way, users can see how much of the currently requested file has loaded as a percentage.

To understand the completeHandler() function, just remember that an if statement evaluates expressions to determine if they're true or false. In this case, two MovieClip properties are compared with the inequality operator (!=), which means "is not equal to." The expression inside the if statement's parentheses is true when the playhead is on any timeline frame but the last one. Only on the timeline's last frame is the value of its currentFrame property not equal to the value of its totalFrames property. Why do we care? In this BeachTrip.fla movie, the user is going to view a handful of JPGs by clicking a set of next and previous buttons. We don't want them to proceed while a requested image is still loading, so the btnNext button will be disabled in each frame until the Event.COMPLETE handler is triggered—on all frames but the last one (because there isn't a next frame after the last frame, of course).

  1. Head back to the Actions panel and add the following new ActionScript beneath what's already there:

    btnPrev.addEventListener(MouseEvent.CLICK, movePrev);
    btnNext.addEventListener(MouseEvent.CLICK, moveNext);
    
    function movePrev(evt:MouseEvent):void {
      prevFrame();
    }
    function moveNext(evt:MouseEvent):void {
      nextFrame();
    }

This should be pretty familiar. Two button symbols, with the instance names btnPrev and btnNext, are associated with custom functions—event handlers—triggered by MouseEvent.CLICK. These functions are named movePrev() and moveNext(), respectively. Each function invokes either MovieClip.prevFrame() or MovieClip.nextFrame(), as relevant, in order to move the main timeline one frame back or one frame forward.

That takes care of buttons in their active state (they're active by default). What's missing is the custom enableButton() function that causes buttons to become active or inactive as desired.

  1. Add the remaining new ActionScript to frame 1 beneath the code already present:

    function enableButton(btn:SimpleButton, isActive:Boolean):void {
      btn.mouseEnabled = isActive;
      if (isActive) {
        btn.alpha = 1;
      } else {
        btn.alpha = 0.5;
      }
    }
    enableButton(btnPrev, false);

This function accepts two parameters, arbitrarily named btn and isActive. The first parameter refers to a SimpleButton instance (in other words, a button symbol); the second is a Boolean value (true or false). Inside the function, the passed-in button symbol is referenced by the btn parameter. Its SimpleButton.mouseEnabled property is set to the value of the isActive parameter. This makes the button either participate in mouse-related events (true) or ignore them (false). Next, an if statement checks the value of isActive. If true, the button's alpha property is set to 1 (100%)—in other words, fully opaque—otherwise, it's set to 0.5 (50%).

In the last line, this function is actually put to use. It renders the btnPrev button disabled—no mouse events and semitransparent—which makes sense in frame 1, because there's no previous frame to go back to.

  1. Now add keyframes in frames 2 through 6 in the scripts layer. You're going to add a few lines of ActionScript to each of these frames, and you'll quickly see the pattern.

  2. Click into frame 2, and use the Actions panel to enter the following:

    req.url = "beach01.jpg";
    loader.load(req);
    
    enableButton(btnPrev, false);
    enableButton(btnNext, false);

    Click in frame 3 and enter this ActionScript:

    req.url = "beach02.jpg";
    loader.load(req);
    
    enableButton(btnPrev, true);
    enableButton(btnNext, false);

    For frame 4, enter this code:

    req.url = "beach03.jpg";
    loader.load(req);
    
    enableButton(btnNext, false);

    Frame 5 gets this ActionScript:

    req.url = "beach04.jpg";
    loader.load(req);
    
    enableButton(btnNext, false);

    And finally, add the following code for frame 6:

    req.url = "beach05.jpg";
    loader.load(req);
    
    enableButton(btnNext, false);

In all cases, the req variable, declared in frame 1, has its URLRequest.url property set to a particular JPG. After that, the Loader.load() method is invoked on the loader instance with req as its parameter. This loads a different JPG for each frame, but in every case, the same loader and req instances do the loading. Because loader was added to the display list of container in frame 1, every JPG will load inside the container movieclip.

In frame 2, btnPrev is disabled (enableButton(btnPrev, false);), because frame 2 represents the beginning of the slide show. The user still shouldn't be allowed to step back to the previous frame. In frame 3, btnPrev is enabled (enableButton(btnPrev, true);), because from that point, the user should be able to move forward and backward. When the user actually clicks the btnPrev button in frame 3, the playhead will move to frame 2, which redisplays the first image and again disables prevButton.

In all frame scripts, btnNext is disabled, which keeps the user from proceeding until the current image's Event.COMPLETE handler is triggered. Remember that this event handler invokes enableButton() and passes it the parameters btnNext and true.

  1. Test your movie and use View

    With all the assets are in place, loading images will be easy.

Creating a movie preloader

As an alternative to loading external assets individually, you may want to use ProgressEvent.PROGRESS to inform the user about the load status of the SWF itself. The mechanism that takes care of this is commonly known as a preloader, and it's a fairly easy feat to accomplish. Preloaders can range from simple to incredibly complex. Regardless of the approach taken, the purpose of a preloader is to get something happening within that 15-second window of opportunity and have the user become engaged in the site almost immediately.

Here, you will create a movie preloader, using the yawning parrot animation seen elsewhere in the book. The goal of this exercise is to display the parrot animation when the SWF is visited for the first time, and to skip past that animation when the SWF has already been loaded. The skipping-past part happens because already-viewed SWFs are cached in the user's temporary Internet files, which means the comparison between loaded bytes and total bytes—evaluated here by an if statement in the first three lines of frame 1—sends the playhead past the animation without further ado. On the other hand, when a SWF is being viewed for the first time, the Event.COMPLETE event, which indicates that the SWF itself is fully loaded, is delayed. This allows the playhead to linger on the parrot animation, while a ProgressEvent.PROGRESS handler updates a percentage in the parrot's nested text field. Eventually, the complete event handler ushers the playhead along. Here's how it works:

  1. Open the NASATrip.fla file in your Exercise/Preloader folder for this chapter. Scrub through the timeline and you will see the yawning parrot, and will then be taken through a series of imported photos that collectively weigh over 200KB.

  2. Open the library and double-click the parrot movieclip to open it in the Symbol Editor. You will notice that we have added the necessary dynamic text field for you, in a box under the parrot (see Figure 14-3). This text field will keep the user informed of load progress.

  3. Click the Scene 1 link to return to the main timeline. Select the keyframe in frame 1 of the scripts layer and open the Actions panel. Type the following code:

    if (loaderInfo.bytesLoaded >= loaderInfo.bytesTotal) {
      gotoAndPlay(26);
    }
    loaderInfo.addEventListener(Event.COMPLETE,
    
    Creating a movie preloader
    completeHandler); function completeHandler(evt:Event):void { if (currentFrame == 15) { play(); } else { gotoAndPlay(26); } }
A yawning parrot and a dynamic text box are the key elements used in this preloader.

Figure 14.3. A yawning parrot and a dynamic text box are the key elements used in this preloader.

Let's take a moment and figure out what's going on, because as familiar as this should look by now, you're still seeing a few curveballs. This time, because the content is all internal to the SWF, you don't need a Loader instance. You're still checking loaded versus total bytes, and still listening for Event.COMPLETE, but in this case, the event is dispatched by a LoaderInfo instance (not ContentLoaderInfo, as with the previous example). The LoaderInfo instance is accessed by way of the DisplayObject.loaderInfo property, referenced in terms of the main timeline. This works because the main timeline is a MovieClip instance, which inherits from the DisplayObject class.

When a user sees this SWF in a browser, the Event.COMPLETE event is dispatched after all the frames in the main timeline have loaded. During the first viewing, this event will be delayed until the SWF's full content (more than 200KB) has loaded. Thanks to this delay, and because the bytesLoaded property of the main timeline's LoaderInfo instance isn't yet greater than or equal to bytesTotal, the playhead will easily make it to frame 15—which features the yawning parrot—and wait there until instructed to continue. Of course, the playhead won't wait unless you tell it to stop, so that's your next step.

  1. Add a keyframe to frame 15 of the main timeline's scripts layer. Use the Actions panel to add the following single line of ActionScript:

    stop()

Thanks to that MovieClip.stop() method, the timeline will wait until the delayed Event.COMPLETE event is dispatched. As you can see from the if statement inside Step 3's completeHandler() function, the timeline will simply resume the play() method from that position as long as the function is triggered while the main timeline's MovieClip.currentFrame property is 15.

On the other hand, if the SWF is already cached on the user's computer, the completeHandler() function is triggered immediately—while the playhead is still in frame 1—which means the MovieClip.gotoAndPlay() method will send the playhead straight to frame 26, past the parrot movieclip. Bingo. So that explains part of the endeavor, but this isn't enough to update the text field inside the parrot symbol. For that, you'll need to write a ProgressEvent.PROGRESS handler. No time like the present.

  1. Add the following new ActionScript below the existing stop() method in frame 15 of the scripts layer:

    loaderInfo.addEventListener(ProgressEvent.PROGRESS,
    
    A yawning parrot and a dynamic text box are the key elements used in this preloader.
    progressHandler); function progressHandler(evt:ProgressEvent):void { var percent:int = Math.round(evt.bytesLoaded / evt.bytesTotal * 100); parrot.percentage.text = percent.toString() + "%"; }

This code hasn't changed, in principle, from the percentage arithmetic you saw in the previous example. As with the Event.COMPLETE handler, this event is associated with the loaderInfo property of the main timeline. The progressHandler() function calculates a percent variable from the evt parameter's bytesLoaded and bytesTotal properties, multiplied by 100 and rounded to the nearest integer. The result, once again, is sent to the text property of a dynamic text field. But this time, the text field's instance name is percentage, and it lives inside a movieclip whose instance name is parrot. Remember that in cases where the SWF has already been viewed, the ActionScript in frame 15 is bypassed altogether.

The remainder of the code is a snap.

  1. In the scripts layer, insert keyframes at frames 39, 49, 59, and 69. Enter the following code into frame 39:

    stop();
    
    btnNext.addEventListener(MouseEvent.CLICK, clickHandler);
    function clickHandler(evt:Event):void {
      play();
    }

This stops the timeline on the first image and wires up a custom clickHandler() function to the MouseEvent.CLICK event, which is dispatched when a user clicks the btnNext symbol that is fully visible on this frame. All this function does is resume the stopped timeline, which needs to be instructed to stop in the remaining keyframes.

  1. Enter a stop(); method in frames 49, 59, and 69.

  2. Test the movie and verify that Flash Player immediately displays images from a NASA exhibition (Figure 14-4), with a button in the lower right to advance through the photos.

  3. Simulate a modem by selecting View

    A yawning parrot and a dynamic text box are the key elements used in this preloader.
The SWF skips past the preloader when already cached.

Figure 14.4. The SWF skips past the preloader when already cached.

Building a slide show with components and XML

The popularity of websites like Flickr and Photobucket prove that people like to share photos. Of course, this was true even before the Internet. But modern technology makes it easier than ever to whip out that tumbling, unfolding wallet and proudly show off all the kids, aunts, uncles, cousin Ed, and Finnegan, not only to friends, but to every human on the planet. At the rate most people take pictures, photo collections just keep growing. So, if you were to make a photo slide show in Flash, you would want to be sure it was easy to update. With components and XML, that goal is closer than you may think.

To explore the concept, we'll start in an interesting location: the Quanjude Roast Duck Restaurant in Beijing, China. During the course of writing this book, one of the authors was in Beijing. One night, he was enjoying dinner in the company of a couple of Adobe engineers, John Zhang and Zhong Zhou. Naturally, one of the dishes was duck and, because of the restaurant's history, there was a particular way in which the duck was served and to be consumed. The author was struggling, and Zhong Zhou called the waitress over to demonstrate the proper (and complex!) procedure. It involved a wafer-thin wrap, duck meat, sauces, scallions, and a couple of other treats, which were to be added in a particular order. It took a couple of tries, but the grimacing author finally nailed it. When he thanked Zhong Zhu for the lesson, Zhong said, "It's really simple if you first master the process."

Mastering the creation of a Flash slide show is a lot like preparing that duck dish: it is all about process. We are going to show you two ways of creating a slide show, but the process is essentially the same for both. In fact, you'll be using some of the same process for the MP3 and video players later in the chapter.

A tour of the Beijing art district

To start, we're going to walk you through a self-contained, "hard-wired" movie that displays a small collection of external JPGs and their captions. The number of JPGs, and the order in which they appear, are "baked in" to the SWF, which means the movie must be edited and republished to accommodate new images. This slide show features ComboBox and Button components to let people choose which JPGs they want to see, and it even uses the UILoader and ProgressBar components to load the images, so this will be something of a cumulative exercise.

Once the test model is complete, we'll free the photo-specific data from its dungeon and move it to an XML file, where it can leap free in the fields like a shorn sheep, or paddle merrily around a pond like a duck. Here we go!

  1. Start a new Flash document and save it as Slideshow.fla in this chapter's Exercise/Slideshow folder. Set the movie's dimensions to 320 × 480. Set the background color to whatever you like (we chose #336699).

  2. Create the following five layers: scripts, progress bar, loader, caption, and nav. Lock the scripts layer to avoid accidentally placing content in this layer.

  3. Open the Components panel (Window

    A tour of the Beijing art district
  4. Drag an instance of the UILoader component to the loader layer. Set its width to 300, height to 400, X position to 10, and Y position to 10. Give it the instance name loader.

  5. Captions will be displayed with a text field. Use the Text tool to create a dynamic text field in the caption layer. Set its width to 300, height to 28, X position. to 10 and Y position to 416. Give this text field the instance name caption. Make the font _sans,18pt, and white, so that it shows over the blue background.

  6. Drag an instance of the ComboBox component to the nav layer. Set its width to 220, X position to 10, and Y position to 450. Give it the instance name images.

  7. Drag an instance of the Button component to the nav layer. Set its width to 70, X position 240, and Y position to 450. Give it the instance name next. Open the Component Inspector panel and set the button's Label parameter to Next. At this point, you have something like the scaffolding shown in Figure 14-5.

    The parts are in place; time for the ActionScript

    Figure 14.5. The parts are in place; time for the ActionScript

Now it's time to bring these parts to life. For the most part, it's a matter of handling events for the components and populating the combo box.

  1. Click into frame 1 of the scripts layer and open the Actions panel. Here's the first chunk of code:

    import fl.data.DataProvider;
    
    var imageData:Array = new Array(
      {label:"798 Art District Photo 1", data:"798_01.jpg",
    
    The parts are in place; time for the ActionScript
    caption:"Lazy day on the street."}, {label:"798 Art District Photo 2", data:"798_02.jpg",
    The parts are in place; time for the ActionScript
    caption:"Wall art."}, {label:"798 Art District Photo 3", data:"798_03.jpg",
    The parts are in place; time for the ActionScript
    caption:"Angry and cute."}, {label:"798 Art District Photo 4", data:"798_04.jpg",
    The parts are in place; time for the ActionScript
    caption:"The modern and the ancient!"}, {label:"798 Art District Photo 5", data:"798_05.jpg",
    The parts are in place; time for the ActionScript
    caption:"Not sure what to make of this."}, {label:"798 Art District Photo 6", data:"798_06.jpg",
    The parts are in place; time for the ActionScript
    caption:"The power of the artist?"}, {label:"798 Art District Photo 7", data:"798_07.jpg",
    The parts are in place; time for the ActionScript
    caption:"Fashion shoot at a steam engine."}, {label:"798 Art District Photo 8", data:"798_08.jpg",
    The parts are in place; time for the ActionScript
    caption:"A street in the district."} )

The first line imports the DataProvider class, which is needed later when it's time to populate the combo box. After that, an arbitrarily named variable, imageData, is set to an instance of the Array class. Arrays are lists of whatever you put in them. You can use the Array.push() method on an instance to add elements to that instance, but you can also pass in the whole collection at once, which we've done here. This array has eight items, separated by commas, and each item is an instance of the generic Object class with three properties.

What, no new Object() statement? How are these objects being created? That's what the curly braces ({}) are for. It's a shortcut, and we're taking it. You'll remember from Chapter 11 that ComboBox instances can be supplied with label and data information, so that explains what those properties are in the array. The caption property is a custom addition.

  1. Press Enter (Return) a couple times and type in the following:

    var currentImage:int = 0;
    var req:URLRequest = new URLRequest();
    
    function changePicture(pict:int):void {
      pb.visible = true;
      caption.text = imageData[pict].caption;
      req.url = imageData[pict].data;
      loader.load(req);
    }
    changePicture(0)

The first line after the comment declares an integer variable, currentImage, and sets it to 0. This number will keep track of which image is being viewed. Next, a req variable holds an instance of the URLRequest class, which will used to request the current image file. The next several lines declare a custom function, changePicture(), which accepts a single parameter, pict. This function does the following three things:

  • Makes the ProgressBar instance visible (yes, it's already visible at this point, but because later code turns off its visibility when an image finishes loading, it needs to be set back).

  • Makes the text field display the current caption. The incoming pict parameter determines which element to retrieve from the imageData array (imageData[pict]), and that element's caption property is retrieved. When the value of pict happens to be 0, the expression effectively says imageData[0], which means, "Pull the first entry from the imageData list, please." Why start at zero? It's just one of those things; arrays start counting from zero rather than one.

  • Makes the Loader instance load the current image. Here, again, the imageData array is consulted, but this time from the relevant item's data property, which is assigned to the URLRequest.url property of the req variable. In turn, req is fed to the loader instance by way of the Loader.load() method.

Immediately after its declaration, the changePicture() function is called, with 0 as its parameter. You're displaying the first image and its caption.

Now we just need to hook up the components.

  1. Press Enter (Return) a couple times and type in the following:

    pb.source = loader;
    
    pb.addEventListener(Event.COMPLETE, completeHandler);
    function completeHandler(evt:Event):void {
      pb.visible = false;
    }

The first line associates the ProgressBar instance with the Loader instance. Thanks to the convenience of components, as the Loader component loads images, the progress bar will "automagically" know how to display load progress. The completeHandler() function makes the progress bar invisible when loading is complete.

  1. Press Enter (Return) a couple times and type in the following:

    images.dataProvider = new DataProvider(imageData);
    
    images.addEventListener(Event.CHANGE, changeHandler);
    function changeHandler(evt:Event):void {
      currentImage = images.selectedIndex;
      changePicture(currentImage);
    }

The first line populates the combo box by setting its ComboBox.dataProvider property to a new DataProvider instance (this is why we need the import statement at the top). All the DataProvider instance needs is an array whose elements have label and data properties, which is exactly what we have in imageData. The caption properties are extra, but they don't hurt anything. That first line shoves the whole imageData array's content into the combo box in one swoop.

Next, the Event.CHANGE event is handled for the combo box. The handler function calls the custom changePicture() function and sets the currentImage variable to a number determined by the combo box's current selection. (The selectedIndex property doesn't care what data is in the selection; it only reports the number of the current selection, and that's all the currentImage variable needs.) This variable is then used as the parameter to the changePicture() function, which updates the current photo.

  1. Press Enter (Return) a couple times and type in the following:

    next.addEventListener(MouseEvent.CLICK, clickHandler);
    function clickHandler(evt:MouseEvent):void {
      currentImage++;
      if (currentImage == imageData.length) {
        currentImage = 0;
      }
      images.selectedIndex = currentImage;
      changePicture(currentImage);
    }

Here, the MouseEvent.CLICK event is handled for the button. The handler function does the following:

  • Increments the currentImage variable by one.

  • Checks to see if currentImage shares the same value as the expression imageData.length (the number of items in the imageData array). If so, it means the user has clicked often enough to progress through all the images, so currentImage is set back to 0.

  • Sets the combo box's current selection to currentImage, to keep the combo box in sync with button clicks.

  • Calls the custom changePicture() function and passes it currrentImage as its parameter.

  1. Test the movie. You'll be treated to a mini-tour of the 798 Art District in Beijing, China. Click the Next button to flip through the pictures in sequence, as shown in Figure 14-6, or use the combo box to skip around.

    A few quick components and a bit of ActionScript, and you're off!

    Figure 14.6. A few quick components and a bit of ActionScript, and you're off!

  2. To simulate image downloads, so you can see the progress bar in action, select View

    A few quick components and a bit of ActionScript, and you're off!

Extending the tour

As it turns out, wandering through the 798 Art District of Beijing makes a decent metaphor for this exercise, because after all of this careful examination of the art in the galleries, we're about to uncover a treasure in a gallery just a few more paces up the street.

Save your file to keep everything safe. Now select File

Extending the tour
import fl.data.DataProvider;

var xmlDoc:XML = new XML()
var xmlLoader:URLLoader = new URLLoader()
var xmlReq:URLRequest = new URLRequest("slideshow.xml")
xmlLoader.load(xmlReq)

xmlLoader.addEventListener(Event.COMPLETE,
Extending the tour
xmlCompleteHandler)
function xmlCompleteHandler (evt:Event):void { xmlDoc = XML(evt.target.data) images.dataProvider = new DataProvider(xmlDoc) changePicture(0) };

The imageData array is gone completely. In its place stands our trusty XML loading formula. The only differences here are the variable names. The URLLoader instance, for example, has been changed to xmlLoader, because loader is already in use as the instance name for the UILoader component. In the same way, the URLRequest instance is named xmlReq, because req is used later in the code, and the XML's completeHandler() function is named xmlCompleteHandler().

This time, we're loading the file slideshow.xml, and that's where the former imageData content now resides. Translated into XML, it looks like this:

<slideshow>
  <slide label="798 Art District Photo 1" data="798_01.jpg"
Extending the tour
caption="Lazy day on the street." /> <slide label="798 Art District Photo 2" data="798_02.jpg"
Extending the tour
caption="Wall art." /> <slide label="798 Art District Photo 3" data="798_03.jpg"
Extending the tour
caption="Angry and cute." /> <slide label="798 Art District Photo 4" data="798_04.jpg"
Extending the tour
caption="The modern and the ancient!" /> <slide label="798 Art District Photo 5" data="798_05.jpg"
Extending the tour
caption="Not sure what to make of this." /> <slide label="798 Art District Photo 6" data="798_06.jpg"
Extending the tour
caption="The power of the artist?" />
<slide label="798 Art District Photo 7" data="798_07.jpg"
Extending the tour
caption="Fashion shoot at a steam engine." /> <slide label="798 Art District Photo 8" data="798_08.jpg"
Extending the tour
caption="A street in the district." /> </slideshow>

This is practically the same as the previous array, except that this time, it's in a separate XML document instead of being hard-wired into the ActionScript.

Let's take another look at the Event.COMPLETE event handler for the xmlLoader instance. The function runs as follows:

function xmlCompleteHandler(evt:Event):void {
  xmlDoc = XML(evt.target.data);
  images.dataProvider = new DataProvider(xmlDoc);
  changePicture(0);
}

Notice that the DataProvider handling has been moved here from its former position next to the combo box Event.CHANGE handler. Why? Because under the circumstances, the combo box can't be populated until the XML has loaded. Next, the changePicture() call has also been moved here from its earlier position. Why? Same reason: until the XML loads, the changePicture() has no reference for what image to summon.

Two more paces!

At or near line 21, you'll find the changeFunction() declaration. You'll need to tweak two lines (changes in bold):

function changePicture(pict:int):void {
  pb.visible = true;
  caption.text = xmlDoc.slide[pict].@caption;
  req.url = xmlDoc.slide[pict].@data;
  loader.load(req);
}

Instead of pulling from the old imageData array, the text field and UILoader component now draw their information from the xml instance, using E4X syntax to specify the relevant <slide> element attributes. Here, the function's incoming pict parameter serves the same purpose as it did before: it specifies which <slide> element to consult.

Note

Don't forget to delete what used to be the last line in this chunk: that is, changePicture(0); which is now called inside the xmlCompleteHandler() function. It's easy to miss!

Here are the last touch-ups. There's a reward in sight! First, delete the following data provider line (which has been moved to the xmlCompleteHandler() function):

images.dataProvider = new DataProvider(imageData)

Finally, revise one reference in the button's event handler (new code in bold):

function clickHandler(evt:MouseEvent):void {
  currentImage++;
  if (currentImage == xmlDoc.slide.length()) {
    currentImage = 0;
  }
  images.selectedIndex = currentImage;
  changePicture(currentImage);
}

Since imageData is no more, that line depends on the number of <slide> elements, instead.

Test the movie and watch the show again. If you think you missed a step, compare your work to the SlideshowXML.fla work in this chapter's Complete folder.

Now that the movie has become XML-ified, you can have some fun editing the slideshow.xml file and running the SWF to see the changes. For example, delete the first three <slide> elements and test the movie again. Like magic, only the three remaining slides and captions display. Change the wording of one of the captions, and then run the SWF again. Change the order of the order of the <slide> elements. With every edit, the SWF takes these changes effortlessly in stride.

Building an MP3 player with XML

When people get around to working with audio in Flash, one of the more common requests is, "Can I make my own MP3 player?" After reading Chapter 5, you already know the answer is yes. By the end of that chapter, you had the beginnings of an MP3 player that could load tracks from a ComboBox instance, as well as toggle playback with a Play/Pause button. In this exercise, we're going to pick up with that project and continue to flesh it out.

Note

Thanks again to Benjamin Tayler, Bryan Dunlay, Philip Darling, and Robbie Butcher, of Tin Bangs (http://www.tinbangs.com/) for the generous use of their music.

There is going to be a lot going on here, so we suggest you set aside sufficient time to carefully follow along. You're about to be introduced to several new and fundamental concepts that will require your attention. Among them are the following:

  • Creating buttons that go the previous or the next audio track

  • Creating a seek slider that allows you to move through an audio selection

  • Creating a volume slider that allows the user to adjust the audio volume

  • Displaying an audio track's ID3 information

The key to this exercise is understanding technique. Although there will be a lot going on, you will discover everything presented here builds upon what you have learned in the book. In the previous exercise, for example, the XML version of the slide show had a Next button. Here you'll have that too, along with the addition of a Prev button. And, again, the external files will be loaded from XML.

This exercise is designed to follow a fairly standard workflow, which is to assemble your assets first, and then "wire them up" using ActionScript. This time, instead of components, you'll be creating some of your own controls.

Setting up the external playlist

The first order of business is to move the MP3 data to an XML file. Open the TinBangs.fla file found in the Exercise/MP3Player folder for this chapter. This file is functionally identical to the one in the Complete folder for Chapter 5. The only difference is that the code comments have been made more obvious, like this:

////////////////////////////////////////
// Obvious code comment
////////////////////////////////////////

Why? Because this project is going to have a lot of ActionScript, and these striking "mile markers" help organize things visually. Why so many slashes? ActionScript ignores them after the first two in the line, so the rest are part of the comment.

The first task is to swap out the Array instance, songList, for an external XML document, just as you did for the Beijing slide show. Doing this will reacquaint you with the existing ActionScript in place.

  1. Click info frame 1 of the scripts layer, open the Actions panel, and then locate the songList variable declaration on line 13, which looks like this:

    var songList:Array = new Array(
      {label:"Select a song", data:""},
      {label:"White Lies (Timekiller)", data:"WhiteLies(Timekiller).mp3"},
      {label:"Young Lions", data:"YoungLions.mp3"},
      {label:"Your Sky is Falling", data:"YourSkyIsFalling.mp3"}
    );

    Delete those lines of code and replace them with the following:

    var songList:XML = new XML();
    var loader:URLLoader = new URLLoader();
    var xmlReq:URLRequest = new URLRequest("playlist.xml");
    loader.load(xmlReq);
    
    loader.addEventListener(Event.COMPLETE, completeHandler);
    function completeHandler(evt:Event):void {
      songList = XML(evt.target.data);
      songsCB.dataProvider = new DataProvider(songList);
    };

    There's nothing new here. The XML instance is named songList in this case, to minimize the impact on the rest of the code, which already refers to the song data by that name. A URLRequest instance already exists as req, so the new one here is named xmlReq. The requested file is now playlist.xml, whose contents you'll see in just a moment. The Event.COMPLETE handler sets songList to the loaded XML document's data, and then passes that to the ComboBox.dataProvider property of the songsCB combo box.

    That last line inside the completeHandler() function—the one that refers to the data provider—originally appeared among the lines of code that configured the ComboBox instance, just before the line that reads addChild(songsCB);. You'll still see it there (should be at or near line 35 at this point), so delete it. (You only need to set the combo box's data provider once, and that needs to happen inside the completeHandler() function, after the XML has loaded.)

    Just so you can see how similar the XML is to the original array, here's the content of the playlist.xml file:

    <playlist>
      <song label="Select a song" data="" />
      <song label="White Lies (Timekiller)" data=
    
    Setting up the external playlist
    "WhiteLies(Timekiller).mp3" /> <song label="Young Lions" data="YoungLions.mp3" /> <song label="Your Sky is Falling" data="YourSkyIsFalling.mp3" /> </playlist>

    Note that, as with the array, the first entry specifies a label attribute but leaves the data attribute blank.

  2. Test the movie so far. It looks and works the same as it did at the end of Chapter 5, but now the MP3 file data is stored outside the FLA.

It's time to add the new stuff. But first, the authors would like to make a community service announcement.

Polishing up the symbols

We interrupt this program to introduce you to a fact of life that happens with collaborative Flash work. As mentioned in Chapter 5, the controller bar—with its VCR buttons and slider control—was created in Adobe Illustrator, and then imported into Flash. For the sake of demonstration, let's assume the designer didn't know how the controls would ultimately be used. If you don't think this will happen in your own Flash journeys, get ready to think again! In fact, count on it.

As a matter of good habit, you'll want to rename your library assets to better suit their actual use in this project. In addition, to improve the user's interactive experience, you'll also want to use the drawing tools to give these VCR buttons—which are actually movieclips—a bigger clickable surface area. This is especially important for the Pause button, because without the fix, the mouse could easily slip between the two vertical bars of the pause icon.

Renaming library assets

Renaming library assets is the sort of work that seems like housekeeping. And it is. but don't underestimate its value! When deadlines loom and a manager is breathing down your neck, it helps to know your library territory like the back of your hand. Take VolumeSlider, for example. In this MP3 player, that symbol is actually going to indicate how much of the audio has played. By dragging that slider, you'll be able to seek to various parts of the song. So, let's give it, and the other assets, better names.

  1. Open the Library panel for the TinBangs.fla file. Locate the library's AudioPlayer.ai Assets folder, and you'll see a number of subfolders that ultimately contain the movieclips used for the controls in the Player layer of the Timeline panel. These include a handful of movieclips and subfolders whose names don't presently suit the purposes of this MP3 player: FastForward, Layer 7, VolumeSlider, Rewind, and VolumeBar.

  2. Double-click the FastForward folder, as shown in Figure 14-7, and rename it to Next. Do the same with the FastForward movieclip. This is, after all, a button that skips to the next song in the playlist, not a fast-forward button.

    Appropriately naming library assets helps when you resume work after a break.

    Figure 14.7. Appropriately naming library assets helps when you resume work after a break.

  3. Rename the VolumeSlider symbol to SeekKnob. Do the same with its containing folder, Layer 7.

  4. Rename the Rewind symbol and its folder to Prev.

  5. Complete your cleanup by renaming the VolumeBar symbol and its folder to SeekBar.

Improving the controls

The previous steps helped you as a designer/developer. Now it's time to help the user.

  1. Double-click the Play symbol to enter its timeline. Drag the playhead to frame 2, and you'll see two vertical bars that represent "pause," as shown in Figure 14-8.

Be sure to keep your mouse-related assets mouse-friendly.

Figure 14.8. Be sure to keep your mouse-related assets mouse-friendly.

Granted, this symbol has been zoomed in quite a bit in the figure, but even at actual size, it's easy to see how the mouse can slip between the two bars, or accidentally miss the symbol altogether by slipping too far left or right. If this were a button symbol, the solution would be elementary: head to the Hit frame and give the button a nice, sizable hit area. With movieclips, which don't have a Hit frame, you need to get creative. In this case, the solution happens in a layer named hit area.

  1. Click frame 1 of the hit area layer, and you'll see a pixelated rectangle appear behind the "play" arrow icon, as shown in Figure 14-9.

    A low-alpha shape provides additional "surface area" for the mouse.

    Figure 14.9. A low-alpha shape provides additional "surface area" for the mouse.

This rectangle is a simple shape, drawn with the Rectangle tool. The reason you can't see it—until the shape is selected—is because the shape's fill color is set to 0% Alpha. From a visual standpoint, it's imperceptible, but when the user hovers a mouse over this symbol, even the invisible shape provides a larger clickable surface area.

Notice that the rectangle spans frames 1 and 2, so that it appears behind both the play and pause icons. This makes the hit area useful, regardless where this symbol's playhead appears.

Note

It is little things like this—giving a shape an opacity value of 0—that will separate you from the rest of the pack. This little trick takes maybe 2 to 3 minutes to accomplish. Someone who is unfamiliar with this will easily spend an hour trying to make the symbol "idiot-proof." This is a classic case of letting the software do the work instead of you overthinking it. In fact, the next step shows you how to do it yourself.

The other VCR controls, and the SeekKnob symbol, need the same treatment. You can draw these shapes if you like, or you can let Flash do the work for you. Let's look at both ways.

  1. Double-click the Prev symbol to enter its timeline. Rename the Layer 1 layer to arrows, then create a new layer named hit area beneath the first. In the hit area layer, use the Rectangle tool to draw a 20 × 20 pixel square with no stroke, and a fill color of #FFFFFF (white) set to 0% Alpha. Position the square so that it evenly fills the area behind the "prev" double arrows (we used an X position of -2 and a Y position of 2).

  2. Right-click (Ctrl-click) frame 1 of the hit area layer and select Copy Frames from the context menu. Now double-click the Next symbol to enter its timeline. Rename Layer 1 to arrows, and then create a new layer beneath the first (no need to name it). Right-click (Ctrl-click) frame 1 of the new layer and select Paste Frames from the context menu. This accomplishes two things: it pastes the shape with the 0% Alpha and also renames the layer to hit area for you. Pretty slick! Reposition the shape so that it evenly fills the area behind the "next" double arrows (we used an X position of -4 and a Y position of 2).

  3. Using whichever approach you prefer, position a similar shape beneath the hollow rectangle in the SeekKnob symbol. In our case, we renamed that symbol's Layer 1 layer to knob, and then pasted the same shape into a new layer. We changed the dimensions of the shape to 12 × 10 and positioned it at an X position of -2.5 and a Y position of -1.

    Note

    Okay, so why two ways of doing the same thing? We are fond of telling anyone who will listen that there are usually 6,000 ways of doing anything in this business. What's the right way? Who cares? The only time someone cares is when it doesn't work.

As it turns out, the Illustrator designer forgot two widgets: a volume slider, which lets the user adjust volume, and a loading indicator, which tells the user an MP3 file is still loading. As often as not, you might need to create such assets yourself, but to speed things along, we've provided what you need in separate file named controls.fla. By using a technique we introduced in Chapter 3, you can quickly share the widgets from that FLA with your current working FLA.

  1. Select File

    A low-alpha shape provides additional "surface area" for the mouse.
  2. With the Player layer selected, drag the LoadingDisplay symbol from the newly opened controls.fla library to the right side of the stage, as shown in Figure 14-10 (we used X: 462, Y: 305). Check the TinBang.fla's own library, and you'll see the movieclip there as well. Easy as that, you now have a loading indicator.

    It's easy to drag in assets from another FLA's library.

    Figure 14.10. It's easy to drag in assets from another FLA's library.

  3. In the TinBangs.fla library, double-click the LoadingDisplay movieclip to open it in the Symbol Editor. Scrub the timeline, and you'll see that the symbol is nothing more than a series of dots that seem to spin.

  4. To make room for the volume slider, select the SeekBar symbol in the player background layer (the long red rectangle) and use the Property inspector to change its width to 138.

  5. With the Player layer selected, drag the VolumeSlider symbol from the controls.fla library to the spot you just opened up—to the right of the other controls and just beneath the loading indicator (we used X: 424, Y: 328).

When you drag the VolumeSlider symbol, an interesting thing happens in the TinBangs.fla library: not only does VolumeSlider appear, but VolumeBar and VolumeKnob come along for the ride, as shown in Figure 14-11. This is nothing to be alarmed about. These other symbols show up because they're nested inside VolumeSlider, so they piggyback their way in.

  1. Drag the volume icon graphic symbol from the controls.fla library to the stage, just to the left of the VolumeSlider symbol (we used X: 414, Y: 336). This is nothing more than an icon that helps indicate the purpose of the slider next to it.

  2. Double-click VolumeSlider in the TinBangs.fla library to open it in the Symbol Editor.

Dragging in a nested asset carries with it the asset's children.

Figure 14.11. Dragging in a nested asset carries with it the asset's children.

This symbol is a bit more complicated than the circle of dots from the previous shared asset, but you've already been introduced to all the concepts. As Figure 14-12 shows, you'll find three layers: knob, mask, and bar. The knob layer contains a rectangular symbol, VolumeKnob, whose shape is composed of a 0% Alpha fill. This is effectively an invisible button, like the hit area shape in step 2, except that the "button" is a movieclip. The mask layer contains five slanted columns, and the bar layer simply contains a red rectangle (this is the VolumeBar symbol). If you like, temporarily lock the mask and bar layers, and you'll see the masking in action. When this symbol is wired up, the user will be able to drag the invisible VolumeKnob symbol left and right. The VolumeBar symbol, partially hidden by the mask, will simply match the position of VolumeKnob, and the result will be an illusion: it will appear to the user that dragging left and right changes a red fill shared by the five slanted columns.

A low-alpha shape inside the rectangular movieclip provides "surface area" for the mouse.

Figure 14.12. A low-alpha shape inside the rectangular movieclip provides "surface area" for the mouse.

  1. Select Edit

    A low-alpha shape inside the rectangular movieclip provides "surface area" for the mouse.

With these assets in place, you're nearly ready to rock 'n' roll. Let's just make sure all the programmable assets have instance names, and then organize the timeline layers.

  1. Carefully select the VolumeBar and VolumeKnob symbols on the stage to verify that they've already been given instance names: volumeBar and volumeKnob, respectively.

  2. Return to the main timeline and, moving left to right, select each button in turn and verify they have the following instance names: btnPrev, btnPlay, and btnNext.

  3. Continuing toward the right, select the SeekKnob symbol and give it the instance name seekKnob. Give the SeekBar symbol the instance name seekBar. For VolumeSlider, make it volumeSlider. Moving up, give LoadingDisplay the instance name loadingDisplay. Finally, moving left again, give the text field the instance name songData.

  4. Select the Player layer by clicking its name. Now select Modify

    A low-alpha shape inside the rectangular movieclip provides "surface area" for the mouse.
  5. The Player layer is still there, but now empty—so delete it. Rename the Bar layer to player background and the interface layer to background image, as shown in Figure 14-13.

Now everything is tidy and much easier to locate.

With everything neatly organized, you're well prepared for a smooth ride.

Figure 14.13. With everything neatly organized, you're well prepared for a smooth ride.

Wiring up the MP3 player controls

Now it's time to add the ActionScript. Fortunately, you have a leg up, because the Play/Pause button is already programmed. You did that in Chapter 5. In order to proceed, we're going to tidy up the existing ActionScript, just as we did with the library and timeline assets. We'll use the obvious code comments to help plot out our travel route.

Click into the scripts layer and review what's currently in place. This includes the revision you made earlier in this section, where songList became an XML instance (it had previously been an Array instance). Compare your work carefully. Nothing has changed since you last touched this code, but see if you can recognize what's going on. We'll meet you on the other side.

import fl.controls.ComboBox;
import fl.data.DataProvider;

////////////////////////////////////////
// Variables
////////////////////////////////////////

var song:Sound;
var channel:SoundChannel;
var req:URLRequest;
var pos:Number;

var songList:XML = new XML();
var loader:URLLoader = new URLLoader();
var xmlReq:URLRequest = new URLRequest("playlist.xml");
loader.load(xmlReq);

loader.addEventListener(Event.COMPLETE, completeHandler);
function completeHandler(evt:Event):void {
  songList = XML(evt.target.data);
  songsCB.dataProvider = new DataProvider(songList);
};

////////////////////////////////////////
// ComboBox
////////////////////////////////////////

// prep
var songsCB:ComboBox = new ComboBox();
songsCB.dropdownWidth = 200;
songsCB.width = 200;
songsCB.height = 24;
songsCB.x = 26;
songsCB.y = 68;
songsCB.dataProvider = new DataProvider(songList);
addChild(songsCB);

// events
songsCB.addEventListener(Event.CHANGE, changeHandler);

function changeHandler(evt:Event):void {
  if (songsCB.selectedItem.data != "") {
    req = new URLRequest(songsCB.selectedItem.data);
    if (channel != null) {
      channel.stop();
    }
    song = new Sound(req);
    channel = song.play();
    btnPlay.gotoAndStop("pause");
  }
};
////////////////////////////////////////
// Buttons
////////////////////////////////////////

// prep
btnPlay.stop();
btnPlay.buttonMode = true;

// events
btnPlay.addEventListener(MouseEvent.CLICK, clickHandler);

function clickHandler(evt:MouseEvent):void {
  if (channel != null) {
    if (btnPlay.currentLabel == "play") {
      channel = song.play(pos);
      btnPlay.gotoAndStop("pause");
    } else {
      pos = channel.position;
      channel.stop();
      btnPlay.gotoAndStop("play");
    }
  }
};

It's worth noting that some of this code overlaps. (Don't worry if you didn't see it! That's a lot of ActionScript to pore through.) In the ComboBox block, for example, inside the changeHandler() function, notice that these two lines:

channel = song.play();
btnPlay.gotoAndStop("pause");

match these two lines in the Buttons block's clickHandler() function (relevant code in bold):

if (btnPlay.currentLabel == "play") {
  channel = song.play(pos);
  btnPlay.gotoAndStop("pause");
} else {

In simple projects, you don't need to lose any sleep over the occasional overlap. But it's definitely something you want to keep in mind. We've looked at some optimization already in this chapter (the preloader exercise), and there's more of that coming in Chapter 15. The concept of optimization applies as much to the structure of your ActionScript as it does to your assets. As we wire up the controls, you'll find that numerous event handlers are going to load, pause, or play a song, so it makes good sense to write custom functions to perform those actions. Then those functions can be reused by your various event handlers. Doing this makes your ActionScript easier to read and, ultimately, there's less of it to type. The result is code that is easier to deal with. We'll now make the revisions to get rid of the overlap.

Add the following new variables to the code inside your Variables block near the top (new code in bold):

////////////////////////////////////////
// Variables
////////////////////////////////////////

var song:Sound;
var channel:SoundChannel;
var xform:SoundTransform;
var req:URLRequest;
var pos:Number;
var currentSong:int;
var rect:Rectangle;

Like the existing variables, the three new ones are declared, but not yet set to anything. The xform variable will be a SoundTransform instance, for controlling audio volume. currentSong is just like the currentImage variable in the Beijing slide show (here, it's used to keep track of the currently playing song). rect will be a Rectangle instance, which is used later to control the draggable distance of the seek and volume slider knobs.

Skip down to the ComboBox block. Within the changeHandler() function, change what you see so that it looks like this (revision in bold):

function changeHandler(evt:Event):void {
  if (songsCB.selectedItem.data != "") {
    currentSong = songsCB.selectedIndex;
    loadSong(songsCB.selectedItem.data);
  }
};

This trims up the function quite a bit. Instead of dealing with the loading code here—URLRequest, checking if the channel instance is null, and so on—those lines have been moved to a set of new functions you're about to write. These new functions will fit between the ComboBox block and the Buttons block. Copy one of those code block commented headings and paste it after the changeHandler() function. Change its caption to Song Functions, like this:

////////////////////////////////////////
// Song Functions
////////////////////////////////////////

After this commented heading, type the following new function:

function loadSong(file:String):void {
  req = new URLRequest(file);
  pauseSong();
  song = new Sound(req);
  song.addEventListener(Event.OPEN, soundOpenHandler);
  song.addEventListener(Event.COMPLETE, soundCompleteHandler);
  song.addEventListener(Event.ID3, soundID3Handler);
  playSong();
};

This is an example of double-dipping, as far as code optimization is concerned. You might even call it "passing the buck." Just as we passed along the loading code earlier, we're passing along some of the ActionScript here again, to two additional custom functions: pauseSong() and playSong(). It's all in the name of keeping the ActionScript lean.

Notice that the loadSong() function accepts a string parameter, which will be referenced by the file variable by code inside the function. In the previous code, the value of this parameter was supplied by the expression songsCB.selectedItem.data, which retrieved the MP3's file name from the ComboBox component's current selection. In later code—namely, the Prev and Next button event handlers—you'll see this same value supplied in other ways.

The req variable, declared early on in the Variables block, is finally set to a new instance of the URLRequest class, which allows the MP3 file to be requested. If a song is currently playing, it's stopped by virtue of the pauseSong() function (you'll see how in the next block of code).

The song variable is set to a new Sound instance, and because the req variable is fed right into the expression new Sound(), we bypass the need for the Sound.load() method. With the new Sound instance in place, it's ready for three event listeners: one when the MP3 is loaded (Event.OPEN), one when loading is complete (Event.COMPLETE), and one when the MP3 file's ID3 tags are encountered (Event.ID3). The event handler functions are intuitively named, and you'll see how they're used shortly.

Finally, the custom playSong() function rolls the music—which makes this a good idea to write those functions.

Let's continue adding code. Press Enter (Return) a couple times, and then type the following new ActionScript:

function playSong(pos:Number = 0):void {
  channel = song.play(pos);
  btnPlay.gotoAndStop("pause");
  seekKnob.addEventListener(Event.ENTER_FRAME, seekKnobUpdate);
};
function pauseSong():void {
  seekKnob.removeEventListener(Event.ENTER_FRAME, seekKnobUpdate);
  if (channel != null) {
    channel.stop();
  }
  btnPlay.gotoAndStop("play");
};

Most of this should seem familiar, but there's some new stuff, too. The playSong() function accepts a parameter, just like loadSong() does, but here, the parameter is already set to a value (pos:Number = 0)—so what's going on? New to ActionScript 3.0, this feature lets you provide default values for your parameters. What's it good for? Well, when referenced from the loadSong() function, playSong() isn't provided with a value; therefore, a default value of 0 is assumed. This will cause the song to play from the beginning when pos is passed into the first line inside this function: channel = song.play(pos);. As you'll see later, the Pause/Play button does pass in a value, because it lets you stop the music and resume from where you left off. In that case, the pos parameter will be supplied with a value, and the default 0 will be overruled.

So, when a song is played, it's assigned to the channel instance, and the btnPlay movieclip is sent to the pause label of its timeline. The other thing that needs to happen—and this is a glimpse ahead—is that the SeekKnob symbol needs to start moving along its track to indicate how much of the song has played. This is managed by way of an Event.ENTER_FRAME event, which triggers a seekKnobUpdate() function you'll write later in the exercise.

Once you understand the playSong() function, the pauseSong() function isn't hard to follow. It doesn't need a parameter. All it does is unhook the seekKnobUpdate() event handler, which halts the traveling of the SeekKnob symbol; determine if the channel instance is null, and if not, stop its playback; and send btnPlay's timeline to the play label.

Earlier, we wired up three Sound-related event listeners. It's time to write the handler functions for two of those. Press Enter (Return) a couple times and type the following new ActionScript:

// events
function soundOpenHandler(evt:Event):void {
  loadingDisplay.visible = true;
  loadingDisplay.play();
};
function soundCompleteHandler(evt:Event):void {
  loadingDisplay.stop();
  loadingDisplay.visible = false;
};

These functions are straightforward. After a quick // events comment, the soundOpenHandler() function simply sets the visibility of the LoadingDisplay symbol to true (this is the spinning dots symbol, imported from a shared library). To actually get the dots to spin, it invokes the MovieClip.play() method on the loadingDisplay instance name. This event handler function responds to the Event.OPEN event, which occurs whenever an MP3 file is loaded.

The soundCompleteHandler() function responds to the Event.COMPLETE event, which means a requested MP3 file has fully downloaded. As you can see, this handler stops the spinning dots and once again turns off the visibility of that movieclip.

Where's the Event.ID3 handler? It could certainly have been written here. Really, it's just a matter of organizational preference, and there's no arguing taste. To us, it makes sense to build out the rest of the code, which is composed entirely of event handlers, in the order in which the buttons and controls appear on the stage. We'll start with the buttons, move rightward to the sliders, then up to the dots, and then left again to the text field. It's the text field that does the two-step with the Event.ID3 event handler, so we'll meet it again at the end.

Ready for a quick intermission? Test the movie where it stands, and you'll see three error messages in the Compiler Errors panel, as shown in Figure 4-14. Those errors are due to three references to two event handler functions that don't exist yet. One of those is the Event.ID3 handler we just mentioned, located inside the loadSong() function. The other is the seekKnobUpdate() reference located in the playSong() and pauseSong() functions.

Find these addEventListener() and removeEventListener() references in the functions just mentioned, and comment them out, like this:

//song.addEventListener(Event.ID3, soundID3Handler);
//seekKnob.addEventListener(Event.ENTER_FRAME, seekKnobUpdate);
//seekKnob.removeEventListener(Event.ENTER_FRAME, seekKnobUpdate);

Test the movie again. The errors disappear.

If you like, compare your work with TinBangsMilestone.fla in the Complete/MP3Player folder for this chapter. When you're ready to move on, you'll be wiring up the buttons.

But before you proceed, make sure to uncomment those three lines again!

Handling the button events

Remember that the Play/Pause button has already been programmed, which speeds things up a bit. Because we have the new playSong() and pauseSong() functions, you will need to make a few changes to what's there. Fortunately, this shortens the existing ActionScript, which is all part of the secondary plot for this exercise: code optimization. Let's do it.

In case you're not already there, click into frame 1 of the scripts layer again and open the Actions panel. Find the Buttons code block and update what you see to the following new lines (new ActionScript in bold):

////////////////////////////////////////
// Buttons
////////////////////////////////////////

// prep
btnPlay.stop();
btnPlay.buttonMode = true;
btnPrev.buttonMode = true;
btnNext.buttonMode = true;

// events
btnPlay.addEventListener(MouseEvent.CLICK, playHandler);
btnPrev.addEventListener(MouseEvent.CLICK, prevHandler);
btnNext.addEventListener(MouseEvent.CLICK, nextHandler);

There's nothing difficult here. The Prev and Next buttons need their MovieClip.buttonMode properties set to true, simply because—like Pause/Play—they're movieclips that are masquerading as buttons. Following suit, they get assigned to their respective event handlers. Because there are now three click-related event handlers, the function originally assigned to the btnPlay instance has been renamed playHandler() (it was formerly clickHandler()).

Speaking of clickHandler(), you need to update it so that it reflects the following new code, making sure to rename it as shown (revisions in bold):

function playHandler(evt:MouseEvent):void {
  if (channel != null) {
    if (btnPlay.currentLabel == "play") {
      playSong(pos);
    } else {
pos = channel.position;
      pauseSong();
    }
  }
};

Here's where the custom functions begin to earn their keep. The behavior of the playhandler() function is intact, but thanks to the playSong() and pauseSong() functions, the actual lines of code have been reduced.

Notice, as before, that on one side of the else clause, the pos variable is set to the SoundChannel.position property of the channel instance. On the other side of that else clause, pos is passed into the playSong() function as a parameter. When you look at the playSong() function definition in the previous section, you'll see that the variable between the function's parentheses also happens to be called pos. That's a coincidence, and nothing more. Whether or not they're named the same, a value that represents the song's position is conveyed, and that's all that matters.

Note

In real-world situations, you'll often find that project requirements change. In fact, it's rare when they don't! When this happens, you'll find yourself better equipped to respond to revisions when you're dealing with reusable functions. If the concept embodied by the playSong() function happens to change, you need to edit only one function in a single place, rather than needing to use a hunt-and-peck approach to touch up numerous blocks of code.

The Prev and Next buttons are taken care of with one function apiece. Add the following two event handlers beneath the playHandler() function:

function prevHandler(evt:MouseEvent):void {
  currentSong--;
  if (currentSong < 1) {
    currentSong = songList.song.length() - 1;
  }
  songsCB.selectedIndex = currentSong;
  loadSong(songList.song[currentSong].@data);
};
function nextHandler(evt:MouseEvent):void {
  currentSong++;
  if (currentSong > songList.song.length() - 1) {
    currentSong = 1;
  }
  songsCB.selectedIndex = currentSong;
  loadSong(songList.song[currentSong].@data);
};

These should be reminiscent of the Next button in the Beijing slide show. Here, these two functions are metaphorically mirror images of each other. In prevHandler(), the value of the currentSong variable is decreased by 1 (currentSong--). If currentSong is less than 1—which it will be, eventually—then the variable is set to one less than the total number of <song> elements in the XML document (songList.song.length() - 1). Why one less than the total? Because arrays start with 0, rather than 1. Why aren't we checking if currentSong is less than 0, then? Because the first entry in the XML, and therefore the ComboBox component, is the "dead" entry without data—the one that says Select a song.

Once currentSong is updated, the selected index of the ComboBox component is configured to reflect that change, and the custom loadSong() function is instructed to load the new current selection. The parameter's expression happens to be based on the XML content, using a bit of E4X syntax—songList.song[currentSong].@data—but it could have just as easily be taken from the ComboBox component.

In contrast, the nextHandler() function increments the value of currentSong, and then sets it back to 1 if it goes beyond one less than the total number of <song> nodes in the XML—in other words, the reverse. After that, the ComboBox component is updated and, once again, the loadSong() function is instructed to load the current selection.

Note

Wait a minute! The last two lines of these functions overlap! Shouldn't they be folded into yet another function—maybe updateSong()? You could certainly do that. Optimization is as much an art as a science, and we encour-age you to find your personal line in the sand.

Programming the sliders

You're about to enter into the thickest part of the ActionScript for this project, so you may want to pull out your machete. Actually, it's not so bad, once you strike past the first bit of foliage. The mosquitoes are pretty big, true, but that makes it all the easier to swat them with the blade.

Joking aside, the ActionScript for the sliders isn't going to make your head explode. To understand it better, it helps to take a closer look at the way the slider-related symbols are laid out. Their registration points, in particular, are designed to make the math as easy as possible, so let's take a gander. Figure 14-14 shows these registration points.

The symbols' registration points are carefully chosen to make the code easier.

Figure 14.14. The symbols' registration points are carefully chosen to make the code easier.

There are two parts to this slider: the SeekKnob symbol and the SeekBar symbol. When the knob is positioned on the bar's left edge, as shown in Figure 14-14, notice that the registration points of each symbol (the two pluses along each symbol's upper edges) are aligned. This happens because SeekBar's registration point—its 0,0 position—is located in that symbol's upper-left corner. SeekKnob's registration point, on the other hand, is located in that symbol's top center.

Both of these symbols are positioned 260 pixels from the left side of the stage. If SeekKnob's registration point was also in its own upper-left corner, it would have to be offset by several pixels to look as if it were hugging the left edge of SeekBar. As it is, however, the numbers are easy. To coordinate its movements with SeekBar, all SeekKnob has to do is know SeekBar's horizontal position (seekBar.x) and take into consideration SeekBar's width (seekBar.width). Figure 14-15 gives a quick visual breakdown.

The position of the knob and position and width of the bar are the critical properties.

Figure 14.15. The position of the knob and position and width of the bar are the critical properties.

To position the knob along the bar's left edge, all you need to do set its MovieClip.x property to the bar's MovieClip.x property. To slide it halfway across, set the knob's x property to the x property of the bar, plus half of the bar's width. To shove it all the way over, set its x property to bar's, plus the full width of the bar. Keep this principle in mind as we work through the seek slider ActionScript.

To begin, copy another one of the commented code block headers and paste it below the last bit of ActionScript (nextHandler(), from the Buttons section). Change the header's caption to Seek slider, and then type in the following ActionScript, so that your code looks like this:

////////////////////////////////////////
// Seek slider
////////////////////////////////////////

// prep
seekKnob.buttonMode = true;

// events
seekKnob.addEventListener(MouseEvent.MOUSE_DOWN, seekStartDrag);

Like the Prev, Play/Pause, and Next movieclip "buttons," the seekKnob instance needs to have its buttonMode property set to true. When the user clicks it, you want the user to be able to start dragging that knob, so the MouseEvent.MOUSE_DOWN event is associated with a custom function you're about to write, called seekStartDrag(). That function is triggered when the user clicks the mouse (MOUSE_DOWN) on the seekKnob instance. Type the following new ActionScript:

function seekStartDrag(evt:MouseEvent):void {
  if (song != null) {
    pauseSong();
    rect = new Rectangle(seekBar.x, seekKnob.y, seekBar.width, 0);
    seekKnob.startDrag(true, rect);
    stage.addEventListener(MouseEvent.MOUSE_UP, seekStopDrag);
  }
};

If the song instance isn't null—it's null, for example, before a song is chosen from the combo box—then pause the song, in case it's playing. Next, define a Rectangle instance (stored in the rect variable), which will be used to constrain dragging to the desired location.

Rectangle instances are specified at a particular location (x and y) and at a particular width and height. In this case, we want the knob to be draggable only from the left side of the bar (seekBar.x, the first parameter) to the right side (seekBar.width, the third parameter). Its vertical position is fine where it is (seekKnob.y, the second parameter) and shouldn't vary from that, which means we set the rectangle to a height of 0 (the fourth parameter).

The MovieClip.startDrag() method, invoked on seekKnob, is fed two parameters: true, which snaps dragging to the symbol's registration point, and rect, which confines dragging to the dimensions just described.

Finally, a MouseEvent.MOUSE_UP event handler is associated with the stage, configured to trigger a custom seekStopDrag() function. Why is this association made with the stage, rather than with seekKnob? Because the user might just drag the mouse off the knob before releasing the mouse (MOUSE_UP). If the event handler were associated with seekKnob, then seekStopDrag() wouldn't be triggered. But when it's assigned to the stage, that pretty much means the mouse can be lifted anywhere, and the dragging routine will stop.

Here's the seekStopDrag() function. Type the following new ActionScript:

function seekStopDrag(evt:MouseEvent):void {
  seekKnob.stopDrag();
  playSong(song.length * (seekKnob.x - seekBar.x) / seekBar.width);
  stage.removeEventListener(MouseEvent.MOUSE_UP, seekStopDrag);
};

The first thing this function does is invoke MovieClip.stopDrag() on the seekKnob instance. That part is easy. The challenge comes in telling the song where to begin playing again, because it all depends on where the knob is currently positioned along the bar. To illustrate, let's imagine the user dragged the knob right to the middle, and let's pretend the song is exactly 60 seconds long. Let's use those figures and run the math.

Here's the actual expression:

song.length * (seekKnob.x - seekBar.x) / seekBar.width

Using the numbers we just agreed on, that equates to this:

60 seconds × (knob's position / bar's position) / bar's width
60 * (329 / 260) / 138

60, multiplied by the difference between 329 and 260 (namely, 69) is 4,140. Divided by 138, the final number is 30 seconds, which is exactly what's expected when the knob is dropped halfway across.

The final total of the arithmetic equation is fed into the playSong() function, which starts the song from whatever value, in seconds, is provided.

The last thing this function does is to tell the stage to stop listening for the MOUSE_UP event, because the event obviously just occurred (since this function handles it).

In the playSong() function definition, seekKnob is associated with an Event.ENTER_FRAME event, which tells the knob to continuously update its position according to how much of the song has played. Here's that function. Type the following new ActionScript:

function seekKnobUpdate(evt:Event):void {
  var pos:Number = seekBar.width * channel.position / song.length;
  if (!isNaN(pos)) {
    seekKnob.x = seekBar.x + pos;
  } else {
seekKnob.x = seekBar.x;
  }
};

Here's that pos variable again (a third one!). This one is unrelated to the other two, except in name. To the authors, pos just seems like an appropriate name for a variable for noting the position of something. In this case, pos is declared within the scope of this function and set to an expression that effectively does the opposite of the expression shown earlier. Let's run the numbers again, assuming that, at this very moment, our hypothetical 60-second song has played halfway through. Here's the actual expression:

seekBar.width * channel.position / song.length,

It equates to this:

bar's width × song's position / song's length
138 * 30 / 60

138 multiplied by 30 is 4,140 (sounds familiar, doesn't it?). 4,140 divided by 60 is 69. Hold that thought.

There may be times when neither channel nor song have property values that yield a valid number when run through the math. To safeguard against that, an if statement uses the isNaN() function (is Not a Number) to prod the value of pos (which is hypothetically 69). If pos is a valid number—that is, if !isNaN(pos) evaluates to true—then it is added to the current MovieClip.x value of seekBar, the sum of which is bestowed upon seekKnob. Because seekBar's position is 260, that (added to 69) puts seekKnob at 329, which is exactly halfway across the bar.

Note

The exclamation point (!) in front of the isNaN() function inverts whatever that function says, in the same way that the inequality operator (!=) means "is not equal to." If you want to find out if a value is not a valid number, check it against isNaN(). On the other hand, if you want to find out if a value is a valid number, check it against !isNaN().

The flip side of that if statement—meaning, pos is an unusable number—simply sets the knob's position to the position of the bar, which resets the knob to its original hug-the-left-side location.

As the song plays through, this seekKnobUpdate() function is triggered every time the timeline enters a frame; in other words, continuously. This causes the knob to indicate progress until the function is instructed to stop.

The mechanics of the volume slider work in pretty much the same way. A similar knob symbol is instructed to drag within a constrained area. The difference is that the knob's position in relation to its bar is used to adjust the volume of the currently playing song. In addition, a separate symbol is instructed to follow the knob, whose movement either hides or reveals that symbol behind a mask. Let's add the code.

Continuing below the previous ActionScript, give yourself another code comment heading, this time captioned as Volume slider. Type in these additional new lines:

////////////////////////////////////////
// Volume slider
////////////////////////////////////////

// prep
volumeSlider.volumeKnob.buttonMode = true;

// events
volumeSlider.volumeKnob.addEventListener(MouseEvent.MOUSE_DOWN,
The position of the knob and position and width of the bar are the critical properties.
volumeStartDrag);

The volumeKnob instance is nested inside volumeSlider, and that's because those movieclips are nested. Other than that, there is nothing remarkable about this addition. Let's keep rolling.

Enter the following new ActionScript, which defines the volumeStartDrag() function just referenced:

function volumeStartDrag(evt:MouseEvent):void {
  rect = new Rectangle(8, volumeSlider.volumeKnob.y,
The position of the knob and position and width of the bar are the critical properties.
volumeSlider.volumeBar.width - 8, 0); volumeSlider.volumeKnob.startDrag(true, rect); volumeSlider.volumeKnob.addEventListener(MouseEvent.MOUSE_MOVE,
The position of the knob and position and width of the bar are the critical properties.
volumeAdjust); stage.addEventListener(MouseEvent.MOUSE_UP, volumeStopDrag); };

As with the other slider, rect is set to a new Rectangle instance when the knob is clicked and fed appropriate values. In this case, the values are purposefully tweaked to move the knob in from the left edge just a bit. Why? Because if the volume knob were dragged all the way to the left, it would completely obscure the red movieclip rectangle behind the slanted five-column mask. Letting it go almost all the way to the left—8 pixels shy, in this case—looks good visually.

The startDrag() method is invoked on volumeKnob, and again the stage is associated with a MouseEvent.MOUSE_UP event to stop the dragging. This time, though, an additional event (MOUSE_MOVE) is associated with a custom function named volumeAdjust(). Let's look at both of those.

Enter the following new ActionScript:

function volumeStopDrag(evt:MouseEvent):void {
  volumeSlider.volumeKnob.stopDrag();
  stage.removeEventListener(MouseEvent.MOUSE_UP, volumeStopDrag);
  volumeSlider.volumeKnob.removeEventListener(MouseEvent.MOUSE_MOVE,
The position of the knob and position and width of the bar are the critical properties.
volumeAdjust); }; function volumeAdjust(evt:MouseEvent):void { volumeSlider.volumeBar.x = volumeSlider.volumeKnob.x; if (channel != null) { xform = channel.soundTransform; xform.volume = (volumeSlider.volumeKnob.x - 8) / (
The position of the knob and position and width of the bar are the critical properties.
volumeSlider.volumeBar.width - 8); channel.soundTransform = xform; } };

The volumeStopDrag() function is old hat by now. It stops the dragging and stops the MOUSE_MOVE handler. Let's break down the volumeAdjust() function.

First off, it sets the position of volumeBar to the position of volumeKnob. That hides and reveals the red rectangle behind its mask in concert with the knob's position. After that, assuming channel is not null, the xform variable—declared early on—is set to the SoundChannel.soundTransform property of the channel instance. This gives xform a SoundTransform.volume property, whose value is set in terms of volumeKnob's position (accounting for that 8-pixel shy span) in relation to the width of volumeBar.

The VolumeBar symbol happens to be 50 pixels wide, so let's run the numbers assuming the knob has been dragged halfway across the valid range. (Normally, halfway across would be 25, but we're adding half of that 8-pixel buffer, so half is 29 here.) Here's the actual expression:

volumeSlider.volumeKnob.x - 8) / (volumeSlider.volumeBar.width - 8

It equates to this:

knob's position - 8, divided by bar's width - 8
29 - 8 / 50 - 8

29 minus 8 is 21. 50 minus 8 is 42. 21 divided by 42 is 0.5, or 50%.

xform's volume property is set to 0.5, and then the final line reassigns xform to the channel.soundTransform property, which cuts the volume in half. Remember that this function is triggered every time the mouse moves, as it drags the knob.

Almost in the clear!

Finishing up the controls

The rest of the controls require barely a flick of the tail. All we need to do is hide the LoadingDisplay symbol (the spinning dots) by default, and handle the Event.ID3 event. Let's do it.

Add another block of code that looks like this:

////////////////////////////////////////
// Loading display
////////////////////////////////////////

loadingDisplay.stop();
loadingDisplay.visible = false;

This stops and hides the spinning dots.

Now, enter your final block of code, and make it look like this:

////////////////////////////////////////
// Song Data
////////////////////////////////////////
function soundID3Handler(evt:Event):void {
  songData.text = song.id3.artist + ": " + song.id3.songName + "
Finishing up the controls
(" + song.id3.year + ")"; };

This function is triggered whenever an MP3's ID3 tags are encountered. Tag information is retrieved from the Sound.id3 property of the song instance—here, song.id3.artist, .songName, and .year—and concatenated into a string fed to the songData text field's text property.

Note

ID3 tags have nothing to do with ActionScript 3.0 per se. The concept is part of the MP3 file format, and it just happens to be supported by ActionScript. On their own, ID3 tag names aren't especially easy to read. The tag intended for the artist's name, for example, is TPE1; the publication year is TYER, and so on. ActionScript provides friendly names for the most popular tags—comment, album, genre, songName, artist, track, and year—but the others are available by their less intuitive tag names. To see the full list, look up the Sound class in the ActionScript 3.0 Language and Components Reference, then skim down the Properties heading until you come to id3. Click that listing.

Test your MP3 player to give it a spin. Kick the tires a bit.

Evaluating and improving the MP3 player

Even with the best of planning, you might be surprised to find that some aspects of a project, including its faults, don't make themselves apparent until the work is done—or at least, until a first draft is done. (Some projects never do seem to end! Hey, at least it's a paycheck.) In Chapter 15, we discuss the idea of planning a FLA beforehand—the authors do believe in the practice, with a passion—but sometimes you can't tell how a car is going to handle until you actually wrap your fingers around the steering wheel and slam your boot on the gas pedal.

In this case, you may have noticed that every time a new song plays, the volume jumps back up to 100%, no matter where you drag the volume slider. Worse, when this happens, the volume is audibly at full, even though the slider might be positioned all the way to the left. That's a bug, and we're going to fix it.

In addition, you might want the player to cycle through the whole playlist, rather than simply stop after a song ends. You might also want the first song to start playing automatically. All of these options are possible, and thanks to the thoughtful arrangement of our existing ActionScript, they're easy to implement.

Let's tie up this MP3 player with a bow. First, let's address the volume bug. Locate the volumeAdjust() function, just above the Loading display block, and give its evt parameter a default value of null—like this (revision in bold):

function volumeAdjust(evt:MouseEvent = null):void {

What does this do? Without the addition, this function requires a MouseEvent parameter, which pretty much means it must be triggered in response to an event, which passes in the MouseEvent automatically. By giving the evt parameter a null value by default, you're making the parameter optional. This means the volumeAdjust() function can be triggered from anywhere, as an event handler or not.

Locate the playSong() function and update it to look like this (revision in bold):

function playSong(pos:Number = 0):void {
  channel = song.play(pos);
  volumeAdjust();
  btnPlay.gotoAndStop("pause");
  seekKnob.addEventListener(Event.ENTER_FRAME, seekKnobUpdate);
};

Just like that, the bug is fixed! The playSong() function actually sets the newly loaded song in motion, to speak, and associates the song instance with the channel instance. With channel updated, the xform variable, referenced inside volumeAdjust(), has what it needs to check the current position of the volume slider and adjust the volume accordingly.

Since we're in the playSong() function anyway, it's the perfect time to add a new event listener that will allow the player loop through its playlist. Update the playSong() function again to look like this (revision in bold):

function playSong(pos:Number = 0):void {
  channel = song.play(pos);
  channel.addEventListener(Event.SOUND_COMPLETE, nextHandler);
  volumeAdjust();
  btnPlay.gotoAndStop("pause");
  seekKnob.addEventListener(Event.ENTER_FRAME, seekKnobUpdate);
};

Once the channel variable is updated, it's associated with the already-written nextHandler() function in response to the Event.SOUND_COMPLETE event, which is dispatched when the sound channel of a currently playing sound reaches the end of the file.

Remember that the nextHandler() function is also associated with the MouseEvent.CLICK event, which is triggered when someone clicks the Next button. The MouseEvent class inherits some of its functionality from the Event class, and in this case, it's safe to strongly type the evt parameter inside the nextHandler() function as Event. This is because, at rock bottom, both Event and MouseEvent instances are ultimately instances of Event.

Locate the nextHandler() function and change it to look like this (revision in bold):

function nextHandler(evt: Event):void {

Finally, to make this MP3 player begin in "auto-play" mode, locate the completeHandler() function, just above the ComboBox block, and add the new lines shown in bold:

function completeHandler(evt:Event):void {
  songList = XML(evt.target.data);
  songsCB.dataProvider = new DataProvider(songList);
  loadSong(songList.song[1].@data);
  songsCB.selectedIndex = 1;
};

When the XML playlist fully loads, completeHandler() is triggered. It populates the ComboBox component. In addition to that, it now invokes the loadSong() function and feeds it the file name from the first <song> element that actually refers to an MP3 file (remember that the very first <song> element—songList.song[0]—doesn't contain file data). After that, the function updates the ComboBox component to its first song entry (the one after the filler Select a song entry), by setting its selectedIndex property to 1.

Test your movie again and, while you're tapping your feet, give yourself a pat on the back.

Building a video controller

You picked up a lot of knowledge about Flash video in Chapter 10. We are willing to bet that quite a few of you finished the chapter and wondered, "Can I create my own video controller . . . from scratch?" Glad you asked! On the heels of the MP3 player, building on what you already know, it won't even be very difficult.

Assembling the controller

To see how the controller is assembled, open the VideoPlayer.fla file in the Exercise folder for this chapter. Notice there is, for all intents and purposes, an empty stage with a controller near the bottom edge. Looking in the library, you will discover the controller is composed of nothing more than a number of movieclips, as shown in Figure 14-16. And you'll see that the entire Flash movie is composed of a single frame on the timeline.

Nothing but a few movieclips

Figure 14.16. Nothing but a few movieclips

The really interesting thing about this controller is that it was created entirely in Flash using the techniques presented in Chapter 2 of this book. The shapes were all drawn with the Rectangle Primitive and Oval tools. Each object was then filled with either a linear or radial gradient and converted to a movieclip symbol. The icons are nothing more than characters from the Webdings font, broken down to shapes.

Feel free to poke through the movieclips to get an idea of how this project was assembled. The play and mute symbols have two frames apiece in their own timelines, and are laid out like the Play/Pause button from the MP3 player exercise; that is, frame labels distinguish between the play/pause icons and mute/unmute icons. The movieclips with icons use the same trick you learned about in the previous exercise—a background shape with 0% Alpha fill—to give these "buttons" greater clickable surface area.

It's all well and good to know how the controller was assembled, but of course, the real magic happens when it gets wired up with ActionScript. There's going to be a bit of code coming at you, but now that you've built an MP3 player, lots of this will look familiar.

Wiring up the video player controls

First, you need a video object under the controls player. Right-click (Ctrl-click) inside the library and select New Video to create the object, and then drag it to the stage in the video layer. Change the object's dimensions so that it matches the width and height of the stage, and position it at 0,0. Give this video object the instance name myVideo.

Select the frame 1 in the scripts layer and open the Actions panel. Click once in the Script pane and enter the following code (to get the commented code block header, you can copy and paste from the previous exercise):

////////////////////////////////////////
// Variables
////////////////////////////////////////

var duration:Number;
var pos:Number;
var xform:SoundTransform;
var rect:Rectangle;

var currentVideo:int = 0;
var seekRate:int = 10;
var rwTimer:Timer = new Timer(100, 0);
var ffTimer:Timer = new Timer(100, 0);

The first four lines contain a number of variables that will be used later in the ActionScript: duration will refer to the length of the currently playing video, pos will correspond to the current position in the video, xform will adjust volume, and rect will constrain dragging for the slider knob.

The second four lines not only declare a few more variables, but initialize them with values, as well. currentVideo indicates the currently playing video, and seekRate determines how quickly the user will be able to fast-forward and rewind the current video. The last two set up two Timer instances that, when started, will trigger their associated functions—coming later—every 100 milliseconds (that is, every tenth of a second). The second parameter for each—a 0 in both cases—means the timers will repeat forever, or until told to stop.

Now it's time for the NetConnection and NetStream hookup. Enter the following new ActionScript beneath the existing code:

// net connection / net stream
var nc:NetConnection = new NetConnection();
nc.connect(null);
var ns:NetStream = new NetStream(nc);
myVideo.attachNetStream(ns);

var listener:Object = new Object();
listener.onMetaData = metaDataHandler;
function metaDataHandler(md:Object):void {
  duration = md.duration;
};
ns.client = listener;

ns.addEventListener(NetStatusEvent.NET_STATUS, statusHandler);
function statusHandler(evt:NetStatusEvent):void {
  if (evt.info.code == "NetStream.Play.Stop") {
    nextHandler();
  }
};

Much of this is code you saw a dozen times in Chapter 10. Most of the NetConnection and NetStream code—with our familiar nc and ns variable names—hasn't differed from elsewhere in the book.

What is new is that fact that we're actually using the onMetaData event this time, rather than throwing it the "oh, shut up already!" empty function seen previously (listener.onMetaData = function(md:Object):void {};). In this case, we do want something in the video's metadata: the duration of the video. We retrieve that value by pulling it from the duration property of the md parameter that represents the metadata:

listener.onMetaData = metaDataHandler;
function metaDataHandler(md:Object):void {
  duration = md.duration;
};

This value is stored in the duration variable declared earlier. You'll see how it's used in the fast-forward and rewind functions.

To make this video player loop through its playlist, as the MP3 player does, the same principle is used, but the mechanics are different here:

ns.addEventListener(NetStatusEvent.NET_STATUS, statusHandler);
function statusHandler(evt:NetStatusEvent):void {
  if (evt.info.code == "NetStream.Play.Stop") {
    nextHandler();
  }
};

A custom function, statusHandler(), is associated with the ns instance by way of a NetStatusEvent.NET_STATUS event. This is an event that is dispatched numerous times throughout the loading and playback of a video file, which makes sense. The video experiences all sorts of changes while it plays (loading, buffering, playing, and so on), and its status changes as a result. The NetStatusEvent class features an info property, which contains a code property that indicates the video's current status. When the video reaches its end—indicated by the string "NetStream.Play.Stop"—we want to trigger the custom nextHandler() function. This is just what we did with the MP3 player. And when we get to the button event handlers, you'll see how well you already recognize the nextHandler() function.

This video player reads its playlist from XML, so the next little bit won't be uncharted territory. Enter the following ActionScript beneath the existing code:

// xml
var videoList:XML = new XML();
var loader:URLLoader = new URLLoader();
var req:URLRequest = new URLRequest("playlist.xml");
loader.load(req);

loader.addEventListener(Event.COMPLETE, completeHandler);
function completeHandler(evt:Event):void {
  videoList = XML(evt.target.data);
  ns.play(videoList.video[currentVideo][email protected]());
  resumeVideo();
};

The only things worth noting here are the toString() method, tacked to the end of the E4X expression, and the reference to a custom resumeVideo() function. The reason toString() is needed here is because we're not using a custom loadVideo() function, like the MP3 player's loadSong(). The MP3 player's loadSong() function effectively converted its parameter's value to a string, and thanks to the omission here, we're taking care of that ourselves.

So how does the XML look this time? Like this:

<playlist>
  <video file="InkSong.mp4" />
  <video file="InspectorGadget.flv" />
  <video file="PeterWolf.flv" />
</playlist>

The resumeVideo() reference may seem a bit weird, because the video should already be playing, right? (Note the ns.play() call in the previous line.)

True enough, but as with the previous exercise, this player also tracks playback progress by way of a traveling slider knob, and the resumeVideo() function assigns the Event.ENTER_FRAME handler that manages that movement. In fact, speak of the devil . . .

Type the following ActionScript beneath the existing code:

////////////////////////////////////////
// Video functions
////////////////////////////////////////
function resumeVideo():void {
  ns.resume();
  videoControls.btnPlay.gotoAndStop("pause");
  videoControls.knob.addEventListener(Event.ENTER_FRAME,
Wiring up the video player controls
seekKnobUpdate); }; function pauseVideo():void { ns.pause(); videoControls.btnPlay.gotoAndStop("play"); videoControls.knob.removeEventListener(Event.ENTER_FRAME,
Wiring up the video player controls
seekKnobUpdate); };

Here are two custom functions that help thin out the code a bit for the remaining event handlers. Because of the animated slider knob and toggleable Play/Pause button, the act of pausing and resuming video playback requires more than just a reference to NetStream.pause() or NetStream.resume(). To save keystrokes down the line, these functions invoke the relevant NetStream methods and, in addition, update the Play/Pause button icon, and then either add or remove the custom seekKnobUpdate() function as a listener of the Event.ENTER_FRAME event.

Note that the knob instance—the knob movieclip in the library—is nested inside the videoControls instance (controls in the library). Nesting the symbols makes it easy to move around all of the playback controls as a single group while on the main timeline.

Handling the button events

It's already time for the buttons! Nothing to it, but to do it.

In the Actions panel, type the following ActionScript beneath what's already there:

// prep
with (videoControls) {
  btnMute.stop();
  btnMute.buttonMode = true;
  btnRewind.buttonMode = true;
  btnPlay.stop();
  btnPlay.buttonMode = true;
  btnFastForward.buttonMode = true;
  btnPrev.buttonMode = true;
  btnNext.buttonMode = true;
};

// events
with (videoControls) {
  btnMute.addEventListener(MouseEvent.CLICK, muteHandler);
  btnRewind.addEventListener(MouseEvent.MOUSE_DOWN,
Handling the button events
rwDownHandler);
btnRewind.addEventListener(MouseEvent.MOUSE_UP, rwUpHandler);
  btnPlay.addEventListener(MouseEvent.CLICK, playHandler);
  btnFastForward.addEventListener(MouseEvent.MOUSE_DOWN,
Handling the button events
ffDownHandler); btnFastForward.addEventListener(MouseEvent.MOUSE_UP, ffUpHandler); btnPrev.addEventListener(MouseEvent.CLICK, prevHandler); btnNext.addEventListener(MouseEvent.CLICK, nextHandler); };

The with() statement is new, but it's an easy concept to grasp, and useful in situations like this one. Basically, it's a programmer's shortcut. All of these movieclip buttons are nested inside the controls symbol, whose instance name is videoControls. Rather than referring tediously to each nested movieclip like this:

videoControls.btnMute.stop();
videoControls.btnMute.buttonMode = true;
videoControls.btnRewind.buttonMode = true;
// . . .

the with() statement allows us to skip the formalities and let the compiler know that, while we're inside those curly brackets ({ . . . }), we're talking about objects inside the videoControls instance.

With that explained, the ActionScript itself should make perfect sense. In the first several lines, the symbols' buttonMode property is set to true, to make these movieclips respond visually like buttons. In addition, btnMute and btnPlay are instructed to stop their timelines (these are the two symbols with more than one frame apiece).

The second group of lines wires up all the corresponding event handlers. Most of the buttons need to respond only to the MouseEvent.CLICK event, but the rewind and fast-forward buttons require the use of click and hold. For this reason, those movieclips are associated with MouseEvent.MOUSE_DOWN to begin the rewind or fast-forward routine, and MouseEvent.MOUSE_UP to cancel it.

Here's the code for the Mute button. Type this beneath the existing ActionScript:

function muteHandler(evt:MouseEvent):void {
  xform = ns.soundTransform;
  if (videoControls.btnMute.currentLabel == "on") {
    videoControls.btnMute.gotoAndStop("off");
    xform.volume = 0;
  } else {
    videoControls.btnMute.gotoAndStop("on");
    xform.volume = 1;
  }
  ns.soundTransform = xform;
};

The mechanism here is identical to the way the Play/Pause button works for the MP3 player: the movieclip's current label is consulted, and if it's showing "on," the playhead is shifted to the "off" frame (and vice versa). Based on this information, the xform instance's volume property is set to either 0 (silent) or 1 (full volume).

Here are the "down" and "up" handlers for the Rewind button. Once you grasp this one, you'll be able to understand the Fast-Forward button as well. Type this beneath the existing ActionScript:

function rwDownHandler(evt:MouseEvent):void {
  rwTimer.start();
};
function rwUpHandler(evt:MouseEvent):void {
  rwTimer.stop();
};

When users want to rewind, they click and hold the Rewind button. This triggers the rwDownHandler() function, which starts the rwTimer instance. The timer is what performs the actual rewinding, and you'll see how that works at the end of this exercise. When the user releases the button, the rwUpHandler() function stops the rwTimer. It's as simple as that.

Type the companion "up" and "down" handlers for the Fast-Forward button as well:

function ffDownHandler(evt:MouseEvent):void {
  ffTimer.start();
};
function ffUpHandler(evt:MouseEvent):void {
  ffTimer.stop();
};

The code for the Play button is just as self-explanatory. Enter its event handler beneath the existing ActionScript:

function playHandler(evt:MouseEvent):void {
  if (videoControls.btnPlay.currentLabel == "play") {
    pauseVideo();
  } else {
    resumeVideo();
  }
};

The code for the Prev and Next buttons is so similar to the MP3 player's version, we're confident you'll be able to connect the dots. Type the following ActionScript after the existing code:

function prevHandler(evt:Event):void {
  currentVideo--;
  if (currentVideo < 0) {
    currentVideo = videoList.video.length() - 1;
  }
  ns.play(videoList.video[currentVideo][email protected]());
  resumeVideo();
};
function nextHandler(evt:Event = null):void {
  currentVideo++;
  if (currentVideo > videoList.video.length() - 1) {
    currentVideo = 0;
  }
  ns.play(videoList.video[currentVideo][email protected]());
  resumeVideo();
};

As with the MP3 player, the nextHandler() function's evt parameter is given a null value by default, and for the same reason: this function is invoked as an event handler and also as a stand-alone function.

Programming the slider

We hope you're beginning to notice a theme here. Maybe a sense of déjà vu? The concepts we're discussing may not quite roll off the tongue just yet, but we hope some of these trees start looking familiar—that you get some comfort from the sense that Lake Nanagook seems to have a few well-worn landmarks. Let's take a look at the slider code for this video player.

Continuing with the existing code, type the following new ActionScript beneath the Buttons block:

////////////////////////////////////////
// Seek slider
////////////////////////////////////////

// prep
videoControls.knob.buttonMode = true;

// events
videoControls.knob.addEventListener(MouseEvent.MOUSE_DOWN,
Programming the slider
seekStartDrag);

This is old hat, by now. The knob is instructed to respond to the mouse cursor like a button, and a custom seekStartDrag() function is associated with the MouseEvent.MOUSE_DOWN event.

Here's that seekStartDrag() function. Type the following ActionScript beneath what's already there:

function seekStartDrag(evt:MouseEvent):void {
  videoControls.knob.removeEventListener(Event.ENTER_FRAME,
Programming the slider
seekKnobUpdate); rect = new Rectangle(videoControls.bar.x, videoControls.knob.y,
Programming the slider
videoControls.bar.width, 0); videoControls.knob.startDrag(true, rect); stage.addEventListener(MouseEvent.MOUSE_UP, seekStopDrag); };

The instance names may have changed, but the dance is still the same.

When the user presses the mouse over the knob in order to drag it, the seekKnobUpdate() function, which animates its movement, needs to be halted. If not, the knob will skip around frantically, trying to obey both the startDrag() method (which happens in this very event handler) and the looping seekKnobUpdate() function. The very first line of this function—removeEventListener()—accomplishes this need perfectly. Why not instead use the custom pauseVideo() function, which also invokes the same removeEventListener()? It's a matter of choice. You certainly could. To mix things up, we decided to let the video continue while the user scrubs around.

The rect variable is assigned a new Rectangle instance, which is employed, as before, to constrain the area of the dragging. To wrap up the function, the stage is asked to keep an ear open for MouseEvent.MOUSE_UP.

Let's go ahead and write the event handler for that MOUSE_UP listener. Type in the following new code:

function seekStopDrag(evt:MouseEvent):void {
  videoControls.knob.stopDrag();
  ns.seek(duration * (videoControls.knob.x - videoControls.bar.x) /
Programming the slider
videoControls.bar.width); if (videoControls.btnPlay.currentLabel == "pause") { videoControls.knob.addEventListener(Event.ENTER_FRAME,
Programming the slider
seekKnobUpdate); } stage.removeEventListener(MouseEvent.MOUSE_UP, seekStopDrag); };

Compare this with the version in the MP3 player, and you'll see how similar they are. When the user releases the mouse—either over the knob or accidentally off of it—the stopDrag() method halts the dragging. The location of the knob, in collaboration with the location of the bar and its width, is multiplied against the duration of the video—remember that this was gathered thanks to the onMetaData event handler—to determine the numeric value fed into the NetStream.seek() method. The seek() method sends the video to the nearest video keyframe specified by its parameter, provided that portion of the video has already downloaded.

The if statement checks if the video is currently playing, and if so, reassigns the Event.ENTER_FRAME event handler that animates the knob's position. Finally, because the stage listener is no longer needed, this function unhooks its MouseEvent.MOUSE_UP association.

And here's the seekKnobUpdate() function, which moves the knob in correspondence with the progression of the video. Type the following ActionScript under the existing code:

function seekKnobUpdate(evt:Event = null):void {
  var pos:Number = videoControls.bar.width * ns.time / duration;
  if (!isNaN(pos)) {
    videoControls.knob.x = videoControls.bar.x + pos;
  } else {
    videoControls.knob.x = videoControls.bar.x;
  }
};

This function operates just like its cousin in the MP3 player. Note that its evt parameter is set to null by default, which makes it optional. This happens here for the same reason you've seen the technique used elsewhere in this chapter: this function needs to be called either as an event handler or on its own. The on-its-own part happens in the Timer event handlers, which are coming up next.

Using timers to rewind and fast-forward

In the NetStream.seek() method used in the previous section, the knob's position determined the time offset in which the video should play. Rewinding and fast-forwarding is nothing more than a repeat performance of the same functionality. The Timer instances you declared at the beginning of this exercise—rwTimer and ffTimer—and the seekRate variable, are about to give you everything you need to accomplish these features.

In the Actions panel, enter the following ActionScript beneath what's already there:

////////////////////////////////////////
// Timers
////////////////////////////////////////

// events
rwTimer.addEventListener(TimerEvent.TIMER, rewind);
ffTimer.addEventListener(TimerEvent.TIMER, fastforward);

When the rwTimer and ffTimer instances were declared, they were told to dispatch the TimerEvent.TIMER event every 100 milliseconds. Here, that event is associated with respective event handler functions for each timer. All you need to do now is write those functions.

Here's the first one of those, rewind(). Type the following ActionScript after the existing code:

function rewind(evt:TimerEvent):void {
  if (ns.time - seekRate > 0) {
    ns.seek(ns.time - seekRate);
  } else {
    ns.seek(0);
  }
  seekKnobUpdate();
};

Remember, when requested, this function executes every tenth of a second. The first thing it does is check the NetStream.time property, to see where the video currently is. Maybe it has been playing for 25 seconds, and if so, the time property would be 25. An if statement determines if the current video's time property minus seekRate (which happens to be 10) is greater than 0. At 25 seconds in, the answer is indeed yes, and if so, the ns instance is instructed to seek to that point in the video. Why check with an if statement? Because you don't want to seek to a negative number.

If the subtraction would, in fact, seek to a negative number, ns is told to seek to 0. Finally, the seekKnobUpdate() function updates the position of the knob to match the current time offset of the video.

With the code as it stands—specifically, a seekRate value of 10—the act of rewinding repeatedly sends the video playhead back 10 seconds at a time, which is a fairly hefty number. What if you want to seek to a finer degree? Just change seekRate to a smaller number—say, 5, 2, or maybe even 0.5. But watch out, if you do: unless you're using RTMP, as you would with Flash Media Server, you can only seek to video keyframes. The frequency of your keyframes (not timeline keyframes, but video keyframes) determines how successfully you can rewind or fast-forward. In the video files included with this chapter, you'll find that InkSong.mp4, which has many keyframes, handles a much smaller seekRate value than either of the FLVs. When you have a mix of videos, you'll need to go with the lowest common denominator, which in this case means the highest common seekRate value. How do you figure that value out? With good, old-fashioned testing.

To wrap it up, type these final lines at the very bottom of your script:

function fastforward(evt:TimerEvent):void {
  if (ns.time + seekRate < duration) {
    ns.seek(ns.time + seekRate);
  }
  seekKnobUpdate();
};

As you can see, this fastforward() code is nothing more than the opposite of its companion, rewind(). The reason there isn't an else clause in this one—it would have been else { ns.seek(duration) }—is that seeking to the very end of a video file is often a dicey thing. The duration value retrieved from the onMetaData handler is one of those "close, but not necessarily a cigar" things. Sometimes metadata isn't as truthful as it should be, and rather than accidentally make the attempt to seek too far past the duration of the file (which generates an error), this function puts the video as near the end of the video as it safely can.

As before, the seekKnobUpdate() function keeps the knob up-to-date on where it needs to be as fast-forwarding occurs. Remember that these timers are started and stopped in the event handlers triggered by mouse presses and releases to the Rewind and Fast-forward buttons.

Test the movie and enjoy the show (see Figure 14-17).

Take a bow.You now know how to create your own personal video controller!

Figure 14.17. Take a bow.You now know how to create your own personal video controller!

Note

After being mesmerized by the musical ink blots—truly an inventive and gorgeous combination of the visual and musical arts—you might be wondering what made us choose the particular videos we did to wrap up this chapter. There are a number of reasons, actually.

First and foremost, these videos are flat-out interesting.

Second, they're an inspiration. Chen Zhou, who created the "Ink's song" video, has accomplished with splashes of ink what Robert Bringhurst (quoted in Chapter 6) admonishes typographers to endeavor with text. The authors would like to thank the China Central Academy of Fine Arts' Media Lab in Beijing for permission to use Chen's video in this book. In the other two files, Greg Pattillo breaks the mold by performing his own interpretation of well-known tunes on the flute—while beatboxing at the same time. Recognized by the New York Times as "the best person in the world at what he does," Greg makes it clear that truly cool things can come from trying something new. We thank Greg for his permission to include his videos (collectively viewed online over 20 million times) with this book.

Third, these files are interesting from a technical standpoint. Why's that? The MP4 is an HD H.264-encoded file, while the FLVs are standard batch-encoded YouTube files. Why is this interesting? It demonstrates Flash Player's flexibility in a compelling way. Using a smattering of XML and less than 200 lines of ActionScript, you're able to build an insanely small (3KB!) video player that handles anything from 73MB HD video to popular YouTube performances. Now think back to Chapter 10. The FLVPlayback component does all of this, but the resulting SWF weighs in at 52KB. Use the drawing tools to create the assets, write a couple hundred lines of code to do it yourself, and your SWF sheds 49KB.

We think that's about as neat as it gets.

Take a bow.You now know how to create your own personal video controller!

If you're interested in more beatbox flute, head over to http://cdbaby.com/cd/projectmusic, where Greg collaborates with Eric Stephenson (cello) and Peter Seymour (bass) in their genre-defying ensemble, PROJECT.

What you have learned

Rather than list what we covered in this chapter, we think it is more important to take a broader view of that statement. Step back for a moment and think about what you knew when you first laid this book on your desk and flamed up Flash CS4. The answer, we suspect, is "Not a lot."

Now think about your working through this chapter. The odds are pretty good you were able to follow along, and we are willing to bet there were a couple of points where you may have asked us to "move along a little quicker." This says to us that we have done our job, and that you may just know a lot more than you are aware of. Congratulations.

We were also a little sneaky with this chapter. If you follow the flow from the start to the end, you will see it actually follows the structure of this book: each exercise is designed to add to your knowledge base by building upon what you learned in the preceding exercise and, as we kept pointing out, in preceding chapters.

Finally, this chapter expanded on practically every concept presented in this book. If you have completed the exercises, then you have quite a bit of practical experience using Flash CS4.

Now that you've learned the ropes and have practiced numerous techniques, let's concentrate on the end game of the Flash design and development process: publishing your file.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.118.14