overview

goal: create an ImageViewer class that can display an image on screen

image resides in a window with a border

might need it for a photo album or a product viewer

prerequisites for this lecture: know what a class is, know what methods and properties are

brainstorm the functional requirements

start by listing all possible functional requirements

  • crop an image to a particular rectangular "view region"
  • display a border around the image
  • display image load progress
  • reposition the viewer
  • resize the viewer
  • pan (reposition) the image within the viewer
  • zoom (resize) the image within the viewer
  • do the simplest thing that works

    next pick the requirements that can most easily be met

  • load an image
  • display an image
  • "never add functionality early" (extreme programming rule)

    creating the public api

    next determine how class will be used by other programmers

    for example:

  • what method would a programmer invoke on an ImageViewer to display an image?
  • what method would a programmer invoke to load an image?
  • are loading and displaying two methods or one?
  • a class's set of public methods and properties is called its "public api"

    the loadImage() method

    a programmer using an ImageViewer instance needs a "load" command

    the "load" command requires a URL (the location of the image to load)

    ImageViewer.loadImage(URL:String)

    method's name is a verb describing the action to perform

    if you're having trouble naming a method, the real cause may be that your method is trying to do too much

    splitting the method into multiple methods or restructure the class

    displaying an image

    in flash, images must reside in a movie clip

    which movie clip will contain our ImageViewer's image?

    perhaps a programmer should be able to specify an existing movie clip:

    var viewer:ImageViewer = new ImageViewer(); viewer.setImageClip(someClip_mc); // clip to hold image viewer.loadImage("someImage.jpg");

    works, but has drawbacks:

  • programmer might set the image clip to _level0, replacing the entire flash player contents with the image
  • changing to a new image-container clip would orphan the old image container
  • avoid these problems by creating the image-container within ImageViewer

    force programmers to specify image container's parent and depth via constructor

    ImageViewer(target:MovieClip, depth:Number)

    here's how our class would be used:

    var viewer:ImageViewer = new ImageViewer(someClip_mc, 1); viewer.loadImage("someImage.jpg");

    when loading is complete, someImage.jpg will be displayed in a new clip, on depth 1 of someClip_mc

    the public api, take 1

    our ImageViewer class's public api now looks like this

    ImageViewer(target:MovieClip, depth:Number) ImageViewer.loadImage(URL:String):Void

    create the class file

    our ImageViewer class must reside in a text file named ImageViewer.as

    create ImageViewer.as in a text editor or in flash's text editor (flash professional only)

    i use SE|PY to create classes

    skeleton code

    start with basic class skeleton

    class ImageViewer { }

    add method skeletons

    class ImageViewer { // The constructor function public function ImageViewer (target:MovieClip, depth:Number) { } // The loadImage( ) method public function loadImage (URL:String):Void { } }

    class doesn't do anything yet, but faithfully represents our api design

    constructor implementation

    constructor function must create a clip in which to display an image

    public function ImageViewer (target:MovieClip, depth:Number) { target.createEmptyMovieClip("container_mc" + depth, depth); }

    loadImage() implementation

    loadImage() needs to load an image into the clip created by the constructor ("container_mc"...)

    public function loadImage (URL:String):Void { container_mc.loadImag.........UH OH! }

    problem! loadImage() cannot access container clip!

    constructor must store a reference to container clip in a property

    once container clip is in a property, loadImage() can access it

    the container_mc property

    new property, container_mc, stores the image container clip

    private var container_mc:MovieClip;

    revise constructor to store clip in container_mc

    public function ImageViewer (target:MovieClip, depth:Number) { container_mc = target.createEmptyMovieClip("container_mc" + depth, depth); }

    loadImage() implementation (try again)

    loadImage() can now access the image container clip via container_mc

    public function loadImage (URL:String):Void { container_mc.loadMovie(URL); }

    ImageViewer code, Version 1

    here's ImageViewer our class so far:

    class ImageViewer { private var container_mc:MovieClip; public function ImageViewer (target:MovieClip, depth:Number) { container_mc = target.createEmptyMovieClip("container_mc" + depth, depth); } public function loadImage (URL:String):Void { container_mc.loadMovie(URL); } }

    using ImageViewer in a movie

    place non-progressive JPEG, picture.jpg, in same folder as ImageViewer.as

    create new .fla file, imageViewer.fla in same folder as ImageViewer.as

    rename "Layer 1" to "scripts"

    add this code to frame 1 of scripts layer:

    var viewer:ImageViewer = new ImageViewer(this, 1); viewer.loadImage("picture.jpg");

    test the movie!

    preloading ImageViewer

    large application can have many classes, 50k - 100k not uncommon

    good form to preload all classes

    make imageViewer.fla load classes at frame 10:

  • Choose File > Publish Settings.
  • On the Flash tab, next to the ActionScript Version, click Settings.
  • For Export Frame for Classes, enter 10.
  • Click OK to confirm the ActionScript Settings.
  • Click OK to confirm the Publish Settings.
  • now add preloader:

  • extend timeline to frame 15
  • add new layer, name it "labels"
  • add keyframes to labels layer at frames 4 and 15
  • label frame 4 "loading" and frame 15 "main"
  • add keyframes to scripts layer at frames 5 and 15
  • add this code to frame 5, scripts layer:
  • if (_framesloaded == _totalframes) { gotoAndStop("main"); } else { gotoAndPlay("loading"); }

  • move this code from frame 1 to frame 15, scripts layer:
  • var viewer:ImageViewer = new ImageViewer(this, 1); viewer.loadImage("picture.jpg");

  • add this code to frame 1, scripts layer:
  • this.createTextField("loadmsg_txt", 0, 200, 200, 0, 0); loadmsg_txt.autoSize = true; loadmsg_txt.text = "Loading...Please wait.";

  • add this code to frame 15, scripts layer:
  • loadmsg_txt.removeTextField();

    cropping the image, adding a border

    next two functional requirements are:

  • cropping an image to a given size
  • drawing a border around an image
  • revised display assets

    can't literally crop an image in flash, have to mask with a movie clip

    border should be drawn in its own movie clip so it can be controlled separately

    container_mc must hold image and mask clip and border clip

    hence, image must be placed in its own movie clip

    container_mc will hold these three movie clips:

  • border_mc (depth 2)
  • mask_mc (depth 1)
  • image_mc (depth 0)
  • visual asset creation methods

    earlier, we created visuals in the constructor function

    now, more visuals to create, so divide the work into private methods:

  • buildViewer() - manages creation of visuals
  • createMainContainer() - creates container_mc clip
  • createImageClip() - creates image_mc clip
  • createImageClipMask() - creates mask_mc clip
  • createBorder() - creates border_mc clip
  • benefits of dividing work into methods:

  • makes code easier to understand
  • simplifies testing (test methods separately)
  • flexibility - assets can be created independently (change border without reloading image)
  • extensibility - allows individual asset creation routines to be modified by a subclass
  • revised constructor

    constructor must now receive mask and border information

  • image position (x, y)
  • mask size (w, h)
  • border thickness
  • border colour
  • revised constructor signature:

    ImageViewer(target:MovieClip, depth:Number, x:Number, y:Number, w:Number, h:Number, borderThickness:Number, borderColor:Number)

    border and mask properties

    new display assets require new properties:

  • container_mc, container for all movie clips
  • target_mc, clip to which container_mc clip is attached
  • containerDepth, depth at which container_mc is attached in target_mc
  • imageDepth, depth on which image_mc is created in container_mc
  • maskDepth, depth on which mask_mc is created in container_mc
  • borderDepth, depth on which border_mc is created in container_mc
  • borderThickness thickness, in pixels, of the image border
  • borderColor, RGB color of the border around the image
  • revised design summary

    here's the updated design of our ImageViewer class:

    ** Constructor ** ImageViewer(target:MovieClip, depth:Number, x:Number, y:Number, w:Number, h:Number, borderThickness:Number, borderColor:Number) ** Private Instance Properties ** container_mc target_mc borderThickness borderColor ** Private Class Properties ** containerDepth imageDepth maskDepth borderDepth ** Public Properties ** None ** Private methods ** buildViewer(x:Number, y:Number, w:Number, h:Number) createMainContainer(x:Number, y:Number) createImageClip( ) createImageClipMask(w:Number, h:Number) createBorder(w:Number, h:Number) ** Public Methods ** loadImage(URL:String)

    notice that the public API has not been changed, only expanded!

    ImageViewer code, Version 2

    code walkthrough for the new ImageViewer design

    class ImageViewer { // Movie clip references. private var container_mc:MovieClip; private var target_mc:MovieClip; // Movie clip depths. private var containerDepth:Number; private static var imageDepth:Number = 0; private static var maskDepth:Number = 1; private static var borderDepth:Number = 2; // Border style. private var borderThickness:Number; private var borderColor:Number; // Constructor public function ImageViewer (target:MovieClip, depth:Number, x:Number, y:Number, w:Number, h:Number, borderThickness:Number, borderColor:Number) { // Assign property values. target_mc = target; containerDepth = depth; this.borderThickness = borderThickness; this.borderColor = borderColor; // Set up the visual assets for this ImageViewer. buildViewer(x, y, w, h); } // Creates the clips to hold the image, mask, and border. // This method subcontracts all its work out to individual // clip-creation methods. private function buildViewer (x:Number, y:Number, w:Number, h:Number):Void { createMainContainer(x, y); createImageClip(); createImageClipMask(w, h); createBorder(w, h); } // Creates the container that holds all the assets. private function createMainContainer (x:Number, y:Number):Void { container_mc = target_mc.createEmptyMovieClip("container_mc" + containerDepth, containerDepth); // Position the container clip. container_mc._x = x; container_mc._y = y; } // Creates the clip into which the image is actually loaded. private function createImageClip ():Void { container_mc.createEmptyMovieClip("image_mc", imageDepth); } // Creates the mask over the image. private function createImageClipMask (w:Number, h:Number):Void { // Only create the mask if a valid width and height are specified. if (!(w > 0 && h > 0)) { return; } // In the container, create a clip to act as the mask over the image. container_mc.createEmptyMovieClip("mask_mc", maskDepth); // Draw a rectangle in the mask. container_mc.mask_mc.moveTo(0, 0); container_mc.mask_mc.beginFill(0x0000FF); // Use blue for debugging. container_mc.mask_mc.lineTo(w, 0); container_mc.mask_mc.lineTo(w, h); container_mc.mask_mc.lineTo(0, h); container_mc.mask_mc.lineTo(0, 0); container_mc.mask_mc.endFill(); // Hide the mask (it will still function as a mask when invisible). // To see the mask during debugging, comment out the next line. container_mc.mask_mc._visible = false; // Notice that we don't apply the mask yet. We must do that // after the image starts loading, otherwise the loading of // the image will remove the mask. } // Creates the border around the image. private function createBorder (w:Number, h:Number):Void { // Only create the border if a valid width and height are specified. if (!(w > 0 && h > 0)) { return; } // In the container, create a clip to hold the border around the image. container_mc.createEmptyMovieClip("border_mc", borderDepth); // Draw a rectangular outline in the border clip, with the // specified dimensions and color. container_mc.border_mc.lineStyle(borderThickness, borderColor); container_mc.border_mc.moveTo(0, 0); container_mc.border_mc.lineTo(w, 0); container_mc.border_mc.lineTo(w, h); container_mc.border_mc.lineTo(0, h); container_mc.border_mc.lineTo(0, 0); } // Loads the image. public function loadImage (URL:String):Void { // Load the JPEG file into the image_mc clip. container_mc.image_mc.loadMovie(URL); // Here comes an ugly hack. We'll clean this up // when we add proper preloading support, in "Take 3": // After one frame passes, the image load will have started, // at which point we safely apply the mask to the image_mc clip. container_mc.onEnterFrame = function ():Void { this.image_mc.setMask(this.mask_mc); delete this.onEnterFrame; } } }

    using ImageViewer, Version 2

    let's try our new ImageViewer out in a movie

    to use the new crop and border features, update one line of code in imageViewer.fla:

    var viewer:ImageViewer = new ImageViewer(this, 1, 100, 100, 250, 250, 10, 0xCE9A3C);

    public API is unchanged, so users of ImageViewer version 1 can easily update to ImageViewer version 2

    furthermore, upgrading to ImageViewer Version 2 does not break old code

    if new constructor parameters are undefined, new methods simply abort:

    private function createImageClipMask (w:Number, h:Number):Void { // Create the mask only if a valid width and height are specified. if (!(w > 0 && h > 0)) { return; } // ...remainder of method not shown }

    after a class is formally released, upgrading to a new version should not break code that uses the old version.

    displaying load progress

    the next (and final) feature we'll implement is load-progress display

    we'll use MovieClipLoader to load the image so we can show the download percentage in text

    we need to make the following changes to ImageViewer:

  • add a MovieClipLoader instance
  • add a text field in which to display load progress
  • add load-progress event handlers
  • modify loadImage() to use MovieClipLoader instead of loadMovie()
  • load-progress properties

    imageLoader property stores the MovieClipLoader instance

    private var imageLoader:MovieClipLoader;

    statusDepth class property specifies the depth of the load-status text field

    private static var statusDepth:Number = 3;

    instantiating MovieClipLoader

    create MovieClipLoader instance in the constructor

    imageLoader = new MovieClipLoader();

    the register the ImageViewer instance to receive the MovieClipLoader's events:

    imageLoader.addListener(this);

    here's the revised constructor:

    public function ImageViewer (target:MovieClip, depth:Number, x:Number, y:Number, w:Number, h:Number, borderThickness:Number, borderColor:Number) { // Assign property values. target_mc = target; containerDepth = depth; this.borderThickness = borderThickness; this.borderColor = borderColor; // Create image loader. imageLoader = new MovieClipLoader(); // Register this instance to receive events // from the imageLoader instance. imageLoader.addListener(this); // Set up the visual assets for this ImageViewer. buildViewer(x, y, w, h); }

    updated image loading system

    code in ImageViewer.loadImage() must change entirely

    now use MovieClipLoader to load the image:

    imageLoader.loadClip(URL, container_mc.image_mc);

    create a load-status text field to show load progress:

    container_mc.createTextField("loadStatus_txt", statusDepth, 0, 0, 0, 0);

    set status text field style and position:

    container_mc.loadStatus_txt.background = true; container_mc.loadStatus_txt.border = true; container_mc.loadStatus_txt.setNewTextFormat(new TextFormat( "Arial, Helvetica, _sans", 10, borderColor, false, false, false, null, null, "right")); container_mc.loadStatus_txt.autoSize = "left"; container_mc.loadStatus_txt._y = 3; container_mc.loadStatus_txt._x = 3; container_mc.loadStatus_txt.text = "LOADING";

    new loadImage() method, full listing:

    public function loadImage (URL:String):Void { imageLoader.loadClip(URL, container_mc.image_mc); container_mc.createTextField("loadStatus_txt", statusDepth, 0, 0, 0, 0); container_mc.loadStatus_txt.background = true; container_mc.loadStatus_txt.border = true; container_mc.loadStatus_txt.setNewTextFormat(new TextFormat( "Arial, Helvetica, _sans", 10, borderColor, false, false, false, null, null, "right")); container_mc.loadStatus_txt.autoSize = "left"; container_mc.loadStatus_txt._y = 3; container_mc.loadStatus_txt._x = 3; container_mc.loadStatus_txt.text = "LOADING"; }

    load progress events

    implement three new methods to handle load progress events:

  • onLoadProgress()
  • onLoadInit()
  • onLoadError()
  • the onLoadProgress() method

    imageLoader invokes onLoadProgress() when part of the image has loaded

    onLoadProgress() displays load status in text field

    public function onLoadProgress (target:MovieClip, bytesLoaded:Number, bytesTotal:Number):Void { container_mc.loadStatus_txt.text = "LOADING: " + Math.floor(bytesLoaded / 1024) + "/" + Math.floor(bytesTotal / 1024) + " KB"; }

    the onLoadInit() method

    imageLoader invokes onLoadInit() when image has loaded and _height and _width properties are initialized

    onLoadInit() has two responsibilities:

  • remove the load status text field
  • apply the cropping mask over the image
  • public function onLoadInit (target:MovieClip):Void { // Remove the loading message. container_mc.loadStatus_txt.removeTextField(); // Apply the mask to the loaded image. container_mc.image_mc.setMask(container_mc.mask_mc); }

    the onLoadError() method

    imageLoader invokes onLoadError() when image load fails

    onLoadError() displays error message in text field

    public function onLoadError (target:MovieClip, errorCode:String):Void { if (errorCode == "URLNotFound") { container_mc.loadStatus_txt.text = "ERROR: File not found."; } else if (errorCode == "LoadNeverCompleted") { container_mc.loadStatus_txt.text = "ERROR: Load failed."; } else { // Catch-all to handle possible future errorCodes. container_mc.loadStatus_txt.text = "Load error: " + errorCode; } }

    deleting resources

    anything your code creates, it should also eventually destroy

    need a destroy() method to remove assets created by ImageViewer

    public function destroy ():Void { // Cancel load event notifications imageLoader.removeListener(this); // Remove movie clips from Stage (removing container_mc removes subclips) container_mc.removeMovieClip(); }

    ImageViewer code, Version 3

    here's the final ImageViewer code, complete with JavaDoc style comments

    /** * ImageViewer, Version 3. * An on-screen rectangular region for displaying a loaded image. * Updates at: http://www.moock.org/eas2/examples/. * * @author: Colin Moock * @version: 2.0.0 */ class ImageViewer { // The movie clip that will contain the all ImageViewer assets. private var container_mc:MovieClip; // The movie clip to which the container_mc will be attached. private var target_mc:MovieClip; // Depths for visual assets. private var containerDepth:Number; private static var imageDepth:Number = 0; private static var maskDepth:Number = 1; private static var borderDepth:Number = 2; private static var statusDepth:Number = 3; // The thickness of the border around the image. private var borderThickness:Number; // The color of the border around the image. private var borderColor:Number; // The MovieClipLoader instance used to load the image. private var imageLoader:MovieClipLoader; /** * ImageViewer Constructor * * @param target The movie clip to which the * ImageViewer will be attached. * @param depth The depth in target on which to * attach the viewer. * @param x The horizonatal position of the viewer. * @param y The vertical position of the viewer. * @param w The width of the viewer, in pixels. * @param h The height of the viewer, in pixels. * @param borderThickness The thickness of the border around the image. * @param borderColor The color of the border around the image. * */ public function ImageViewer (target:MovieClip, depth:Number, x:Number, y:Number, w:Number, h:Number, borderThickness:Number, borderColor:Number) { // Assign property values. target_mc = target; containerDepth = depth; this.borderThickness = borderThickness; this.borderColor = borderColor; imageLoader = new MovieClipLoader(); // Register this instance to receive events // from the imageLoader instance. imageLoader.addListener(this); // Set up the visual assets for this ImageViewer. buildViewer(x, y, w, h); } /** * Creates the onscreen assets for this ImageViewer. * The movie clip hierarchy is: * [d]: container_mc * 2: border_mc * 1: mask_mc (masks image_mc) * 0: image_mc * where [d] is the user-supplied depth passed to the constructor. * * @param x The horizonatal position of the viewer. * @param y The vertical position of the viewer. * @param w The width of the viewer, in pixels. * @param h The height of the viewer, in pixels. */ private function buildViewer (x:Number, y:Number, w:Number, h:Number):Void { // Create the clips to hold the image, mask, and border. createMainContainer(x, y); createImageClip(); createImageClipMask(w, h); createBorder(w, h); } /** * Creates a movie clip, container_mc, to contain * the ImageViewer visual assests. * * @param x The horizonatal position of the * container_mc movie clip. * @param y The vertical position of the * container_mc movie clip. */ private function createMainContainer (x:Number, y:Number):Void { container_mc = target_mc.createEmptyMovieClip("container_mc" + containerDepth, containerDepth); container_mc._x = x; container_mc._y = y; } /** * Creates the clip into which the image is actually loaded. */ private function createImageClip ():Void { container_mc.createEmptyMovieClip("image_mc", imageDepth); } /** * Creates the mask over the image. Note that this method does * not actually apply the mask to the image clip because a clip's * mask is lost when new content is loaded into it. Hence, the mask * is applied from onLoadInit(). * * @param w The width of the mask, in pixels. * @param h The height of the mask, in pixels. */ private function createImageClipMask (w:Number, h:Number):Void { // Only create the mask if a valid width and height are specified. if (!(w > 0 && h > 0)) { return; } // In the container, create a clip to act as the mask over the image. container_mc.createEmptyMovieClip("mask_mc", maskDepth); // Draw a rectangle in the mask. container_mc.mask_mc.moveTo(0, 0); container_mc.mask_mc.beginFill(0x0000FF); // Use blue for debugging. container_mc.mask_mc.lineTo(w, 0); container_mc.mask_mc.lineTo(w, h); container_mc.mask_mc.lineTo(0, h); container_mc.mask_mc.lineTo(0, 0); container_mc.mask_mc.endFill(); // Hide the mask (it will still function as a mask when invisible). container_mc.mask_mc._visible = false; } /** * Creates the border around the image. * * @param w The width of the border, in pixels. * @param h The height of the border, in pixels. */ private function createBorder (w:Number, h:Number):Void { // Only create the border if a valid width and height are specified. if (!(w > 0 && h > 0)) { return; } // In the container, create a clip to hold the border around the image. container_mc.createEmptyMovieClip("border_mc", borderDepth); // Draw a rectangular outline in the border clip, with the // specified dimensions and color. container_mc.border_mc.lineStyle(borderThickness, borderColor); container_mc.border_mc.moveTo(0, 0); container_mc.border_mc.lineTo(w, 0); container_mc.border_mc.lineTo(w, h); container_mc.border_mc.lineTo(0, h); container_mc.border_mc.lineTo(0, 0); } /** * Loads a .jpeg file into the image viewer. * * @param URL The local or remote address of the image to load. */ public function loadImage (URL:String):Void { imageLoader.loadClip(URL, container_mc.image_mc); // Create a load-status text field to show the user load progress. container_mc.createTextField("loadStatus_txt", statusDepth, 0, 0, 0, 0); container_mc.loadStatus_txt.background = true; container_mc.loadStatus_txt.border = true; container_mc.loadStatus_txt.setNewTextFormat(new TextFormat( "Arial, Helvetica, _sans", 10, borderColor, false, false, false, null, null, "right")); container_mc.loadStatus_txt.autoSize = "left"; // Position the load-status text field container_mc.loadStatus_txt._y = 3; container_mc.loadStatus_txt._x = 3; // Indicate that the image is loading. container_mc.loadStatus_txt.text = "LOADING"; } /** * MovieClipLoader handler. Triggered by imageLoader when data arrives. * * @param target A reference to the movie clip for which * progress is being reported. * @param bytesLoaded The number of bytes of target * that have loaded so far. * @param bytesTotal The total size of target, in bytes. */ public function onLoadProgress (target:MovieClip, bytesLoaded:Number, bytesTotal:Number):Void { container_mc.loadStatus_txt.text = "LOADING: " + Math.floor(bytesLoaded / 1024) + "/" + Math.floor(bytesTotal / 1024) + " KB"; } /** * MovieClipLoader handler. Triggered by imageLoader when loading is done. * * @param target A reference to the movie clip for which * loading has finished. */ public function onLoadInit (target:MovieClip):Void { // Remove the loading message. container_mc.loadStatus_txt.removeTextField(); // Apply the mask to the loaded image. container_mc.image_mc.setMask(container_mc.mask_mc); } /** * MovieClipLoader handler. Triggered by imageLoader when loading fails. * * * @param target A reference to the movie clip for which * loading failed. * @param errorCode A string stating the cause of the load failure. */ public function onLoadError (target:MovieClip, errorCode:String):Void { if (errorCode == "URLNotFound") { container_mc.loadStatus_txt.text = "ERROR: File not found."; } else if (errorCode == "LoadNeverCompleted") { container_mc.loadStatus_txt.text = "ERROR: Load failed."; } else { // Catch-all to handle possible future errorCodes. container_mc.loadStatus_txt.text = "Load error: " + errorCode; } } /** * Must be called before the ImageViewer instance is deleted. * Gives the instance a chance to destroy any resources it has created. */ public function destroy ():Void { // Cancel load event notifications. imageLoader.removeListener(this); // Remove movie clips from Stage. container_mc.removeMovieClip(); } }

    using ImageViewer, Version 3

    nothing has changed in our public API!

    to use ImageViewer, Version 3, just recompile (re-export the movie)

    demonstrates goal of OOP: allow changes to one module without affecting any code that uses that module