| 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:
For the image viewer/slideshow application, we will make seven components:
25.3.1 Designing the Image ElementThe 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:
To create the Image component, complete the following steps:
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 PaneBefore 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. Figure 25-1. 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:
Follow these steps to create the image view pane component:
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 PaneThe 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:
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 ViewerThe 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:
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 ComponentThe 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. Figure 25-2. The sequencer with two sequence items![]() The Sequence Item component should have the following functionality:
Complete the following steps to create the Sequence Item component:
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 ComponentThe 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:
Complete these steps to create the Sequencer component:
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 ComponentThe Menu component is the part of the application that does the following:
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:
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:
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}); } |