ActionScript.org Flash, Flex and ActionScript Resources - http://www.actionscript.org/resources
A WordBubble Class
http://www.actionscript.org/resources/articles/1006/1/A-WordBubble-Class/Page1.html
Chris Bristol
I have been using Flash in my web development since version 4. As ActionScript became a more powerful language, I found myself fascinated by the interaction between it and PHP and MySQL. In recent years, I have worked at eliminating the use of the timeline altogether, building everything in pure AS3, and making my code reusable. The fantastic resources of the vast online Flash community has been a great source of help and inspiration. I hope to be able to give back a little of that. You can reach me via my website: Xty Digital Design
 
By Chris Bristol
Published on May 5, 2010
 
This is a reusable class that you can drop into any project to pop up a word bubble containing some explanatory text, a warning, a reminder, whatever. It works like a tool tip for Flash.

Getting Set Up
The WordBubble class is a fairly simple class that pops up a WordBubble that you can use for a variety of things such as a tool tip. I don't know what you properly call classes like this, but Jody Hall calls them Reusable Classes and that works for me. I tend to think of them as code components, but, unlike components, they are much more accessible. Basically, anytime I find myself copying and pasting the same code from one project to another, I think, aha!, this would make a good class. And then, in the future all I have to do is set up a line or two of code in my present project instead of copying and pasting everything over and over. Once you get a few of these classes made, you are well on your way to having your own class library, and should think about setting it up properly. The advantage to setting up your own class library is that when you make additions or changes to a particular class you only have to do it once and every project you use that class in gets the benefit the next time you recompile, or republish, it. If you add additional parameters to the constructor function you will have to change the code in the projects that are affected, but this is far simpler than updating functions and adding in new variables to each project individually. If you don't have your own class library, I urge you to take a few minutes and set one up. It makes life a lot easier.

A Word About Class Packages and Class Libraries

If you are new to Classes, you may be a bit mystified by the Class Package structure. At the top of each Class you will see something like this:
[as]package ca.xty.myUtils [/as]
And in the code where you import classes into your project you will have something like this:
[as]import ca.xty.myUtils.WordBubble;[/as]
What this is telling you is that the WordBubble.as Class lives in a folder called myUtils. The myUtils folder lives in another folder called xty. The xty folder lives in another folder called ca. This is a conventional way to organize your classes. What you call your folders is up to you, but the objective here is to have a unique class path. People often use their domains since that pretty well guarantees you a unique path. To take full advantage of your library, the Flash IDE can be set up to automatically include your library. First, you need to make another folder to hold all these other folders. I call my folder flashClasses. Where you put the flashClasses folder on your computer doesn't really matter, but mine lives in the main folder where I keep all my flash projects. So, set up the flashClasses folder and then open up Flash. In CS4, go to Edit-> Preferences-> ActionScript. Down near the bottom, click on ActionScript 3.0 Settings. Near the top, you'll see a space labeled Source Path. Click the + sign to add a new path. Now click the folder icon to browse to the flashClasses folder you just made and click OK. When you set up the folder structure for your class library, put it all inside the flashClasses folder. Now Flash will include the flashClasses folder, and make all of it's contents, available to any of your movies with a simple import statement. So, when you use - import ca.xty.myUtils.WordBubble;, the path to the ca folder is assumed. This procedure makes it easy to organize your classes, and find them when you need them.

The alternative to the above procedure is to simply drop the ca folder into the same folder as your .fla, but if you are going to be serious about using Classes, it is worth the initial effort to get things set up properly. Enough housekeeping, let's get going.

The WordBubble
First let's have a look at what this class does. The gray background is there to show you the size of the movie in order to better demonstrate how the WordBubble automatically stays aligned with the stage.


Now, download the wordBubble.zip file and let's dig into the code and see how this works.

The Demo Files

Open up the wordBubble.zip and you will see a bunch of files and folders. If you set up a class library with the main folder being called flashClasses, you can drop the ca folder and all it's sub folders into the flashClasses folder and not have to change a thing. The caurina folder, which holds all the files and folders related to the Tweener classes can also be placed in the flashClasses folder and used as is.
The wordBubbleTest.fla is the fla that runs the demonstration we want to look at first. Looking through the fla, you will see that there is nothing on the stage, but you will find some items in the library and you'll see that under the Properties heading, the Document Class is set to WBDemo. The WBDemo.as class is commented to give you an idea of how this is set up. If you take a look at it, you see that we have set up a variable called wb as an instance of our WordBubble class. In this example we will be using the ROLL_OVER and ROLL_OUT event listeners to call the WordBubble. Have a look in the overHandler function and you'll see how little it takes to set this class in motion.

[as]
wb = new WordBubble(150, 25, 0x000000, 0xff0000, 0x980101, "This is Button 1 and it uses the timer feature.", 2);
setWBXY();
[/as]

We use our variable wb to create a new instance of WordBubble and pass the class several parameters.
1) width of the WordBubble
2) height of the WordBubble
3) text color
4 & 5) the colors of the background gradient
6) the text we want to display
7) an optional parameter that sets up a timer function to automatically make the WordBubble disappear, after 2 seconds in this case.

The setWBXY() function is responsible for handling the positioning of the WordBubble on the stage.
[as]
private function setWBXY():void{
    wb.x = mouseX - (wb.width/2);
    if(wb.x < 5){
        wb.x = 5;
    }
    if((wb.x + wb.width) > stage.stageWidth){
        wb.x = (stage.stageWidth - wb.width) - 5;
    }
    wb.y = mouseY - (wb.height + 5);
    if(wb.y < 0){
        wb.y = mouseY + 5;
    }
    addChild(wb);
}
[/as]

We use the mouseX and mouseY properties, then subtract half the width of the WordBubble to center it in relation to the mouse pointer. Next we make sure that it remains on stage by correcting it's position if necessary. Once everything is set up properly, the WordBubble is added to the display list. Now let's take a look at the WordBubble.as class and see how it operates.



WordBubble.as Class - From The Top
Open up the WordBubble.as file. At the very top is a bunch of commented out stuff. These are the functions you would need to add to your Document Class if you were using this in a ROLL_OVER / ROLL_OUT capacity. There are just there to help you, and me, remember how to use this class 6 months from now.
The first line of code is the package declaration:

[as]package ca.xty.myUtils {[/as]

If you have your own class library set up, and you are using your own folder structure, change this to reflect the new location of the WordBubble.as file. Remember to also change the import statement in the WBDemo.as file. If you simply dropped the ca folder and it's contents into your main flashClasses folder, you don't need to change a thing. Now we import all the classes we require to make this class work. It uses the Caurina Tweener classes, which I have included in the zip file. You can substitute your favorite Tween engine if you want to.

Next up, we define the variables we'll be using in this class. My convention is to use an underscore to mark the variables we get from the passed parameters.

[as]
private var _wbWidth:int;
private var _wbHeight:int;
private var _txtColor:Number;
private var _bgColor1:Number;
private var _bgColor2:Number;
private var _wbText:String;
private var _wbInterval:int;
   
private var wbTF:TextField;

private var wbFormat:TextFormat;
   
private var xPos:int;
private var yPos:int;
       
private var popUpPanel:Sprite;

private var timer:Timer;
[/as]

In the constructor function, we pick up the passed parameters and assign them to our class variables. Once we have the _wbInterval, we check to see whether it is equal to zero, and, if it's not, then we multiply it by 1000 because Flash Timers use milliseconds.
[as]
public function WordBubble(wdth:int, hght:int, txtColor:Number, bgColor1:Number, bgColor2:Number, wbText:String, wbInt:int = 0):void{
    _wbWidth = wdth;
    _wbHeight = hght;
    _txtColor = txtColor;
    _bgColor1 = bgColor1;
    _bgColor2 = bgColor2;
    _wbText = wbText;
    _wbInterval = wbInt;
    if(_wbInterval != 0){
        _wbInterval *= 1000;
    }
[/as]

Next we set up our text format for the text we will be showing. Notice that we use the _txtColor variable to set the font color.

[as]
wbFormat = new TextFormat();
wbFormat.font = "Verdana";
wbFormat.size = 11;
wbFormat.color = _txtColor;
wbFormat.align = "center";
[/as]

Now we create a new Sprite called popUpPanel. This will be the container that holds all of the elements that make up the WordBubble. This makes life simple for a few reasons:
1) When we want to get rid of the WordBubble all we have to do is removeChild(popUpPanel)
2) It makes it easy to line things up using the popUpPanel as the reference point. We start at the co-ordinates 0,0 and build everything from there. We are not worried about how this will look on stage since it is the instance of the WordBubble, wb, that we will be positioning on the stage.
3) We now have a single object to tween.
And, because we are going to tween the WordBubble, we set the initial value of alpha to 0.

[as]
popUpPanel = new Sprite();
addChild(popUpPanel);
popUpPanel.alpha = 0;
[/as]

Now we initialize our first Shape, add it to the popUpPanel and set it's x and y properties to 0,0. Remember, these co-ordinates are those of the popUpPanel, and have nothing to do with the main stage.
[as]
var bg1:Shape = new Shape();
bg1.x = 0;
bg1.y = 0;
popUpPanel.addChild(bg1);
[/as]

Variable bg1 is going to be a gradient filled rounded rectangle. The _bgColor1 and _bgColor2 variables are put to use here, as are the _wbWidth and _wbHeight.
[as]
var fillType:String = GradientType.LINEAR;
var colors:Array = [_bgColor1, _bgColor2];
var alphas:Array = [1, 1];
var ratios:Array = [0, 255];
var matrix:Matrix = new Matrix();
var gradWidth:Number = _wbWidth;
var gradHeight:Number = _wbHeight;
var gradRotation:Number = 90 / 180 * Math.PI;
var gradOffsetX:Number = 0;
var gradOffsetY:Number = 0;

matrix.createGradientBox(gradWidth, gradHeight, gradRotation, gradOffsetX, gradOffsetY);
var spreadMethod:String = SpreadMethod.PAD;
bg1.graphics.beginGradientFill(fillType, colors, alphas, ratios, matrix, spreadMethod);
bg1.graphics.drawRoundRect(0, 0, _wbWidth, _wbHeight, 12);
bg1.graphics.endFill();
[/as]

Now we draw a second rounded rectangle on top of the first one. The second one, bgW, is 6 pixels smaller in both the height and the width. It is also positioned at the co-ordinates 3, 3. This gives us the illusion of a 3 pixel border around the second rounded rectangle bgW. Here the colors for the gradient are fixed, but you could easily add them as parameters and set them on the fly.

[as]
var bgW:Shape = new Shape();
bgW.x = 3;
bgW.y = 3;
popUpPanel.addChild(bgW);
           
var fillTypeW:String = GradientType.LINEAR;
var colorsW:Array = [0xffffff, 0xeeeeee];
var alphasW:Array = [1, 1];
var ratiosW:Array = [0, 255];
var matrixW:Matrix = new Matrix();
var gradWidthW:Number = gradWidth - 6;
var gradHeightW:Number = gradHeight - 6;;
var gradRotationW:Number = 90 / 180 * Math.PI;
var gradOffsetXW:Number = 0;
var gradOffsetYW:Number = 0;

matrixW.createGradientBox(gradWidthW, gradHeightW, gradRotationW, gradOffsetXW, gradOffsetYW);
var spreadMethodW:String = SpreadMethod.PAD;
bgW.graphics.beginGradientFill(fillTypeW, colorsW, alphasW, ratiosW, matrixW, spreadMethodW);
bgW.graphics.drawRoundRect(0, 0, gradWidthW, gradHeightW, 12);
bgW.graphics.endFill();
[/as]

Now that our background is all set up, let's add our text field. To give us a bit of padding, we set the x and y co-ordinates to be 3 less than the second rounded rectangle's (bgW) x and y. Yes, we could have used the leftMargin and rightMargin properties in the text format, but there is no top or bottom margins available, so this way is more consistent for us. Then, when we declare the width and height we make it 6 pixels less than the width and height of bgW. Because we can't be certain how much text will be passed, we set the text field's autoSize property to LEFT and set multiline and wordWrap to true. This combination will force the text field to expand down if it requires more room.

[as]
wbTF = new TextField();
wbTF.x = bgW.x + 3;
wbTF.y = bgW.y + 6;
wbTF.width = bgW.width - 6;
wbTF.height = bgW.height - 6;
wbTF.autoSize = TextFieldAutoSize.LEFT;
wbTF.multiline = true;
wbTF.wordWrap = true;
wbTF.htmlText = _wbText;
wbTF.setTextFormat(wbFormat);
popUpPanel.addChild(wbTF);
[/as]

Once the text field wbTF is in place, and our text has been added, we check it's height against the height of the first rounded rectangle, bg1. If the height of bg1 minus the height of wbTF is less than our top and bottom margins combined - a total of 12 - then we need to make both of the background areas bigger.

[as]
if(bg1.height - wbTF.height < 12){
    bg1.height = wbTF.height + 12;
    bgW.height = bg1.height - 6;
    wbTF.y = bg1.y + 6;
}
[/as]

Before we bring the WordBubble to life, we need to check once more and see what the value of _wbInterval is. If it is not equal to 0, then we want to use the timer feature, and so we use the first Tween statement, which includes an onComplete function - startTimer - which, of course, starts our timer. If _wbInterval is in fact equal to 0, then we use the second Tween statement and pop the WordBubble into life.

[as]
if(_wbInterval != 0){
    Tweener.addTween(popUpPanel, {alpha:1, time:1, delay:.1, transition:"easeOutExpo", onComplete:startTimer});
}else{
    Tweener.addTween(popUpPanel, {alpha:1, time:1, delay:.1, transition:"easeOutExpo"});
}
[/as]

The function, startTimer(), starts our timer using the interval we set via the constructor and calls the next function, onTimerComplete, where we fade out the WordBubble, remove the event listener for the timer and then head to our last function, removeWB, which - you may have guessed - removes the WordBubble by getting rid of the popUpPanel.

[as]
private function startTimer():void{
      timer = new Timer(_wbInterval, 1);
      timer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimerComplete);
      timer.start();
}

private function onTimerComplete(event:TimerEvent):void{
    Tweener.addTween(popUpPanel, {alpha:0, time:1, transition:"easeOutExpo", onComplete:removeWB});
    timer.removeEventListener(TimerEvent.TIMER_COMPLETE, onTimerComplete);
}

private function removeWB():void{
    removeChild(popUpPanel);
}
[/as]

And that's all of it. Now let's recap what you need to do to make this work with any project, and then take a look at how you can use this class on the timeline.

The Recap
Here are the steps we need to do in order to set this up in a situation where we have a Document Class file being used.

1) Declare a variable for the WordBubble
[as]
private var wb:WordBubble;
[/as]

2) Set up event listeners on your buttons or MovieClips.
[as]
but1.addEventListener(MouseEvent.ROLL_OVER, overHandler);
but1.addEventListener(MouseEvent.ROLL_OUT, outHandler);
[/as]

3) Set up the functions overHandler and outHandler. These are set up for use with buttons. If you are using MovieClips be sure and use the overMCHandler.

[as]
private function overHandler(e:MouseEvent):void{
    switch(e.currentTarget.label){
        case "Button 1":
            wb = new WordBubble(150, 25, 0x000000, 0xff0000, 0x980101, "This is Button 1 and it uses the timer feature.", 2);
            setWBXY();
            return;
        case "Button 2":
            wb = new WordBubble(150, 30, 0x000000, 0xff0000, 0x980101, "This is Button 2");
            setWBXY();
            return;
        case "Button 3":
            wb = new WordBubble(100, 25, 0x000000, 0xff0000, 0x980101, "This is Button 3");
            setWBXY();
            return;
    }
}

private function outHandler(e:MouseEvent):void{
    removeChild(wb);
}
[/as]

4) Set up the function setWBXY() to position the WordBubble.
[as]
private function setWBXY():void{
    wb.x = mouseX - (wb.width/2);
    if(wb.x < 5){
        wb.x = 5;
    }
    if((wb.x + wb.width) > stage.stageWidth){
        wb.x = (stage.stageWidth - wb.width) - 5;
    }
    wb.y = mouseY - (wb.height + 5);
    if(wb.y < 0){
        wb.y = mouseY + 5;
    }
    addChild(wb);
}
[/as]

And you are good to go!

Can I Add This Code to the Timeline?

Sure you can. The one big difference is the designation "private". Private can only be used inside of Class files, so just eliminate all of the private designations from the code that you place on the timeline and put in things like var wb:WordBubble; instead of private var wb:WordBubble; and function setWBXY instead of private function setWBXY. You will still need to import your WordBubble class unless you include only the WordBubble.as file in your project folder. If you do this - and you know you should be setting up your class library - make sure that you change the package details at the top of the file to read just package{ instead of package ca.xty.myUtils{. I have included a movie called wordBubbleFLATest in the zip file to demonstrate this.

Customize Your WordBubble

The parameters you set give you quite a lot of variety and flexibility, but you don't have to stop there. Play with the code that makes up the rounded rectangles. You can alter their shape or add more colors to the gradient. In the drawRoundRect constructor - drawRoundRect(0, 0, _wbWidth, _wbHeight, 12); - the last parameter - 12 - represents the curve of the rounded rectangle. Play with this number to change the shape of the WordBubble. To mess around with the gradient fill, see the tutorial- AS3 Dynamic Gradient Fills by Ryan Walker - for an excellent explanation on the workings of the gradientFill.

Another way to add variety is through your choice of event listeners. Here we have used the ROLL_OVER/ROLL_OUT MouseEvents, but you don't have to. For instance, I recently used the WordBubble in a project that required a prompt if the user does not perform a certain action to get started. I set up a Timer to wait 10 seconds and if a particular ComboBox was not clicked, then a WordBubble instance appears and prompts the user into making a choice from that ComboBox. Once the choice is made, the WordBubble disappears. If the choice is made before the 10 seconds has elapsed, the timer is stopped and the WordBubble never appears.

And, of course, you can add additional fields, such as a title field. The WordBubble as it is, fills a simple need , but it can be an excellent start for creating a more complex class, and that will be the subject of my next tutorial. Called the InfoBox, we will be building a class that will let us include things like an image and hyperlinked text.

Worth Noting

Something I noticed from the feedback on my first tutorial dealing with AS Classes was a common mistake a few people made, as did I, when dealing with Classes for the first time. The AS files do not get uploaded to your server. Unlike an XML file, or a Text File, Flash does not read information from the AS files on your server. AS files are complied into the final swf when you publish your flash movie. So, when you make changes to any of the AS files, save the file, then re-publish your movie to make the changes take effect.

As always, if you have any questions, don't be shy. Contact information can be found in my profile.