Components are incredibly useful for developing well-constructed, object-oriented Flash applications. You should consider making elements of a movie into components when they meet either of these criteria:
The same elements, or similar elements, are used multiple times throughout a movie.
The element has complex behaviors and can be treated as a discrete unit.
For the image viewer/slideshow application, we will make seven components:
A component that loads images given a URL
A component that allows an image to be moved and resized
A component into which image view panes are added
The viewer for the full image slideshow sequence
One of the thumbnail items that can be added to the sequencer
The component into which thumbnails are added and ordered
The menu for the application
The preview pane, sequencer, and sequence viewer all include elements that load images. Rather than reinvent the wheel with each, it makes sense to create a single Image component that can be utilized in each case. The Image component should have basic functionality that includes:
Loading an image from a given URL
Monitoring load progress with a progress bar
Invoking a callback function when loading is complete
Resizing itself to scale (maintaining the aspect ratio) to fit within specific dimensions
To create the Image component, complete the following steps:
Create a new movie clip symbol named Image.
Edit the linkage properties of the symbol.
Select the Export for ActionScript and Export in First Frame checkboxes.
Set the linkage identifier to ImageSymbol.
Click OK.
Edit the new symbol.
On the default layer, add the following code to the first frame:
#initclip 0 // The constructor creates a new movie clip into which the image is loaded. function Image ( ) { this.createEmptyMovieClip("imageHolder", this.getNewDepth( )); } Image.prototype = new MovieClip( ); // The load( ) method loads an image from a URL into the image holder. Image.prototype.load = function (url, w, h) { // Attach an instance of the progress bar, which monitors the load progress. this.attachMovie("FProgressBarSymbol", "pBar", this.getNewDepth( )); // Load the image into imageHolder. this.imageHolder.loadMovie(url); // Set the target for the progress bar and specify // the load progress callback method. this.pBar.setLoadTarget(this.imageHolder); this.pBar.setChangeHandler("onLoadProgress", this); // Center the progress bar. this.pBar._x = w/2 - this.pBar._width/2; this.pBar._y = h/2 - this.pBar._height/2; }; // The onLoadProgress( ) method is invoked automatically by the progress bar each // time there is some load progress. Image.prototype.onLoadProgress = function ( ) { // Check to see if the progress is 100%. if (this.pBar.getPercentComplete( ) == 100) { // Make the progress bar invisible. this.pBar._visible = false; // Get the height and width of the image as it is when it is originally // loaded. This is used to properly scale the image later. this.origHeight = this._height; this.origWidth = this._width; // If an onLoad callback is defined for the image component, invoke it. this.onLoadPath[this.onLoadCB](this); } }; // The scale( ) method resizes the image to fit within a specified width and // height while maintaining the aspect ratio. If fill is true, the image fills // the specified area even at the expense of cutting off one of the sides. // Otherwise, the image is resized to fit entirely within the boundaries, // leaving space on the sides if necessary. Image.prototype.scale = function (w, h, fill) { var iw = this.origWidth; var ih = this.origHeight; var scale = 1; // The scale ratios in the x and y directions are obtained // by dividing the width and height of the boundaries by the // width and height of the original image. var xscale = w/iw; var yscale = h/ih; if (fill) { // Set scale to the larger of xscale and yscale. scale = (xscale > yscale) ? xscale : yscale; } else { // set scale to the smaller of xscale and yscale. scale = (xscale > yscale) ? yscale : xscale; } // Set the _xscale and _yscale values of the component to the value of scale // times 100 (which converts the scale ratio to a percentage). this._xscale = scale * 100; this._yscale = scale * 100; }; // Set the onLoad callback where the function name is given as a string and the // path is an optional parameter indicating the path to the callback function. Image.prototype.setOnLoad = function (functionName, path) { if (path == undefined) { path = this._parent; } this.onLoadCB = functionName; this.onLoadPath = path; }; Object.registerClass("ImageSymbol", Image); #endinitclip
The Image component is short and uncomplicated. Each of the methods is straightforward. Let's look at each of them more closely to review what each one does and how.
The load( ) method initiates the loading of the image into the image holder movie clip with the loadMovie( ) method. Additionally, the load( ) method creates a progress bar and sets it to monitor the load progress of the image using the setLoadTarget( ) method. Also, using the setChangeHandler( ) method, we tell the progress bar to call the onLoadProgress( ) method of the image component whenever there is any load progress. We refer to the Image component using the keyword this, which refers to the component from which the load( ) method was invoked in the first place.
Image.prototype.load = function (url, w, h) { this.attachMovie("FProgressBarSymbol", "pBar", this.getNewDepth( )); this.imageHolder.loadMovie(url); this.pBar.setLoadTarget(this.imageHolder); this.pBar.setChangeHandler("onLoadProgress", this); this.pBar._x = w/2 - this.pBar._width/2; this.pBar._y = h/2 - this.pBar._height/2; };
The onLoadProgress( ) method is invoked automatically by the progress bar whenever there is any progress made with the loading image. We want to wait until the image is completely loaded, so we use the getPercentComplete( ) method to check whether the percentage loaded is equal to 100. If it is, then we make the progress bar invisible (the loaded image is visible, and we don't want the progress bar to obscure it). Additionally, we get the original height and width of the image, which is necessary for proper scaling. And finally, if there is an onLoad callback defined for the component, we invoke it.
Image.prototype.onLoadProgress = function ( ) { if (this.pBar.getPercentComplete( ) == 100) { this.pBar._visible = false; this.origHeight = this._height; this.origWidth = this._width; this.onLoadPath[this.onLoadCB](this); } };
The scale( ) method is the most complex of all the methods of the Image component. But even so, it probably looks scarier than it really is. The w and h parameters tell the method the dimensions of the area into which we want the image to fit. Given these dimensions, and the dimensions of the original image size, we can find the scale ratios in the x and y directions by dividing the width and height of the new area by the width and height of the original image size.
For example, if the new area's dimensions are 120 x 60, and the original image size is 240 x 120, the xscale and yscale ratios are both 1/2. In other words, we want to scale the image to 50% of the original size. Now, if we didn't care about the aspect ratio of the image, we wouldn't need to perform any further calculations. However, we want to make sure that the image doesn't end up looking squished. For example, if the original image dimensions are 240 x 120, but the new area's dimensions are 120 x 90, the xscale and yscale ratios are not equal, and the image would be squished. We want to use the same ratio to set the scale properties in both the x and y directions. Therefore, we need to determine which of the ratios to use. If the fill parameter is true, we want use the larger of the two ratios so that the image fills the entire area, even though some of the image might extend beyond the boundaries. Otherwise, we use the smaller ratio, since that will ensure that the entire image fits within the boundaries. Then, once we have determined the correct ratio, we set the _xscale and _yscale properties of the component to the ratio times 100 to create a percentage.
Image.prototype.scale = function (w, h, fill) { var iw = this.origWidth; var ih = this.origHeight; var scale = 1; var xscale = w/iw; var yscale = h/ih; if (fill) { scale = (xscale > yscale) ? xscale : yscale; } else { scale = (xscale > yscale) ? yscale : xscale; } this._xscale = scale * 100; this._yscale = scale * 100; };
The setOnLoad( ) method enables you to specify an onLoad callback function. When you call this method you must provide it a string name of a function. Optionally, you can also specify the path in which the function can be found. If no path is specified, the component looks to the parent timeline.
Image.prototype.setOnLoad = function (functionName, path) { if (path == undefined) { path = this._parent; } this.onLoadCB = functionName; this.onLoadPath = path; };
Before we get to the preview pane, we need to first look at its subelements, the Image View Pane components. The image view panes load the low-resolution images so that they can be viewed, dragged, resized, collapsed/expanded, and closed. Figure 25-1 shows an example of a preview pane with two opened image viewers.
The Image Viewer component includes a title bar, a close button, a frame/outline, a resize button, and an Image component. The image viewer should have the following functionality:
Loading an image using the Image component
Automatically sizing itself to match the dimensions of the loaded image
Displaying the image title in the title bar
Becoming draggable when the title bar is clicked
Collapsing/expanding when the title bar is double-clicked
Closing when the close button is clicked
Resizing when the resize button is dragged
Follow these steps to create the image view pane component:
Create a new movie clip symbol named ImageViewPane.
Edit the linkage properties of the symbol.
Select the Export for ActionScript and Export in First Frame checkboxes.
Set the linkage identifier to ImageViewPaneSymbol.
Click OK.
Edit the new symbol.
On the default layer, add the following code to the first frame:
#initclip 1 // The constructor creates title bar and view pane movie clips, and within // the view pane movie clip, it loads an instance of the image component. function ImageViewPane ( ) { this.createEmptyMovieClip("titleBar", this.getNewDepth( )); this.createEmptyMovieClip("viewPane", this.getNewDepth( )); this.viewPane.attachMovie("ImageSymbol", "img", this.viewPane.getNewDepth( )); } ImageViewPane.prototype = new MovieClip( ); // The load( ) method loads the image from the specified // URL and sets the title text. ImageViewPane.prototype.load = function (url, title) { // Draw the title bar and the view pane. this.makeTitleBar(title, 200, 20); this.makeViewPane(200, 100); // Call the load( ) method of the image component. this.viewPane.img.load(url, 200, 100); // Set the onLoad callback function of the Image component to the // onImageLoad( ) method of the image view pane. this.viewPane.img.setOnLoad("onImageLoad", this); }; // The open( ) method opens the component if it has otherwise been closed. ImageViewPane.prototype.open = function ( ) { // Make sure everything is visible. this.viewPane._visible = true; this._visible = true; // Call the onSelect callback if it is defined. this.onSelectPath[this.onSelectCB](this); }; // The makeViewPane( ) method draws the view pane portion of the component, given // the width and height. ImageViewPane.prototype.makeViewPane = function (w, h) { // If the view pane frame is undefined, create the movie clip. If the frame has // a greater depth than the Image component instance, swap depths so that the // image is not hidden. if (this.viewPane.frame == undefined) { this.viewPane.createEmptyMovieClip("frame", this.viewPane.getNewDepth( )); if (this.viewPane.frame.getDepth() > this.viewPane.img.getDepth( )) { this.viewPane.frame.swapDepths(this.viewPane.img); } } // Clear any existing frame and draw a rectangle to the specified dimensions. with (this.viewPane.frame) { clear( ); lineStyle(0, 0x000000, 100); beginFill(0xFFFFFF, 100); drawRectangle(w, h); endFill( ); _x = w/2; _y = h/2 + 21; } // If the resize button is undefined, create it. if (this.viewPane.resizeBtn == undefined) { this.viewPane.createEmptyMovieClip("resizeBtn", this.viewPane.getNewDepth( )); // Draw a triangle. with (this.viewPane.resizeBtn) { lineStyle(0, 0x000000, 100); beginFill(0xFFFFFF, 100); drawTriangle(10, 10, 90, 180, -5, 15); endFill( ); } // When the resize button is pressed, make it draggable and set the // isBeingResized property of the image view pane instance to true. this.viewPane.resizeBtn.onPress = function ( ) { var viewer = this._parent._parent; viewer.isBeingResized = true; this.startDrag( ); }; // When the resize button is released, do the opposite of onPress( ). this.viewPane.resizeBtn.onRelease = function ( ) { var viewer = this._parent._parent; viewer.isBeingResized = false; this.stopDrag( ); }; } // Position the resize button in the lower-right corner of the image view pane. this.viewPane.resizeBtn._x = w; this.viewPane.resizeBtn._y = h; }; // Create the title bar, given the title and the width and height. ImageViewPane.prototype.makeTitleBar = function (title, w, h) { // If the bar portion of the title bar is undefined, create it. if (this.titleBar.bar == undefined) { this.titleBar.createEmptyMovieClip("bar", this.titleBar.getNewDepth( )); this.titleBar.bar.onPress = function ( ) { var viewer = this._parent._parent; // Invoke the onSelect callback function. viewer.onSelectPath[viewer.onSelectCB](viewer); // If the user double-clicked the title bar, expand/collapse the view pane. // Otherwise, it's a single click, so make the image view pane draggable. var currentTime = getTimer( ); if (currentTime - this.previousTime < 500) { viewer.viewPane._visible = !viewer.viewPane._visible; } else { viewer.startDrag( ); } this.previousTime = currentTime; }; // When the bar is released, stop dragging the image view pane and invoke the // onUpdate( ) callback function. this.titleBar.bar.onRelease = function ( ) { viewer = this._parent._parent; viewer.stopDrag( ); viewer.onUpdatePath[viewer.onUpdateCB](viewer); } } // Draw (or redraw) the rectangle. with (this.titleBar.bar) { clear( ); lineStyle(0, 0, 100); beginFill(0xDFDFDF, 100); drawRectangle(w, h); endFill( ); _x = w/2; _y = h/2; } // If the title text field is not yet defined, create it. if (this.titleBar.title == undefined) { this.titleBar.createTextField("title", this.titleBar.getNewDepth( ), 0, 0, 0, 0); this.titleBar.title.selectable = false; } // Set the width of the title to the width of the image view pane. this.titleBar.title._width = w; this.titleBar.title._height = h; // Assign the title to the title text field. this.titleBar.title.text = title; // If the close button is not yet defined, create it. if (this.titleBar.closeBtn == undefined) { this.titleBar.createEmptyMovieClip("closeBtn", this.titleBar.getNewDepth( )); // Draw a 10 x 10 square. with (this.titleBar.closeBtn) { lineStyle(0, 0x000000, 100); beginFill(0xE7E7E7, 100); drawRectangle(10, 10); endFill( ); } // When the close button is released, hide the entire image view pane. this.titleBar.closeBtn.onRelease = function ( ) { this._parent._parent._visible = false; }; } // Position the close button at the upper-right corner of the image view pane. this.titleBar.closeBtn._x = w - 10; this.titleBar.closeBtn._y = 10; }; // The onImageLoad( ) method is invoked automatically when the image has // completed loading. ImageViewPane.prototype.onImageLoad = function ( ) { var img = this.viewPane.img; // Create the title bar and view pane to fit the loaded image. this.makeTitleBar(this.titleBar.title.text, img._width, 20); this.makeViewPane(img._width, img._height); // Move the image down by 21 pixels so it does not cover the title bar. img._y += 21; // Call the onUpdate callback function. this.onUpdatePath[this.onUpdateCB](this); }; // The onEnterFrame( ) method continually checks to see if the view pane is being // resized. If so, it calls the resize( ) method with the coordinates of the // resize button. ImageViewPane.prototype.onEnterFrame = function ( ) { if (this.isBeingResized) { this.resize(this.viewPane.resizeBtn._x, this.viewPane.resizeBtn._y); } }; // The resize( ) method resizes the image, the view pane, and the title bar to // the specified width and height. ImageViewPane.prototype.resize = function (w, h) { // Call makeViewPane( ) and makeTitleBar( ) to // redraw the view pane and title bar. this.makeViewPane(w, h); this.makeTitleBar(this.titleBar.title.text, w, 20); // Call the scale( ) method of the image component to resize the loaded image. this.viewPane.img.scale(w, h); // Reposition the Image component so that it is centered in the view pane. this.viewPane.img._x = w/2 - this.viewPane.img._width/2; this.viewPane.img._y = h/2 - this.viewPane.img._height/2 + 21; }; // Set the onSelect and onUpdate callback functions. ImageViewPane.prototype.setOnSelect = function (functionName, path) { if (path == undefined) { path = this._parent; } this.onSelectCB = functionName; this.onSelectPath = path; }; ImageViewPane.prototype.setOnUpdate = function (functionName, path) { if (path == undefined) { path = this._parent; } this.onUpdateCB = functionName; this.onUpdatePath = path; }; Object.registerClass("ImageViewPaneSymbol", ImageViewPane); #endinitclip
Much of the Image View Pane component is quite straightforward. However, there are some parts that deserve closer examination. Undoubtedly, it will be much clearer once we look at what is going on.
The constructor instantiates several of the main parts of the component. The title bar movie clip is the rectangle that appears at the top of the image and displays the title. The view pane is the remaining portion of the image view pane that appears below the title bar and contains the image itself.
function ImageViewPane ( ) { this.createEmptyMovieClip("titleBar", this.getNewDepth( )); this.createEmptyMovieClip("viewPane", this.getNewDepth( )); this.viewPane.attachMovie("ImageSymbol", "img", this.viewPane.getNewDepth( )); }
To avoid unnecessarily reloading images, the close button merely makes the image view pane invisible. Therefore, the open( ) method can open a closed image view pane by setting the _visible properties to true. Additionally, the open( ) method invokes the onSelect callback function because when an image view pane is opened, it should be selected as well.
ImageViewPane.prototype.open = function ( ) { this.viewPane._visible = true; this._visible = true; this.onSelectPath[this.onSelectCB](this); };
The makeViewPane( ) method is long, but it is not very complicated when you look at it more closely.
First, we create the frame movie clip if it has not been created. The frame is the outline and background in which the image appears. Therefore, if the frame is created such that the depth is greater than the Image component, we swap their depths so that the image is not hidden behind the frame:
if (this.viewPane.frame == undefined) { this.viewPane.createEmptyMovieClip("frame", this.viewPane.getNewDepth( )); if (this.viewPane.frame.getDepth() > this.viewPane.img.getDepth( )) { this.viewPane.frame.swapDepths(this.viewPane.img); } }
Once we are sure the frame exists, we draw a filled rectangle within it. The clear( ) method clears out any content that might have previously existed within the movie clip. Then we position the frame correctly. Since the drawRectangle( ) method draws a rectangle with its center at (0,0), we move the rectangle down and to the right by half its height and width. The frame is moved down another 21 pixels to accommodate the title bar (which is 20 pixels high).
with (this.viewPane.frame) { clear( ); lineStyle(0, 0x000000, 100); beginFill(0xFFFFFF, 100); drawRectangle(w, h); endFill( ); _x = w/2; _y = h/2 + 21; }
Next, we create the resize button and draw a triangle in it, if it doesn't exist. Also, we assign event handler methods to the button so that it is draggable when pressed. The isBeingResized property tells the image view pane whether the image is being resized.
if (this.viewPane.resizeBtn == undefined) { this.viewPane.createEmptyMovieClip("resizeBtn", this.viewPane.getNewDepth( )); with (this.viewPane.resizeBtn) { lineStyle(0, 0x000000, 100); beginFill(0xFFFFFF, 100); drawTriangle(10, 10, 90, 180, -5, 15); endFill( ); } this.viewPane.resizeBtn.onPress = function ( ) { var viewer = this._parent._parent; viewer.isBeingResized = true; this.startDrag( ); }; this.viewPane.resizeBtn.onRelease = function ( ) { var viewer = this._parent._parent; viewer.isBeingResized = false; this.stopDrag( ); }; }
The resize button should always appear in the lower-right corner of the image view pane:
this.viewPane.resizeBtn._x = w; this.viewPane.resizeBtn._y = h;
Like the makeViewPane( ) method, the makeTitleBar( ) method is long but not overly complex. Let's take a closer look at some of the code.
First, we create the bar portion of the title bar if it doesn't exist. We also assign onPress( ) and onRelease( ) methods to it. When the bar is pressed, the onPress( ) method determines if the click was a single-click or a double-click. A single-click initiates a startDrag( ) action. A double-click toggles the view pane's visibility, creating the effect of collapsing and expanding the view pane. We check for double-clicks by recording the time (using getTimer( )) of each press. If two presses occur within 500 milliseconds, it constitutes a double-click.
if (this.titleBar.bar == undefined) { this.titleBar.createEmptyMovieClip("bar", this.titleBar.getNewDepth( )); this.titleBar.bar.onPress = function ( ) { var viewer = this._parent._parent; viewer.onSelectPath[viewer.onSelectCB](viewer); var currentTime = getTimer( ); if (currentTime - this.previousTime < 500) { viewer.viewPane._visible = !viewer.viewPane._visible; } else { viewer.startDrag( ); } this.previousTime = currentTime; }; this.titleBar.bar.onRelease = function ( ) { viewer = this._parent._parent; viewer.stopDrag( ); viewer.onUpdatePath[viewer.onUpdateCB](viewer); }; }
Once we know the bar exists, we want to draw a rectangle with the specified dimensions. The clear( ) method makes sure that the previous content is cleared out first:
with (this.titleBar.bar) { clear( ); lineStyle(0, 0x000000, 100); beginFill(0xDFDFDF, 100); drawRectangle(w, h); endFill( ); _x = w/2; _y = h/2; }
If the title text field is undefined, we create it. Also, the text field should be nonselectable. This is important because otherwise it could interfere with the events of the bar portion of the title bar.
if (this.titleBar.title == undefined) { this.titleBar.createTextField("title", this.titleBar.getNewDepth( ), 0, 0, 0, 0); this.titleBar.title.selectable = false; }
If the close button is not yet defined, we create it and draw a square within it. When the close button is released, the visibility of the image view pane is set to false. This creates the effect of closing the image view pane. However, since the component is still on the Stage, it can later be reopened without having the reload the image.
if (this.titleBar.closeBtn == undefined) { this.titleBar.createEmptyMovieClip("closeBtn", this.titleBar.getNewDepth( )); with (this.titleBar.closeBtn) { lineStyle(0, 0x000000, 100); beginFill(0xE7E7E7, 100); drawRectangle(10, 10); endFill( ); } this.titleBar.closeBtn.onRelease = function ( ) { this._parent._parent._visible = false; }; }
When the image is loaded, the onImageLoad( ) method is invoked automatically. This is important because once the image is loaded, we want to correctly size the image view pane to accommodate the image.
ImageViewPane.prototype.onImageLoad = function ( ) { var img = this.viewPane.img; this.makeTitleBar(this.titleBar.title.text, img._width, 20); this.makeViewPane(img._width, img._height); img._y += 21; this.onUpdatePath[this.onUpdateCB](this); };
The isBeingResized property is set to true only when the resize button is being pressed. When this occurs, we invoke the resize( ) method and give it the coordinates of the resize button (which is always in the lower-right corner of the image, at the maximum width and height).
ImageViewPane.prototype.onEnterFrame = function ( ) { if (this.isBeingResized) { this.resize(this.viewPane.resizeBtn._x, this.viewPane.resizeBtn._y); } };
The Preview Pane component serves as a scrolling container for the image view panes for the low-resolution images.
Complete the following steps to create the Preview Pane component:
Create a new movie clip symbol named PreviewPane.
Edit the linkage properties of the symbol.
Select the Export for ActionScript and Export in First Frame checkboxes.
Set the linkage identifier to PreviewPaneSymbol.
Click OK.
Edit the new symbol.
On the default layer, add the following code to the first frame:
#initclip function PreviewPane ( ) { // Add a scroll pane to the component, within which // the preview images are contained. this.attachMovie("FScrollPaneSymbol", "sp", this.getNewDepth( )); // Create a movie clip for the scroll content. this.createEmptyMovieClip("content", _root.getNewDepth( )); // Create a movie clip within the scroll content and draw a rectangle in it. // The background movie clip is used to properly align the scroll contents to // the scroll pane. this.content.createEmptyMovieClip("background", this.content.getNewDepth( )); with (this.content.background) { lineStyle(0, 0x000000, 0); drawRectangle(320, 240, 0, 0, 160, 120); } // Call updateViewer( ) to set the scroll pane to target the scroll content. this.updateViewer( ); // Create arrays to keep track of the images that are currently opened and the // images that have been loaded (whether open or not). this.currentViewAr = new Array( ); this.loadedAr = new Array( ); } PreviewPane.prototype = new MovieClip( ); // Return a reference to an Image View Pane component if one already exists for // the specified URL. Image view panes may be within the preview pane but may not // be visible if they were closed. PreviewPane.prototype.isURLLoaded = function (url) { for (var i = 0; i < this.loadedAr.length; i++) { if (this.loadedAr[i].url == url) { return this.loadedAr[i].vp; } } return false; }; PreviewPane.prototype.setSize = function (w, h) { this.sp.setSize(w, h); }; // The updateViewer( ) method adjusts the scroll content. This method is invoked // every time there is a change made to the scroll content. PreviewPane.prototype.updateViewer = function ( ) { this.content.background._width = this.content._width; this.content.background._height = this.content._height; this.sp.setScrollContent(this.content); if (this.content._width > this.sp._width - 10) { this.sp.setHScroll(true); } }; // The bringToFront( ) method brings the // specified image view pane to the foreground PreviewPane.prototype.bringToFront = function (viewer) { // Remove and return the last value in the currentViewAr array (the image view // pane with the greatest depth). var topViewer = this.currentViewAr.pop( ); // If the specified image view pane is not already on top . . . if (viewer != topViewer) { // Swap the depths of the selected view pane with the top view pane, bringing // the selected view pane to the foreground. viewer.swapDepths(topViewer); // Search through the currentViewAr array for the index of the selected image // view pane and assign the value of the (previously) top view pane to that // index in the array. for (var i = 0; i < this.currentViewAr.length; i++) { if (this.currentViewAr[i] == viewer) { break; } } this.currentViewAr[i] = topViewer; } // Append the selected viewer to the end of the currentViewAr array. this.currentViewAr.push(viewer); }; // The open( ) method opens an image view pane given a URL and a title. PreviewPane.prototype.open = function (url, title) { var uniqueVal = this.content.getNewDepth( ); // If an image view pane with the same URL is already loaded, isURLLoaded( ) // returns a reference to it. Otherwise, it returns false. var loaded = this.isURLLoaded(url); // If loaded is . . . if (loaded == false) { // . . . false, then create a new image view pane and load the image into it. var vp = this.content.attachMovie("ImageViewPaneSymbol", "vp" + uniqueVal, uniqueVal); vp.load(url, title); // Set the onSelect callback function to the bringToFront( ) method // so that when the image view pane is selected, it is always // brought to the foreground. vp.setOnSelect("bringToFront", this); // Set the onUpdate callback function to the updateViewer( ) // method so that when the image view pane is updated in any way, // the preview pane is also updated. vp.setOnUpdate("updateViewer", this); // Add the image view pane to the currentViewAr and loadedAr arrays. this.currentViewAr.push(vp); this.loadedAr.push({url: url, vp: vp}); } else { // If loaded is true, call the open( ) method of the image pane. loaded.open( ); } }; Object.registerClass("PreviewPaneSymbol", PreviewPane); #endinitclip
The Preview Pane component is not very long, nor does it introduce too many new concepts. However, there are a few areas that could use a little further study.
The constructor creates a scroll pane and a movie clip for the scroll content. This part is standard. However, we use a little trick to keep the scroll content correctly positioned within the scroll pane. We create the background movie clip within the scroll content clip. We then draw a rectangle within the background with an invisible outline! This may seem a little confusing at first, but there is a good reason why we do it this way. A scroll pane always aligns the scroll content so that the upper-left corner of the actual content is in the upper-left corner of the scroll pane. The scroll pane doesn't align the scroll content relative to the registration point of the scroll content movie clip. The problem is that if the user opens one image view pane in the preview pane, that one image view pane is aligned such that the upper-left corner is in the upper-left corner of the preview pane. If the user then drags the image view pane, the scroll pane will realign the scroll content such that the image view pane appears in the same position as it did previously. By adding a background movie clip to the scroll content that is aligned with the upper-left corner at the scroll content's registration point, we force the scroll pane to align the scroll content to the registration point (in most cases).
function PreviewPane ( ) { this.attachMovie("FScrollPaneSymbol", "sp", this.getNewDepth( )); this.createEmptyMovieClip("content", _root.getNewDepth( )); this.content.createEmptyMovieClip("background", this.content.getNewDepth( )); with (this.content.background) { lineStyle(0, 0x000000, 0); drawRectangle(320, 240, 0, 0, 160, 120); } this.updateViewer( ); this.currentViewAr = new Array( ); this.loadedAr = new Array( ); }
To close an image view pane we just make it invisible. This saves the user from having to reload the image if he decides to open it again. The isURLLoaded( ) method searches through the loadedAr array to find any image view pane (even an invisible one) that has already been loaded for the specified URL. If none is found, the method returns false.
PreviewPane.prototype.isURLLoaded = function (url) { for (var i = 0; i < this.loadedAr.length; i++) { if (this.loadedAr[i].url == url) { return this.loadedAr[i].vp; } } return false; };
We need to create the updateViewer( ) method to update the scroll pane when the scroll content changes. First, we resize the background movie clip so that its dimensions match the rest of the content. This helps to ensure that the content remains properly aligned. Then, the setScrollContent( ) method resets the scroll content for the scroll pane with the updated info. Finally, if the scroll pane has a vertical scrollbar, it is possible that part of the scroll contents can be hidden without the possibility of scrolling horizontally. Therefore, if the scroll content's width is greater than the width of the scroll pane minus the width of a scroll bar, we tell the scroll pane to display a horizontal scroll bar.
PreviewPane.prototype.updateViewer = function ( ) { this.content.background._width = this.content._width; this.content.background._height = this.content._height; this.sp.setScrollContent(this.content); if (this.content._width > this.sp._width - 10) { this.sp.setHScroll(true); } };
When an image view pane is selected, it should be brought in front of the rest of the content. The currentViewAr array contains references to all the opened image view panes; the elements are in the order of depth, with the greatest depth at the end of the array. Therefore, we can get a reference to the image view pane that is currently on top by calling the pop( ) method of the currentViewAr array, which also removes the element from the end of the array and returns its value. If the top view pane and the selected view pane are not the same, we swap their depths and swap the positions of the elements in the array.
PreviewPane.prototype.bringToFront = function (viewer) { var topViewer = this.currentViewAr.pop( ); if (viewer != topViewer) { viewer.swapDepths(topViewer); for (var i = 0; i < this.currentViewAr.length; i++) { if (this.currentViewAr[i] == viewer) { break; } } this.currentViewAr[i] = topViewer; } this.currentViewAr.push(viewer); };
The open( ) method opens an image, given a URL and a title, and is designed such that it can determine whether to make an existing image view pane visible or create a new image view pane:
PreviewPane.prototype.open = function (url, title) { var uniqueVal = this.content.getNewDepth( ); var loaded = this.isURLLoaded(url); if (loaded == false) { var vp = this.content.attachMovie("ImageViewPaneSymbol", "vp" + uniqueVal, uniqueVal); vp.load(url, title); vp.setOnSelect("bringToFront", this); vp.setOnUpdate("updateViewer", this); this.currentViewAr.push(vp); this.loadedAr.push({url: url, vp: vp}); } else { loaded.open( ); } };
The sequence viewer component is the portion of the application that plays back the images as a slide show. The component should fill the Flash Player with a black background and play the full images in sequence using setInterval( ) to determine when to change images.
Follow these steps to create the Sequence Viewer component:
Create a new movie clip symbol named SequenceViewer.
Edit the linkage properties of the symbol.
Select the Export for ActionScript and Export in First Frame checkboxes.
Set the linkage identifier to SequenceViewerSymbol.
Click OK.
Edit the new symbol.
On the default layer, add the following code to the first frame:
#initclip function SequenceViewer ( ) { // The array of image items. this.items = new Array( ); // Create the black rectangle to fill the background of the Player. this.createEmptyMovieClip("background", this.getNewDepth( )); with (this.background) { lineStyle(0, 0x000000, 0); beginFill(0, 100); drawRectangle(Stage.width, Stage.height); endFill( ); _x = Stage.width/2; _y = Stage.height/2; } // Make the viewer invisible to start. this._visible = false; } // SequenceViewer subclasses MovieClip. SequenceViewer.prototype = new MovieClip( ); // Add an image to the sequence viewer by URL. SequenceViewer.prototype.addItem = function (url) { var uniqueVal = this.getNewDepth( ); // Add an image component and load the image into it. var img = this.attachMovie("ImageSymbol", "image" + uniqueVal, uniqueVal); img.load(url); // Set the onLoad callback for the image component // to the onImageLoad( ) method. img.setOnLoad("onImageLoad", this); // Add the image to the items array. this.items.push(img); // Make the image invisible to start. img._visible = false; }; // When the image loads, scale it to fill in the Player. SequenceViewer.prototype.onImageLoad = function (img) { img.scale(Stage.width, Stage.height, true); }; // Change the order of an image in the sequence. SequenceViewer.prototype.changeOrder = function (prevIndex, newIndex) { var img = this.items[prevIndex]; this.items.splice(prevIndex, 1); this.items.splice(newIndex, 0, img); }; // Start the playback of the images. SequenceViewer.prototype.play = function (intervalm randomize) { // Make the viewer visible. this._visible = true; // Set the itemIndex property to -1 so that the first image shown is index 0. this.itemIndex = -1; // Call the nextImage( ) method at the specified interval. Also, pass it the // value of the randomize parameter. this.playInterval = setInterval(this, "nextImage", interval, randomize); }; // The stop( ) method stops the playback of the images. SequenceViewer.prototype.stop = function ( ) { // Clear the interval. clearInterval(this.playInterval); // Make the viewer invisible. this._visible = false; // Set the current image to be invisible. Otherwise, when the sequence is // played again, this image will still be visible. this.items[this.itemIndex]._visible = false; this.itemIndex = 0; }; // The nextImage( ) method is called at the specified // interval when the sequence is played. SequenceViewer.prototype.nextImage = function (randomize) { // The previous image is made invisible and the index is incremented. this.items[this.itemIndex++]._visible = false; // If we're at the last image, start over at the first image. if (this.itemIndex > this.items.length - 1) { this.itemIndex = 0; } // If randomize is true, create a random index. if (randomize) { this.itemIndex = Math.round(Math.random( ) * (this.items.length - 1)); } // Make the item with the specified index visible. this.items[this.itemIndex]._visible = true; }; // The removeItem( ) method removes the item with the specified index from the // movie and from the array. SequenceViewer.prototype.removeItem = function (index) { this.items[index].removeMovieClip( ); this.items.splice(index, 1); }; Object.registerClass("SequenceViewerSymbol", SequenceViewer); #endinitclip
The Sequence Viewer component is not very complicated. Let's shed some light on the parts that might appear to be more complicated than they really are.
The constructor does three things. First of all, it creates the items array property, which is used to hold references to all the Image components. Next, it creates the background movie clip, which is a black rectangle that fills the Player while the sequence viewer is playing. And finally, the constructor initializes the viewer as invisible because we don't want to see the sequence viewer until the user selects the option to play the sequence.
function SequenceViewer ( ) { this.items = new Array( ); this.createEmptyMovieClip("background", this.getNewDepth( )); with (this.background) { lineStyle(0, 0x000000, 0); beginFill(0, 100); drawRectangle(Stage.width, Stage.height); endFill( ); _x = Stage.width/2; _y = Stage.height/2; } this._visible = false; }
The addItem( ) method is a straightforward method that adds a new Image component to the viewer. When the image is added, we also append it to the items array. The items array is what determines the order in which the sequence plays back, so each new image is added to the end of the sequence. Additionally, we want to make each new image invisible, because the playback works by turning on and off the visibility of the images in sequence.
SequenceViewer.prototype.addItem = function (url) { var uniqueVal = this.getNewDepth( ); var img = this.attachMovie("ImageSymbol", "image" + uniqueVal, uniqueVal); img.load(url); img.setOnLoad("onImageLoad", this); this.items.push(img); img._visible = false; };
The changeOrder( ) method changes the order of an element in the sequence, given the original index and the new index. We accomplish this by first deleting the element at the old index and then inserting it into the array at the new index.
SequenceViewer.prototype.changeOrder = function (prevIndex, newIndex) { var img = this.items[prevIndex]; this.items.splice(prevIndex, 1); this.items.splice(newIndex, 0, img); };
We want to play back the images, one at a time, at a set interval. Therefore, we use the setInterval( ) function to repeatedly call a method that updates the image display. In this case, we save the interval ID to a property (playInterval) so that we can clear the interval when the user stops the playback:
SequenceViewer.prototype.play = function (interval, randomize) { this._visible = true; this.itemIndex = -1; this.playInterval = setInterval(this, "nextImage", interval, randomize); };
The stop( ) method clears the play interval, first and foremost. This stops the images from being played. We also want to make the viewer invisible again. Additionally, it is important that we reset the last image that was visible to be invisible again. If we didn't do this, there could be problems with overlapping images when the sequence is played again.
SequenceViewer.prototype.stop = function ( ) { clearInterval(this.playInterval); this._visible = false; this.items[this.itemIndex]._visible = false; this.itemIndex = 0; };
The nextImage( ) method is called at the interval when the sequence is played. Each time the method is called, we make the previous image invisible and make the current image visible. In this case, we increment the itemIndex value within the first line. Because the increment operator (++) appears at the end of the variable, the value is incremented after the previous value is used in the first line. This saves a line of code, although you could insert another line after the first and increment the value there instead.
SequenceViewer.prototype.nextImage = function (randomize) { this.items[this.itemIndex++]._visible = false; if (this.itemIndex > this.items.length - 1) { this.itemIndex = 0; } if (randomize) { this.itemIndex = Math.round(Math.random( ) * (this.items.length - 1)); } this.items[this.itemIndex]._visible = true; };
The sequencer is composed of sequence items. The items are rectangles into which thumbnails are loaded. Figure 25-2 shows an example of the sequencer with two sequence items in it.
The Sequence Item component should have the following functionality:
Loads a thumbnail from a URL
Is selectable (outline highlights blue to indicate selection)
Can be dragged and dropped within the constraints of the sequencer
Complete the following steps to create the Sequence Item component:
Create a new movie clip symbol named SequenceItem.
Edit the linkage properties of the symbol.
Select the Export for ActionScript and Export in First Frame checkboxes.
Set the linkage identifier to SequenceItemSymbol.
Click OK.
Edit the new symbol.
On the default layer, add the following code to the first frame:
#initclip 1 function SequencerItem ( ) { // Create the background movie clip (the rectangular frame). Add a fill and a // outline to the background and draw a filled rectangle and an outline in // them. this.createEmptyMovieClip("background", this.getNewDepth( )); this.background.createEmptyMovieClip("fill", this.background.getNewDepth( )); this.background.createEmptyMovieClip("outline", this.background.getNewDepth( )); with (this.background.fill) { lineStyle(0, 0x000000, 0); beginFill(0xFFFFFF, 100); drawRectangle(100, 50); endFill( ); _x = 50; _y = 25; } with (this.background.outline) { lineStyle(0, 0x000000, 100); drawRectangle(100, 50); _x = 50; _y = 25; } // Create a color object to target the outline. this.background.outline.col = new Color(this.background.outline); }; SequencerItem.prototype = new MovieClip( ); // The loadImage( ) method adds an image component and loads an image from a URL. SequencerItem.prototype.loadImage = function (url) { this.url = url; this.attachMovie("ImageSymbol", "img", this.getNewDepth( )); this.img.load(url, 100, 50); this.img.setOnLoad("onImageLoad", this); }; // The onImageLoad( ) method is the callback function that is invoked // automatically when the image has completed loading. At that point, it scales // the image to fit within the sequence item frame and moves it to the center. SequencerItem.prototype.onImageLoad = function (imageHolder) { this.img.scale(100, 50); this.img._x = this.background._width/2 - this.img._width/2; this.img._y = this.background._height/2 - this.img._height/2; }; // The onEnterFrame( ) method continually checks to see if the component is being // dragged (dragging is set to true when the component is pressed). If it is, the // method performs a series of actions. SequencerItem.prototype.onEnterFrame = function ( ) { if (this.dragging) { // Loop through all the other sequence items in the sequencer (this._parent), // and if the item that is being dragged has a lower depth than another item // that it is being dragged over, swap depths. for (var mc in this._parent) { if (this.hitTest(this._parent[mc]) && this.getDepth( ) < this._parent[mc].getDepth( )) { this.swapDepths(this._parent[mc]); } } // Get a reference to the sequencer's scroll pane and its scroll content. var sp = this._parent._parent.sp; var sc = sp.getScrollContent( ); // If the mouse is outside the scroll pane, increment or decrement the scroll // position accordingly. Also, move the sequencer item accordingly. if (sp._xmouse > sp._width) { sp.setScrollPosition(sp.getScrollPosition( ).x + 5, 0); } else if (sp._xmouse < 0) { sp.setScrollPosition(sp.getScrollPosition( ).x - 5, 0); } this._x = sc._xmouse - this.clickPosition; } }; SequencerItem.prototype.onPress = function ( ) { // Get the x coordinate of the mouse within the item's coordinate system at the // time the mouse was pressed. this.clickPosition = this._xmouse; // Get the x coordinate of the item before it is moved. This is used to snap // items to the correct positions. this.startPosition = this._x; // Set dragging to true so that the actions in // the onEnterFrame( ) method are activated. this.dragging = true; // Make the item draggable along the X axis within the sequencer. this.startDrag(false, 0, this._y, this._parent._width, this._y); // Toggle the selected state. this.selected = !this.selected; if (this.selected) { this.background.outline.col.setRGB(0xFF); this.onSelectPath[this.onSelectCB](this); } }; SequencerItem.prototype.onRelease = function ( ) { // Set dragging to false so the onEnterFrame( ) actions // stop executing and stop the draggability. this.dragging = false; this.stopDrag( ); // Get the drop target and split it into an array using a slash as the // delimiter. The value of the drop target is given in Flash 4 syntax, so the // slashes are used where dots are used in Flash 5+ syntax. var itemBAr = this._droptarget.split("/"); // Remove the end items from the array until // the last element contains the value "item". while(itemBAr[itemBAr.length - 1].indexOf("item") == -1) { itemBAr.pop( ); } // Call the onDrop( ) callback function, and pass it the reference to this item // and the drop target item. This.onDropPath[this.onDropCB](this, eval(itemBAr.join("/"))); if (!this.selected) { this.deselect( ); } }; // Set the onReleaseOutside( ) method to do the same thing as onRelease( ). SequencerItem.prototype.onReleaseOutside = SliderMenuItem.prototype.onRelease; SequencerItem.prototype.deselect = function ( ) { this.selected = false; this.background.outline.col.setRGB(0); }; SequencerItem.prototype.setOnSelect = function (functionName, path) { if (path == undefined) { path = this._parent; } this.onSelectCB = functionName; this.onSelectPath = path; }; SequencerItem.prototype.setOnDrop = function (functionName, path) { if (path == undefined) { path = this._parent; } this.onDropCB = functionName; this.onDropPath = path; }; Object.registerClass("SequencerItemSymbol", SequencerItem); #endinitclip
The Sequencer Item component contains many code elements that are similar to the other components that we have already created throughout this chapter. However, there are a few parts of the code that involve techniques that are unique to the Sequence Item component, and they bear further discussion.
In the onEnterFrame( ) method, the component continually checks to see if the property named dragging is true. The dragging property is true when, and only when, the user is pressing the component. This technique is nothing new, as we have used it throughout several of the other components. What is new is the rest of the code.
First of all, when the sequencer item is being dragged, we want to make sure that it appears above all the other sequence items that might be in the sequencer. We achieve this by performing a hit test on every other sequencer item and swapping depths with any item that the selected component is obscured by (overlapping and beneath). Sequencer items are contained within a scroll content movie clip within the sequencer, so we can loop through all the elements of the parent movie clip using a for...in statement. Then, we check to see if the selected item is touching another sequencer item with a hitTest( ) method. If the selected item is beneath an item for which the hit test is true, we use swapDepths( ) to bring the selected item forward.
for (var mc in this._parent) { if (this.hitTest(this._parent[mc]) && this.getDepth( ) < this._parent[mc].getDepth( )) { this.swapDepths(this._parent[mc]); } }
The next part of the onEnterFrame( ) method code may look like the most challenging thus far, but it is not so bad once you understand the problem we are trying to solve. When the user clicks on the sequencer item and then drags the mouse pointer beyond the sequencer scroll pane, the scroll pane does not scroll. This is not the desired behavior. We want the scroll pane to automatically scroll in the same direction as the mouse pointer. To accomplish this, we continually compare the position of the mouse pointer to the boundaries of the scroll pane (given by 0 on the left and _width on the right). If the mouse position is greater than the width of the scroll pane, we increment the scroll pane's scroll position by five. On the other hand, if the mouse position is less than zero (meaning it is to the left of the scroll pane), we decrement the scroll position by five. In addition to this, we move the sequencer item; otherwise, the sequencer item and the scroll pane content would be out of synch. In the onPress( ) method, we recorded the value of the x coordinate where the mouse clicked on the item to begin with. We then set the x coordinate of the item to the x coordinate of the mouse pointer minus the offset at which the user clicked on the item.
var sp = this._parent._parent.sp; var sc = sp.getScrollContent( ); if (sp._xmouse > sp._width) { sp.setScrollPosition(sp.getScrollPosition( ).x + 5, 0); } else if (sp._xmouse < 0) { sp.setScrollPosition(sp.getScrollPosition( ).x - 5, 0); } this._x = sc._xmouse - this.clickPosition;
The onPress( ) method contains only a few things that need to be mentioned here. The x coordinate of the sequencer item at the time it is clicked is saved in the startPosition property. This value is used later, when the item is released (and the dropItem( ) method is called), to determine where to place the item. Also, the startDrag( ) method constrains the area in which the item can be moved to a horizontal line spanning the width of the sequencer scroll pane's contents.
SequencerItem.prototype.onPress = function ( ) { this.clickPosition = this._xmouse; this.startPosition = this._x; this.dragging = true; this.startDrag(false, 0, this._y, this._parent._width, this._y); this.selected = !this.selected; if (this.selected) { this.background.outline.col.setRGB(0xFF); this.onSelectPath[this.onSelectCB](this); } };
The onRelease( ) method involves some code that might appear confusing until we look at it in a little more detail. This method attempts to get a reference to a sequencer item onto which the selected sequencer item is dropped. We then call the dropItem( ) method of the sequencer with both a reference to the selected item and the drop target item. This is relatively simple, except for the fact that Flash reports the innermost nested movie clip as the drop target. This means that if, for example, the selected sequencer item is dragged over and dropped onto the sequencer item instance named item3, the value returned by the _droptarget property might be "/seqncr/sc/item3/img/imageHolder" or "/seqncr/sc/item3/background/fill" (in which seqncr is the instance name of the sequencer on the main timeline). The reason for this is that the _droptarget property reports the nested movie clips of img.imageHolder and background.fill instead of the parent movie clip, item3.
This is the expected behavior. However, we want to extract the portion of the path that resolves to the sequencer item ins