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
do the simplest thing that works
next pick the requirements that can most easily be met
"never add functionality early" (extreme programming rule)
creating the public api
next determine how class will be used by other programmers
for example:
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:
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:
now add preloader:
if (_framesloaded == _totalframes) { gotoAndStop("main"); } else { gotoAndPlay("loading"); }
var viewer:ImageViewer = new ImageViewer(this, 1); viewer.loadImage("picture.jpg");
this.createTextField("loadmsg_txt", 0, 200, 200, 0, 0); loadmsg_txt.autoSize = true; loadmsg_txt.text = "Loading...Please wait.";
loadmsg_txt.removeTextField();
cropping the image, adding a border
next two functional requirements are:
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:
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 visualscreateMainContainer()
- creates container_mc
clipcreateImageClip()
- creates image_mc
clipcreateImageClipMask()
- creates mask_mc
clipcreateBorder()
- creates border_mc
clipbenefits of dividing work into methods:
revised constructor
constructor must now receive mask and border information
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 clipstarget_mc
, clip to which container_mc
clip is attachedcontainerDepth
, 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 borderborderColor
, RGB color of the border around the imagerevised 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:
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:
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