25.3 Creating the Components
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:
- Image
-
A component that loads images given a URL
- ImageViewPane
-
A component that allows an image to be moved and resized
- PreviewPane
-
A component into which image view panes are added
- SequenceViewer
-
The viewer for the full image slideshow sequence
- SequencerItem
-
One of the thumbnail items that can be added to the sequencer
- Sequencer
-
The component into which thumbnails are added and ordered
- Menu
-
The menu for the application
25.3.1 Designing the Image Element
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;
};
25.3.2 Designing the Image View Pane
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);
}
};
25.3.3 Designing the Preview Pane
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( );
}
};
25.3.4 Designing the Sequence Viewer
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;
};
25.3.5 Designing the Sequencer Item Component
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 instance,
such as "/seqncr/sc/item3". Since
the parts of the path returned by _droptarget are
separated by slashes (since it is given in Flash 4 syntax), it is
convenient to use the split( ) method to split
the string into an array using a slash as the delimiter. At that
point, we use a while loop to remove elements
from the end of the array until the last element contains the
substring "item". Then, we call the
onDrop( ) callback function with a reference to
the selected sequencer item (this), and a
reference to the drop target item. To get an actual reference to the
drop target item, we have to use join( ) to
reassemble the path as a Flash 4-syntax string and use
eval( ) to convert that string to a movie clip
reference.
SequencerItem.prototype.onRelease = function ( ) {
this.dragging = false;
this.stopDrag( );
var itemBAr = this._droptarget.split("/");
while(itemBAr[itemBAr.length - 1].indexOf("item") == -1) {
itemBAr.pop( );
}
this.onDropPath[this.onDropCB](this, eval(itemBAr.join("/")));
if (!this.selected) {
this.deselect( );
}
};
25.3.6 Designing the Sequencer Component
The sequencer
is the part of the application in which the thumbnails are loaded and
can be ordered by the user. The sequencer itself is composed of a
scroll pane and sequencer item components. The sequencer must:
Add new sequencer items when the user chooses to add an image to the
sequencer Remove a selected sequencer item Reorder sequencer items when they are dragged and dropped by the user Target a sequence viewer
Complete these steps to create the Sequencer component:
Create a new movie clip symbol named Sequencer. Edit the linkage properties of the symbol. Select the Export for ActionScript and Export in First Frame
checkboxes. Set the linkage identifier to SequencerSymbol. Click OK. Edit the new symbol. On the default layer, add the following code to the first frame: #initclip
function Sequencer ( ) {
// Define the sequencer to have a width of 500 and a height of 75.
var w = 500;
var h = 75;
// Define the width and height for each sequencer item.
this.itemWidth = 100;
this.itemHeight = 50;
// Add a scroll pane and set the size of it to the width and height.
this.attachMovie("FScrollPaneSymbol", "sp", this.getNewDepth( ));
this.sp.setSize(w, h);
// Create a scroll content movie clip and create a background in it. The
// background serves a similar purpose to the background for the scroll content
// in the Preview Pane component.
this.createEmptyMovieClip("sc", this.getNewDepth( ));
this.sc.createEmptyMovieClip("background", 0);
with (this.sc.background) {
lineStyle(0, 0x000000, 100);
beginFill(0xFFFFFF, 100);
drawRectangle(w, h);
endFill( );
_x += w/2;
_y += h/2;
}
// Create the array that is used to store the order of the sequencer items.
this.items = new Array( );
}
Sequencer.prototype = new MovieClip( );
Sequencer.prototype.getItems = function ( ) {
return this.items;
};
// Add an item to the sequencer with a URL to an image.
Sequencer.prototype.addItem = function (url) {
var uniqueVal = this.sc.getNewDepth( );
// Add a Sequencer Item component and load the image into it.
var item = this.sc.attachMovie("SequencerItemSymbol", "item" + uniqueVal,
uniqueVal);
item.loadImage(url);
// Position the sequencer item such that it is to the right of any other items
// in the sequencer.
item._x += (this.items.length * 105) + 5;
item._y += 5;
item.setOnDrop("dropItem", this);
item.setOnSelect("setSelected", this);
// Set the background of the scroll content to match the width of the scroll
// content plus five (so that there is a five-pixel margin on the right side of
// the scroll content).
this.sc.background._width = this.sc._width + 5;
this.sc.background._x = this.sc.background._width/2;
// Update the scroll pane view.
this.sp.setScrollContent(this.sc);
// Add the sequencer item to the items array.
this.items.push(item);
};
// The dropItem( ) method is invoked any time one of the sequencer items is
// released by the user. It is passed a reference to the item that was dropped
// and a reference to the drop target.
Sequencer.prototype.dropItem = function (itemA, itemB) {
// If the drop target (itemB) is either undefined or is the scroll content
// background, reset itemA back to the starting position.
if (itemB == undefined || itemB == this.sc.background) {
itemA._x = itemA.startPosition;
} else {
// . . . Otherwise, set itemA to the same position as the drop target.
itemA._x = itemB._x;
var aIndex = 0;
var bIndex = 0;
var aSet = false;
var bSet = false;
// Loop through the sequencer items and find the indexes of itemA and itemB.
for (var i = 0; i < this.items.length; i++) {
if (this.items[i] == itemA) {
aIndex = i;
aSet = true;
} else if (this.items[i] == itemB) {
bIndex = i;
bSet = true;
}
if (aSet && bSet) {
break;
}
}
// Shift the rest of the items appropriately.
if (aIndex < bIndex) {
for (var i = aIndex + 1; i <= bIndex; i++) {
this.items[i]._x -= (this.itemWidth + 5);
}
} else {
for (var i = bIndex; i < aIndex; i++) {
this.items[i]._x += (this.itemWidth + 5);
}
}
// Change the order of the items.
this.items.splice(aIndex, 1);
this.items.splice(bIndex, 0, itemA);
// Call the changeOrder( ) method of the targeted sequence viewer so that the
// order of the images during playback is correct.
this.sequenceVwr.changeOrder(aIndex, bIndex);
}
};
// Set an item to be selected, and deselect the rest of the items.
Sequencer.prototype.setSelected = function (item) {
for (var i = 0; i < this.items.length; i++) {
if (this.items[i] == item) {
this.selectedItemIndex = i;
} else {
this.items[i].deselect( );
}
}
};
// The removeSelected( ) method removes the selected item from the sequencer.
Sequencer.prototype.removeSelected = function ( ) {
// Shift all the items after the removed item.
for (var i = this.selectedItemIndex + 1; i < this.items.length; i++) {
this.items[i]._x -= 105;
}
// Remove the selected Sequencer Item component.
this.items[this.selectedItemIndex].removeMovieClip( );
// Remove the item from the items array.
this.items.splice(this.selectedItemIndex, 1);
// Remove the item from the sequence viewer as well.
this.sequenceVwr.removeItem(this.selectedItemIndex);
};
// Set the targeted sequence viewer.
Sequencer.prototype.setSequenceViewer = function (sequenceVwr) {
this.sequenceVwr = sequenceVwr;
};
Object.registerClass("SequencerSymbol", Sequencer);
#endinitclip
The sequencer component employs many of the same techniques as
several of the other components throughout this chapter, so much of
the code should be quite familiar to you. However, there are a few
code snippets that do require a little further illumination.
The dropItem( ) method is, perhaps, the most
intimidating of the methods in this component class. However, upon
closer examination you will see that it is not difficult to
understand. The method is set to be the onDrop
callback function for each of the sequencer items. This means that
each time a sequencer item is dropped, the dropItem(
) method is called and passed a reference to the item that
was dropped, as well as the drop target item. We first determine
whether the drop target is another sequencer item. If the drop target
item (itemB) is undefined or the scroll content
background, we know that the item was not dropped on another
sequencer item. Therefore, we want to reset the position of the
dropped item to its starting position, which is recorded in the
item's
startPosition property:
if (itemB == undefined || itemB == this.sc.background) {
itemA._x = itemA.startPosition;
}
Otherwise, if the drop target is a sequencer item, we rearrange all
the sequencer items appropriately. The first thing to do, therefore,
is to set the position of itemA to the position of
itemB. This snaps itemA to the
correct slot in the sequencer.
itemA._x = itemB._x;
Once itemA is in place, we still need to
accomplish several remaining tasks, for which we need to know the
indexes of itemA and itemB
within the items array. Therefore, we use a
for loop to search for itemA
and itemB in the items array.
When we find a match, we set the appropriate variable (either
aIndex or bIndex) to the value
of the looping index (i). To make the loop more
efficient, we use two Boolean variables to keep track of whether
itemA and itemB have been
located. Once both indexes have been located, we break out of the
for loop.
var aIndex = 0;
var bIndex = 0;
var aSet = false;
var bSet = false;
for (var i = 0; i < this.items.length; i++) {
if (this.items[i] == itemA) {
aIndex = i;
aSet = true;
} else if (this.items[i] == itemB) {
bIndex = i;
bSet = true;
}
if (aSet && bSet) {
break;
}
}
Once the indexes are known, we can reposition the appropriate items.
If itemA was originally positioned to the left of
itemB, we shift all the elements from (but not
including) itemA through (and including)
itemB to the left by the width of a single item
plus the 5-pixel buffer. Otherwise, if itemA was
originally to the right of itemB, we shift
everything from (and including) itemB up to (but
not including) itemA to the right by the width of
a single item plus the 5-pixel buffer.
if (aIndex < bIndex) {
for (var i = aIndex + 1; i <= bIndex; i++) {
this.items[i]._x -= (this.itemWidth + 5);
}
} else {
for (var i = bIndex; i < aIndex; i++) {
this.items[i]._x += (this.itemWidth + 5);
}
}
Finally, we adjust the order of the items in the
items array as well as the targeted sequence
viewer. We use the splice( ) method to first
remove itemA from the items
array. Then, we use the splice( ) method to
insert itemA back into the array at the index that
was previously assigned to itemB.
this.items.splice(aIndex, 1);
this.items.splice(bIndex, 0, itemA);
this.sequenceVwr.changeOrder(aIndex, bIndex);
25.3.7 Designing the Menu Component
The Menu
component is the part of the application that does the following:
Reads an XML document and populates a list box with available images Allows users to preview low-resolution images Allows users to add images to the sequencer Allows users to start the playback of the sequence of images
The first step in designing the Menu component is to create the
XML document that the menu uses
to populate itself. We won't actually load the XML
directly from the Menu component (we do that in the main application
routine), but you need to be familiar with the structure of the
document to understand parts of the Menu component code. Follow these
steps:
Open a new text document in an external text editor. Add code with the following structure to your document. You can use
the URLs that are in this sample document (if you will play the Flash
movie in the Standalone Player), or you can replace them with your
own valid URLs to images on your own computer or the server on which
you will be serving the Flash movie. This example document includes
two <image> elements. If you want to add
more images to your application, you can add more
<image> elements to your XML document. <images>
<image>
<title>Waterfall</title>
<thumbnail>http://www.person13.com/ascb/data/images/image1_thumbnail.jpg
</thumbnail>
<lowRes>http://www.person13.com/ascb/data/images/image1_lowRes.jpg</lowRes>
<full>http://www.person13.com/ascb/data/images/image1.jpg</full>
</image>
<image>
<title>Lake</title>
<thumbnail>http://www.person13.com/ascb/data/images/image2_thumbnail.jpg
</thumbnail>
<lowRes>http://www.person13.com/ascb/data/images/image2_lowRes.jpg</lowRes>
<full>http://www.person13.com/ascb/data/images/image2.jpg</full>
</image>
</images> Save the document as images.xml to the same
directory as your Flash document.
The root element of the XML document is
<images>, and the root element contains
<image> child nodes for each image that
should appear in the menu. Each <image>
element, in turn, has four child nodes:
<title>,
<thumbnail>,
<lowRes>, and
<full>. The title is the name of the image
that appears both in the list box and in the title bar of the preview
image. If you do not have a low-resolution and/or thumbnail version
of an image, you can use the same URL for two or all three of the
image variations. The application automatically resizes the
thumbnails, and the preview images can be of any size. The only
purpose in having variations for each of the images is because the
low-resolution and thumbnail versions can have smaller file sizes
(and therefore take less time to load).
To create the Menu component, complete the following steps:
Create a new movie clip symbol named Menu. Edit the linkage properties of the symbol. Select the Export for ActionScript and Export in First Frame
checkboxes. Set the linkage identifier to MenuSymbol. Click OK. Edit the new symbol. On the default layer, add the following code to the first frame: #initclip
function Menu ( ) {
// Create the list box to list the available images and add a value to it that
// says "loading . . . " to initialize it.
this.attachMovie("FListBoxSymbol", "imagesMenu", this.getNewDepth( ));
this.imagesMenu.addItem("loading...");
// Add the preview, sequencer add, and play buttons.
this.attachMovie("FPushButtonSymbol", "previewBtn", this.getNewDepth( ));
this.attachMovie("FPushButtonSymbol", "addBtn", this.getNewDepth( ));
this.attachMovie("FPushButtonSymbol", "playBtn", this.getNewDepth( ));
// Create a table to organize the menu elements.
var tr0 = new TableRow(0, new TableColumn(0, this.imagesMenu));
var tr1 = new TableRow(0, new TableColumn(0, this.previewBtn));
var tr2 = new TableRow(0, new TableColumn(0, this.addBtn));
var tr3 = new TableRow(0, new TableColumn(0, this.playBtn));
var t = new Table(3, 0, 0, tr0, tr1, tr2, tr3);
}
Menu.prototype = new MovieClip( );
// The setValues( ) method takes an XML object parameter and populates the menu.
Menu.prototype.setValues = function (xmlData) {
// Remove the "loading . . . " message from the list box.
this.imagesMenu.removeItemAt(0);
var imageNodes = xmlData.firstChild.childNodes;
var imageNode, title, thumbnail, full;
// Loop through all the <image> nodes.
for (var i = 0; i < imageNodes.length; i++) {
imageNode = imageNodes[i];
// Get the title, thumbnail URL, low-resolution URL, and full image URL.
title = imageNode.firstChild.firstChild.nodeValue;
thumbnail = imageNode.firstChild.nextSibling.firstChild.nodeValue;
lowRes = imageNode.firstChild.nextSibling.nextSibling.firstChild.nodeValue;
full = imageNode.lastChild.firstChild.nodeValue;
// Add an item to the list box. The item should display the image title and
// the data for the item should be an object with thumbnail, full, lowRes,
// and title properties.
this.imagesMenu.addItem(title, {thumbnail: thumbnail, full: full,
lowRes: lowRes, title: title});
}
// Adjust the width of the list box to accommodate the titles.
this.imagesMenu.adjustWidth( );
this.previewBtn.setLabel("preview image");
this.addBtn.setLabel("add to sequence");
this.playBtn.setLabel("start sequence");
this.playBtn.setClickHandler("startSequence", this);
this.previewBtn.setClickHandler("previewImage", this);
this.addBtn.setClickHandler("addImageToSequence", this);
};
// Set the reference to the preview pane.
Menu.prototype.setPreviewPane = function (previewPn) {
this.previewPn = previewPn;
};
// This is the callback function for the preview button. It calls the open( )
// method of the preview pane with the selected low-resolution URL and title.
Menu.prototype.previewImage = function ( ) {
var selected = this.imagesMenu.getValue( );
var lrURL = selected.lowRes;
var title = selected.title;
this.previewPn.open(lrURL, title);
};
// Set the reference to the sequencer and sequence viewer.
Menu.prototype.setSequencer = function (sqncr, seqViewer) {
this.sqncr = sqncr;
this.seqViewer = seqViewer;
};
// This is the callback function for the sequencer add button. It calls the
// addItem( ) methods of both the sequencer and the sequence viewer with the
// appropriate URLs.
Menu.prototype.addImageToSequence = function ( ) {
var selected = this.imagesMenu.getValue( );
var tnURL = selected.thumbnail;
var fullURL = selected.full;
this.sqncr.addItem(tnURL);
this.seqViewer.addItem(fullURL);
};
// This is the callback function for the start sequence playback button. It calls
// the play( ) method of the sequence viewer with an interval of 3000
// milliseconds (3 seconds) per image.
Menu.prototype.startSequence = function ( ) {
this.seqViewer.play(3000);
};
Object.registerClass("MenuSymbol", Menu);
#endinitclip
The Menu component class is fairly straightforward. The
setValues( ) method is the only portion of it that
may potentially be a little confusing at first. The method is passed
an XML object parameter. The
XML object should be in the same format as the
images.xml document, and it should not have any
extra whitespace nodes (we take care of all of this in the main
routine of the application). Then, inside the setValues(
) method, we extract the values for each
image's title and the three URLs. We then assign
those values to items within the list box. The
label for each list box item should be the title,
but the data should be an object that contains all
the values for that image. This is important because it then gives us
access to all that information for an image when it is selected from
the menu.
for (var i = 0; i < imageNodes.length; i++) {
imageNode = imageNodes[i];
title = imageNode.firstChild.firstChild.nodeValue;
thumbnail = imageNode.firstChild.nextSibling.firstChild.nodeValue;
lowRes = imageNode.firstChild.nextSibling.nextSibling.firstChild.nodeValue;
full = imageNode.lastChild.firstChild.nodeValue;
this.imagesMenu.addItem(title, {thumbnail: thumbnail, full: full,
lowRes: lowRes, title: title});
}
|