ActionScript.org Flash, Flex and ActionScript Resources - http://www.actionscript.org/resources
Another Useful, Reusable AS3 Class, "Expander"
http://www.actionscript.org/resources/articles/708/1/Another-Useful-Reusable-AS3-Class-Expander/Page1.html
Jody Hall
My interest in Flash started mostly because of a Jib-Jab cartoon ("This Land") in 2004. I'm the author of a feature I call "Mazoons," which are a combination of mazes and cartoons. In 2002, I even had a book published, "Super Silly Mazes." I'm not a professional programmer, but making my mazes interactive by programming them with Flash became a hobby/obsession of mine, to the point where I have now learned more than I bargained for. Lately I'm working on a new website about Flash and Actionscript 3.0 called The Flash Connection
By Jody Hall
Published on December 2, 2007
 
Difficulty: Easy

If you went through my earlier tutorial, you made two very useful, reusable classes. Not only that, but you learned a lot about the way classes work, how to store them in your own packages, how to import them, and how to make objects from them. Along the way, you also learned how to drag Movie Clips using a class, and also how to move them using the keyboard.

In this tutorial, you'll create another cool class that makes an ordinary MovieClip instance into an expanding help box. This could easily be incorporated into one of your future Flash programs, where you might want to give your users a help window that expands, yet stays inconspicuous and out of the way when collapsed. You'll also learn to use the Tween class, and I'll explain how to use getter and setter methods so that users of your class (or you!) can customize the way the box animates without editing the class file itself.

Introduction: Expander in action
Hi, welcome to another exciting edition of "useful, reusuable classes!"

I thought that this time I'd give a little preview of our featured class in action! So, here is what we're about to make in this tutorial. Haha, okay. I admit it: the "lorem ipsum" text in the expanded MovieClip's message isn't all that exciting. But the effect is cool, and  you'll just have to imagine your own text and/or images in place of the lorem ipsum stuff, because it's just an ordinary Movie Clip, and you can put into it whatever you want:


Crafting a MovieClip for "Expander"
This time we'll start with the fla file first, and craft a suitable expanding help box MovieClip. Open a new Flash file (Actionscript 3.0). Then, go to the Insert menu, and choose "New Symbol." Name it "helpbox," choose MovieClip type, and click OK.

Now, you're in the edit mode of a new MovieClip symbol. In the clip's timeline, create two more layers. Now you should have three layers. Name them (from the top down), "actions," "text," and "bg." Next, click the second frame, top layer. Hold the mouse down and drag till you highlight the second frame on all three layers, then let go. With the second frame of all three layers selected, press F6 to add keyframes to all of them at once. Your timeline should now look like this:


Whenever I design a MovieClip, I usually start from the back and work my way to the front, locking the layers as I go. That way nothing gets placed where I don't want it. So starting with the bg (background) layer, click the first frame there. Choose the Rectangle tool, choose whatever colors you want for stroke and fill (the stroke is optional, but do choose a fill, as our mouse events are going to require it). Draw a small rectangle (bear in mind that this is the collapsed box we're drawing here). Select it all and align it to the upper left corner. I chose a black stroke with a thickness of 5, rounded corners set at 6, and a white fill, and drew this:

Click the second frame of the bg layer, and draw a larger rectangle. You can use whatever colors you want; I chose to keep the same look. This one is going to be the expanded box, of course. You should make it at least five times larger, although there's no exact size or proportions you need to use. Make it whatever size you think you'll need to accomodate a paragraph or two of text. Again, make sure it's aligned with the top left corner. Here's what I drew, using the same stroke and fill settings:

You should have both frames of the bg layer completed now, so go ahead and lock it. Click on the first frame of the text layer. Select the Text tool, and in the properties panel, choose static text. Click the stage to place a new static textbox. In the properties panel, choose a font face, size, color, and boldness. I chose the "Cooper Black" font, black color, with bold, and found that a size of 16 was about right. In the text box, I typed the word "Help," then chose the arrow (selection) tool to center it to my liking. This was the result:

Next, click on the second frame of the text layer. Click the Text tool again. Again, choose static text, and create a larger static text box with a couple of paragraphs of information, whatever you want. I chose to use a bit of "lorem ipsum" text as just kind of a placeholder. It'll do fine for the purposes of this tutorial, I think. I chose the same font face, color, size, etc. So my expanded box on the second frame now  looks like this:

And that's it for the text layer, so go ahead and lock it. Next, click the first frame of the actions layer. Press F9 to get the actions panel. Put a stop() command on line 1 of the actions panel for this frame. Click the second frame of the actions layer and do the same thing, so that there's a stop() command on each frame of the actions layer. Lock the actions layer. The MovieClip design is complete. Go ahead and click the Scene 1 button to return to the main timeline.

Drag an instance of the MovieClip you just created to the stage. With the instance selected, go to the properties panel and type "help" as its instance name. The instance is ready for the Expander class to control it.

Writing the Expander Class
With your fla file still open, go to File, New. Open a new Actionscript File. Type the following into the script pane:
[as]package com.mysite.effects {
    import flash.display.MovieClip;
   
    public class Expander {
        private var _clip:MovieClip;
       
        public function Expander(clip:MovieClip) {
            _clip = clip;
        }
    }
}[/as]
Choose file, Save As. In the "Save As" dialog box, navigate to the com/mysite folder, create a new folder called "effects," enter that new folder, and save the file there as "Expander.as." Hopefully you did the last tutorial and you already have the "com" and "mysite" folders arranged underneath a classpath folder. If not, check out my first tutorial, and make sure you understand the classpath and how to make packages, and then come back. This is not to imply that it must be done this way or it won't work, of course. I just want to make sure you understand that you can store your classes in your own packages and then import them at will. Once you have the hang of it, you can then name your folders whatever you want, and organize it your way. In fact, I recommend it!

The basic structure above is the same thing we started with last time. To review, we know that we must import the MovieClip class, as we'll be passing in our "help" MovieClip as a parameter. The constructor of the class sets a private variable, "_clip" to be equal to the clip that we pass in. That way, our passed-in MovieClip becomes a property of our class.  This is all the same stuff we did before.

What we're going to do, naturally, is cause the clip to expand when it's clicked on with the mouse, and then collapse when it's clicked again. We purposely crafted the MovieClip with two different frames so that we could switch frames just before we expand it. This way, the collapsed clip can bear a different message than the expanded one, and all we have to do to change frames is use the "gotoAndStop()" method on our "_clip" property, which now refers to the same MovieClip instance that's on the stage.

We know that we're going to need to use mouse events, and I've also told you that we're going to use the Tween class to animate the help box. So, we'll import the Tween class, the TweenEvent class, and all the easing classes. Add these lines to the import statement block:
[as]import flash.events.MouseEvent;
import fl.transitions.Tween;
import fl.transitions.easing.*;
import fl.transitions.TweenEvent;
[/as]
There are several things to note here. First is that many of these classes and packages are in "fl" instead of "flash." All I can say is that if you don't know the location of a class, consult the help files (the "code hints" popup also helps quite a bit). Also new here is the use of the (*) to import an entire package at once. When you see an asterisk in an import statement, just think "all classes." This one statement will import all the easing classes in that package. You need not worry that they won't all get used, either; the compiler is smart enough to just include the ones you actually use. But importing them all means that we can change easing methods and recompile without changing the import statement every time. Finally, there's the TweenEvent class, and I just wanted to note here that it isn't located in the flash.events package, as you might think.

With all those classes imported, we can go on and write the class. First, we'll create a bunch of variables of the Number datatype. We're going to Tween the height, width, and alpha properties of our MovieClip, so the six variables will represent those values (add all these to the variables list, right after the "_clip" one):
[as]private var _startWidth:Number;
private var _endWidth:Number;
private var _startHeight:Number;
private var _endHeight:Number;
private var _startAlpha:Number = 0.2;
private var _endAlpha:Number = 1;[/as]
The reason _startAlpha and _endAlpha are given values right in the varible list is so that we can start with some kind of default values for those. The others will be given default values in the constructor, which we'll turn to next. Change the constructor so that it reads like this:
[as]public function Expander(clip:MovieClip) {
    _clip = clip;
    _startWidth = _clip.width;
    _startHeight = _clip.height;
    _clip.gotoAndStop(2);
    _endWidth = _clip.width;
    _endHeight = _clip.height;
    _clip.gotoAndStop(1);
    _clip.buttonMode = true;
    _clip.addEventListener(MouseEvent.CLICK, expandMe);
    initClip();
}[/as]
This utilizes a cool little trick. First, the _startWidth and _startHeight properties are set equal to the width and height of the _clip itself. This is the width and height of the clip as it sits stopped on frame 1. Then, we tell the _clip to gotoAndStop(2) so that we can read the width and height again. These values will be different, because when the _clip is stopped on frame 2, it's wider and taller than when it's on frame 1. This way, we can capture a default _endWidth and _endHeight to use. Then we put the _clip back to frame 1 again. We set buttonMode to true to get a hand cursor, and add an event listener to respond to a mouse click. At this point, we have yet to write the expandMe function, and we'll get to that. But first, let's write the initClip function referred to in the last line. Here is is:
[as]private function initClip() {
    _clip.width = _startWidth;
    _clip.height = _startHeight;
    _clip.alpha = _startAlpha;
}[/as]
All this does is further initializes our MovieClip. This might not make sense to you at first. We just finished setting our variables equal to the _clip's width and height, and now we turn around and do the reverse! The only thing that seems to really get done here is setting the _clip's alpha. However, there's a good reason for separating these lines from the constructor. Later, when we make our setter functions, we can call initClip again to re-intialize the _clip so that it resizes.

Before we move on to write the expandMe and contractMe functions, let's add five more variables to the variables list:
[as]private var _easing:Function = Bounce.easeOut;
private var _duration:Number = 2;
private var twWidth:Tween;
private var twHeight:Tween;
private var twAlpha:Tween;[/as]
This gives us one convenient place where we can change the _easing method and the _duration of the animation. Changing it here is easier than changing it in all the six other places we're going to be using it!

Finally, there's the three Tween variables, named after the properties that they'll be operating on. I didn't deem it necessary to precede these names with an underscore, as we won't be manipulating them with getters and setters, which is where I think underscores come in handy (to distinguish the names, as you'll see). Also, if you work with the Tween class, you soon find out the hard way that these variables need to be declared outside the function that uses them, otherwise they can get "garbage collected" and your animations won't behave right. This is an issue new to Actionscript 3.0. Take my word on this one.

Expander Class Continued: expandMe and contractMe functions
Now let's write the expandMe function:
[as]private function expandMe(event:MouseEvent):void {
    _clip.removeEventListener(MouseEvent.CLICK, expandMe);
    _clip.gotoAndStop(2);
    twWidth = new Tween(_clip, "width", _easing, _startWidth, _endWidth, _duration, true);
    twHeight = new Tween(_clip, "height", _easing, _startHeight, _endHeight, _duration, true);
    twAlpha = new Tween(_clip, "alpha", _easing, _startAlpha, _endAlpha, _duration, true);
    twAlpha.addEventListener(TweenEvent.MOTION_FINISH, finishHandler);
    function finishHandler(event:TweenEvent) {
        twAlpha.removeEventListener(TweenEvent.MOTION_FINISH, finishHandler);
        _clip.addEventListener(MouseEvent.CLICK, contractMe);
    }
}[/as]
The first thing expandMe does is removes the event listener that called it. In most cases, when you remove an event listener, you can just copy and paste the same line where you added it, then just change the word "add" to "remove." We remove it because we don't want to allow a second click until after our animation completes. And the next click we get will be to collapse the box instead. The next thing this function does is switches frames on the _clip, so that we're animating the box that's on the second frame instead.

The Tween class takes seven parameters:
The name of the object to Tween
The property of the object to Tween (must be in quotes, as it's a String)
The easing method to use
The starting value
The ending value
The duration (can be expressed either in frames or seconds)
Whether or not to use seconds to express duration (true or false)

As you can see in the function, four of these values are represented by variables, which, like I said before, makes it much easier to change them from one spot, but will become absolutely necessary later, when we write the setter functions. If you've worked with Tweens before, you know that as soon as you say new Tween(), and give it all those parameters, it goes! Unless you supply code to stop it, it just starts going immediately.

Those three tweens will run simultaneously, then. One tween animates the width, one animates the height, and the last one animates the alpha. They all use the same easing method, and they all have the same duration. We add an event listener to let us know when the animation is done, so that we can do some other stuff. This uses the TweenEvent class, and the event we want to monitor is the "MOTION_FINISH" event. There are three tweens going, but it's only necessary to add this event to one of them, and I just chose the last one... well, just because, that's all! Purely arbitrary. Notice that the handler function for the motion finish event is nested right here inside the outer function (expandMe). Nothing wrong with doing it this way. The thing to note here is that it's not necessary, nor is it allowed, to use an access modifier like "private" in front of it, because this function doesn't belong to the class when we do it this way.

When the motion is done, the _clip is fully expanded, and we remove the event listener for motion finish, then add an event listener to the _clip to listen for another mouse click. This one tells it to run the function "contractMe" when it's clicked (refer back to the last line in the above code box, not the following one).

Now, let's write "contractMe," which is going to look quite a bit like "expandMe" :
[as]function contractMe(event:MouseEvent):void {
    _clip.removeEventListener(MouseEvent.CLICK, contractMe);
    twWidth = new Tween(_clip, "width", _easing, _endWidth, _startWidth, _duration, true);
    twHeight = new Tween(_clip, "height", _easing, _endHeight, _startHeight, _duration, true);
    twAlpha = new Tween(_clip, "alpha", _easing, _endAlpha, _startAlpha, _duration, true);
    twAlpha.addEventListener(TweenEvent.MOTION_FINISH, finishHandler);
    function finishHandler(event:TweenEvent) {
        twAlpha.removeEventListener(TweenEvent.MOTION_FINISH, finishHandler);
        _clip.gotoAndStop(1);
        initClip();
        _clip.addEventListener(MouseEvent.CLICK, expandMe);
    }
}[/as]
This function first removes the listener that called it. Then, the biggest difference here is that the starting and ending values given to the tweens are reversed in the parameter list, so that they all go from the larger value to the smaller. Also, when the motion finishes, the clip will be fully contracted, and we want the _clip to revert back to frame 1 again. I also found (when testing this) that it's necessary to call initClip again, to set the width and height of the _clip back to what it was when it started. Otherwise, the graphic on the first frame winds up extremely shrunken. Finally, the listener is added that will expand the _clip again.

So, you can see that expandMe and contractMe take turns being the handler for the _clip. Since the listener isn't reset until the motion completes, it's not possible to register a click on the _clip while it's in motion. I did that on purpose! However, should you decide that your help box should register clicks immediately, even if it's in mid-animation, that's easy to change, and I'm sure you won't have any problem figuring out how.

So now our class is complete except for the setter functions we'll write (I know, I keep talking about those!). At this point, it should be fully functional:
[as]package com.mysite.effects {
    import flash.display.MovieClip;
    import flash.events.MouseEvent;
    import fl.transitions.Tween;
    import fl.transitions.TweenEvent;
    import fl.transitions.easing.*;
   
    public class Expander {
        private var _clip:MovieClip;
        private var _startWidth:Number;
        private var _endWidth:Number;
        private var _startHeight:Number;
        private var _endHeight:Number;
        private var _startAlpha:Number = 0.2;
        private var _endAlpha:Number = 1;
        private var _easing:Function = Bounce.easeOut;
        private var _duration:Number = 2;
        private var twWidth:Tween;
        private var twHeight:Tween;
        private var twAlpha:Tween;
       
        public function Expander(clip:MovieClip) {
            _clip = clip;
            _startWidth = _clip.width;
            _startHeight = _clip.height;
            _clip.gotoAndStop(2);
            _endWidth = _clip.width;
            _endHeight = _clip.height;
            _clip.gotoAndStop(1);
            _clip.buttonMode = true;
            _clip.addEventListener(MouseEvent.CLICK, expandMe);
            initClip();
        }
        private function initClip() {
            _clip.width = _startWidth;
            _clip.height = _startHeight;
            _clip.alpha = _startAlpha;
        }
        private function expandMe(event:MouseEvent):void {
            _clip.removeEventListener(MouseEvent.CLICK, expandMe);
            _clip.gotoAndStop(2);
            twWidth = new Tween(_clip, "width", _easing, _startWidth, _endWidth, _duration, true);
            twHeight = new Tween(_clip, "height", _easing, _startHeight, _endHeight, _duration, true);
            twAlpha = new Tween(_clip, "alpha", _easing, _startAlpha, _endAlpha, _duration, true);
            twAlpha.addEventListener(TweenEvent.MOTION_FINISH, finishHandler);
            function finishHandler(event:TweenEvent) {
                twAlpha.removeEventListener(TweenEvent.MOTION_FINISH, finishHandler);
                _clip.addEventListener(MouseEvent.CLICK, contractMe);
            }
        }
        private function contractMe(event:MouseEvent):void {
            _clip.removeEventListener(MouseEvent.CLICK, contractMe);
            twWidth = new Tween(_clip, "width", _easing, _endWidth, _startWidth, _duration, true);
            twHeight = new Tween(_clip, "height", _easing, _endHeight, _startHeight, _duration, true);
            twAlpha = new Tween(_clip, "alpha", _easing, _endAlpha, _startAlpha, _duration, true);
            twAlpha.addEventListener(TweenEvent.MOTION_FINISH, finishHandler);
            function finishHandler(event:TweenEvent) {
                twAlpha.removeEventListener(TweenEvent.MOTION_FINISH, finishHandler);
                _clip.gotoAndStop(1);
                initClip();
                _clip.addEventListener(MouseEvent.CLICK, expandMe);
            }
        }
    }
}[/as]
Now go back to the fla file again, and enter the following on frame 1 of the main timeline:
[as]import com.mysite.effects.Expander;
var expander:Expander = new Expander(help);[/as]
Press Ctrl-Enter to test the movie. It should be running error free, and cause our "help" clip to expand and contract like a champ. The only thing left to do is to add some customization, which we'll do next. I'll also show you an easier way (than last tutorial) to make our _clip come to the front of everything else.

Customizing Expander with "setter" functions
To make it easy for users of our class to change its private properties without actually editing the class file, we employ "setter" functions. These functions are special, and are set apart from other functions through the use of the keyword "set." They're written as public functions, otherwise they wouldn't be of much use. Let's add one to our Expander class. Just insert it in the list of functions, right after the private ones (that is, right after "contractMe"):
[as]public function set startWidth(theValue:Number) {
    _startWidth = theValue;
    initClip();
}[/as]
Save the file. Now go back to the fla file again. Change the code there so that it reads:
[as]import com.mysite.effects.Expander;
var expander:Expander = new Expander(help);
expander.startWidth = 40;[/as]
Test the movie. Your help box will now start and end with a different value, that being the new one you just gave it. Notice that although "set startWidth" is a method (note also there's a space in the name, which is unusual), and it expects a parameter, from the external code we actually are able to treat it as though it were a property. Rather than calling it with its argument in parentheses, like we normally would, with this kind of method we call it using an equals sign instead, as though we were setting a property.

I can almost hear you asking, "Why don't we just use a public property, then?" The answer is that by doing this, we protect the actual property from being changed improperly by external code. By using a separate setter function, we also introduce another layer of protection. This way, we could also potentially filter the value we get using conditionals, so that we can decide what range of values we will or won't accept. Also, in our present case, it allows us to add in a call to the initClip function so that our value gets updated immediately.

And that's how setters work! I'm going to show you a "getter" function as well, just so you'll know how they work, but "Expander" isn't going to be using any. But if it did, it might look like this:
[as]public function get startWidth():Number {
    return _startWidth;
}[/as]
Notice that "setters" always receive an argument as a parameter, and "getters" always have a return value. The same opportunity to run other code and/or filter data exists with getters as well. The same technique of using dot syntax and treating it like a property applies as well.

This simple concept of getters and setters is sometimes difficult for those new to classes to comprehend. I know, because I have been one of them. The reason is that it's one of those things that breaks the ordinary rules. The concept of getters and setters might be thought of as analogous to "castling" in a game of chess: it's one of those things that defy the rules of how the pieces normally move.

In any case, here are the setter functions that you can add to the list of functions in Expander:
[as]public function set startWidth(theValue:Number) {
    _startWidth = theValue;
    initClip();
}
public function set startHeight(theValue:Number) {
    _startHeight = theValue;
    initClip();
}
public function set startAlpha(theValue:Number) {
    _startAlpha = theValue;
    initClip();
}
public function set duration(theValue:Number) {
    _duration = theValue;
}
public function set easing(theValue:Function) {
    _easing = theValue;
}
public function set endWidth(theValue:Number) {
    _endWidth = theValue;
}
public function set endHeight(theValue:Number) {
    _endHeight = theValue;
}
public function set endAlpha(theValue:Number) {
    _endAlpha = theValue;
}[/as]
Now, change the code in the fla file to the following (let's set a bunch of values to test our setter functions):
[as]import com.mysite.effects.Expander;
import fl.transitions.easing.*;
var expander:Expander = new Expander(help);
expander.startWidth = 40;
expander.startHeight = 10;
expander.startAlpha = .1;
expander.duration = 6;
expander.easing = Regular.easeOut;
expander.endWidth = 500;
expander.endHeight = 300;
expander.endAlpha = .8;
[/as]
Note that you have to import the easing classes into the fla file as well. Fla files automatically find the classes in the packages that start with "flash" so that no import is necessary. Not so with the classes resisding in packages under "fl." They must be imported. We had to add that here just so we could send Expander a different easing class to use. Anyway, test the movie. Now the Expander class is fully functional, and customizable from outside the class. Congratulations!

There's just one final loose end: Whenever an instance of our class expands or contracts, it ought to come to the front of Flash's stacking order. That way, if there were more than one instance of Expander on the stage, whichever one you clicked would come to the front. I also told you there was an easier way than the line of code I gave you in the last tutorial. All you really have to do is perform an addChild on the instance (from it's parent). Even though it already is a child, telling the parent to add it as a child again doesn't hurt anything, and has the effect of bringing the clip to the front of anything else on the display list. Add this line to the beginning (make it the first line) of both the expandMe and contractMe functions:
[as]_clip.parent.addChild(_clip);[/as]

And that concludes this tutorial. I hope you've enjoyed it, and learned some valuable things. If you did, I hope you'll let me know. Thanks in advance!

Happy coding!

Jody Hall