DekGenius.com
[ Team LiB ] Previous Section Next Section

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:

  1. Create a new movie clip symbol named Image.

  2. Edit the linkage properties of the symbol.

  3. Select the Export for ActionScript and Export in First Frame checkboxes.

  4. Set the linkage identifier to ImageSymbol.

  5. Click OK.

  6. Edit the new symbol.

  7. 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.

Figure 25-1. A preview pane with two opened image viewers
figs/ascb_2501.gif

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:

  1. Create a new movie clip symbol named ImageViewPane.

  2. Edit the linkage properties of the symbol.

  3. Select the Export for ActionScript and Export in First Frame checkboxes.

  4. Set the linkage identifier to ImageViewPaneSymbol.

  5. Click OK.

  6. Edit the new symbol.

  7. 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:

  1. Create a new movie clip symbol named PreviewPane.

  2. Edit the linkage properties of the symbol.

  3. Select the Export for ActionScript and Export in First Frame checkboxes.

  4. Set the linkage identifier to PreviewPaneSymbol.

  5. Click OK.

  6. Edit the new symbol.

  7. 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:

  1. Create a new movie clip symbol named SequenceViewer.

  2. Edit the linkage properties of the symbol.

  3. Select the Export for ActionScript and Export in First Frame checkboxes.

  4. Set the linkage identifier to SequenceViewerSymbol.

  5. Click OK.

  6. Edit the new symbol.

  7. 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.

Figure 25-2. The sequencer with two sequence items
figs/ascb_2502.gif

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:

  1. Create a new movie clip symbol named SequenceItem.

  2. Edit the linkage properties of the symbol.

  3. Select the Export for ActionScript and Export in First Frame checkboxes.

  4. Set the linkage identifier to SequenceItemSymbol.

  5. Click OK.

  6. Edit the new symbol.

  7. 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:

  1. Create a new movie clip symbol named Sequencer.

  2. Edit the linkage properties of the symbol.

  3. Select the Export for ActionScript and Export in First Frame checkboxes.

  4. Set the linkage identifier to SequencerSymbol.

  5. Click OK.

  6. Edit the new symbol.

  7. 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:

  1. Open a new text document in an external text editor.

  2. 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>
  3. 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:

  1. Create a new movie clip symbol named Menu.

  2. Edit the linkage properties of the symbol.

  3. Select the Export for ActionScript and Export in First Frame checkboxes.

  4. Set the linkage identifier to MenuSymbol.

  5. Click OK.

  6. Edit the new symbol.

  7. 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});
}
    [ Team LiB ] Previous Section Next Section