Criar um scrollbar no ADOBE FLASH AS3!

http://theflashconnection.com/book/export/html/186

 

Create your own Flash AS3 Scrollbar!

Many times when you are programming in Flash, you may have a project where you need a scrollbar. You may also wish that you could customize it and make it look however you want it to look. You could use the scrollbar that comes in the components set. The UI scrollbar will let you apply it to a textfield to scroll text. Similarly, the ScrollPane component will let you scroll any kind of content just by setting it a MovieClip instance as the “source.” But in both those cases, the look is really difficult to customize, and the default look is just so “clinical,” and in your finished program, anyone could tell that you just took the easy way out and used the components. You probably realize that if you could just create your own scrollbar, you could make it look exactly like you want. But just how hard is it to make your own scrollbar?

As you are going to find out in this article, it’s really not all that difficult. There is some math involved, but even that’s relatively easy. So, I am going to show you how, step by step. And in my usual “down to the bone” descriptive style, I’ll make sure that you do understand all the steps. But if you’re not up to reading the whole article, you can also just grab the finished files from the last page, and put them to use! I’ve tried to make the class as easy to use as humanly possible!

This task is definitely more within your reach than you might think. By seeing not only the formulas involved, but just HOW they are arrived at, you will be able to build your own custom scrollbar class. If there is anything presented here that is not how you would have done it, you can tailor it to your needs!

We’re going to start out with timeline code, then when all the formulas are worked out and we have a timeline prototype working, we’ll take that same code and build it into a cool reusable class! Sound good? The fun begins on the next page!

A new kind of drag and drop

Create a new fla file (Actionscript 3.0). Or just download the file above. The first thing to do is to create a “thumb” for our future scrollbar. I just used the rectangle tool to draw a 25 x 25 pixel square with no stroke outline. Then I selected it and converted it to a MovieClip symbol, chose an upper left registration point, and gave it a library name of Thumb. The layer that the instance is on I named thumb. Lasty, I gave it an instance name of thumb in the properties panel. The library name and the layer name don’t matter in our programming, it is the instance name that does.

What we are going to do is to get the thumb dragging and dropping. But we’re not going to do it with the startDrag() and stopDrag() methods, even though we could. Why not? Well, because there actually exists another way to do drag and drop that’s much smoother and more responsive. The startDrag() and stopDrag() methods are fine for some kinds of drag and drop interaction, like I wrote about in my Flash AS3 drag and droptutorial. But they are dependent on the frame rate of the movie, too. They are fine, and very efficient, for those drag and drop games, but they are a bit too choppy for the finer control that our scrollbar will need.

Besides, this other way of dragging is not that hard, it’s just a few lines of code in strategic places. The thing is, if we use the MOUSE_MOVE event for our dragging, there is an updateAfterEvent() method supplied by the MouseEvent class in that case. The updateAfterEvent() method in AS3 is only available with two kinds of events: MouseEvent.MOUSE_MOVE and TimerEvent.TIMER. But what it does is forces the screen to update immediately after the event fires, even if that’s happening faster than the movie’s framerate. The difference is definitely noticeable.

Here’s how it works: We create two variables which we’ll call xOffset and yOffset. These are both of the Number type. These will be used to get the offset of the mouse pointer in relation to the upper left corner of the thumb whenever the user mouses down on it. The following illustration should make that more clear:

Then, we can use these numbers to position the thumb while it’s following the mouse. The code that makes this happen ideally should have a certain structure I have found to work really well. It goes like this: add an event listener for MOUSE_DOWN to the thumb. Inside of that event handler, add event listeners to the stage for both MOUSE_MOVE and MOUSE_UP. Then, in the MOUSE_UP handler function, remove both of those listeners.

That way we don’t have any danger of event listeners being triggered when we don’t want them to be. The MOUSE_MOVE and MOUSE_UP listeners will only be active while the mouse button is down. The MOUSE_MOVE event fires continually, in fact every time the mouse moves, even if it’s only by a little bit. Inside the MOUSE_MOVE handler is where we make the thumb follow the mouse. Naturally, we can just make the thumb’s x and y location equal to the mouse pointer’s x and y location, but adjust it by subtracting those offset numbers we recorded. Finally, in this handler, we just call the updateAfterEvent() method provided by the MouseEvent class, which makes all the motion very smooth.

Here’s the code for this new kind of drag and drop:

import flash.events.MouseEvent;

var xOffset:Number;
var yOffset:Number;

thumb.buttonMode = true;
thumb.addEventListener(MouseEvent.MOUSE_DOWN, thumb_onMouseDown);
function thumb_onMouseDown(event:MouseEvent):void {
	xOffset = mouseX - thumb.x;
	yOffset = mouseY - thumb.y;
	stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.addEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

function stage_onMouseMove(event:MouseEvent):void {
	thumb.x = mouseX - xOffset;
	thumb.y = mouseY - yOffset;
	event.updateAfterEvent();
}

function stage_onMouseUp(event:MouseEvent):void {
	stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.removeEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

And here’s how the swf behaves:

At this point the dragging is unrestricted. You can drag this “thumb” anywhere you want around the stage. Notice how smooth and responsive it is! On the next page, we’ll see how to restrict it to a certain area.

Restricting our dragging: Vertical only!

Restricting the dragging, and confining it to only the y direction is easy: Just forget what I told you about that xOffset number and get rid of it. Here’s the code with all the lines that referenced the xOffset deleted:

import flash.events.MouseEvent;

var yOffset:Number;

thumb.buttonMode = true;
thumb.addEventListener(MouseEvent.MOUSE_DOWN, thumb_onMouseDown);
function thumb_onMouseDown(event:MouseEvent):void {
	yOffset = mouseY - thumb.y;
	stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.addEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

function stage_onMouseMove(event:MouseEvent):void {
	thumb.y = mouseY - yOffset;
	event.updateAfterEvent();
}

function stage_onMouseUp(event:MouseEvent):void {
	stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.removeEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

And here’s how the swf behaves with those code changes:

Now you can only drag up and down! Next we will proceed to add a “track” movie clip. That happens on the next page. This page was a really short one, wasn’t it? Don’t get too used to it. 

Further restricting dragging: Getting on “track”

Next, we’ll create and add a “track” movie clip. Then, we’ll see how we can further restrict the up and down movement of the thumb, and limit it to this track. So in the fla file, add a new layer to the timeline and name it track. On this layer, draw another rectangle. I made mine narrower than the thumb (12 pixels wide), 250 pixels high, used a different color (green), and placed it at the same y location as the thumb. Then I moved the thumb to a location a short distance to the right of the track, for illustrative purposes.

Then I selected the rectangle and converted it to a MovieClip symbol, and named it Track, and once again chose an upper left registration point. In the properties panel, I gave it an instance name of track. Eventually, of course, the thumb will be in front of the track, so you can go ahead and drag the track’s layer to the bottom of the layer stack (I did).

Now that we have a track and it has that instance name, we can use that name in our programming. We can also use its properties to limit the dragging of the thumb.

We’ll create two new variables, both Numbers, called topLimit and bottomLimit. These will represent the top and bottom y locations that the thumb is going to be restricted to. The thumb’s top limit will be the track’s y location, naturally. The bottom limit will be the track’s y location plus the track’s height minus the thumb’s height. However, the track’s height minus the thumb’s height is an important number also, as it represents the distance (in pixels) that the thumb is allowed to travel. Therefore, we will save this number into a variable of its own and call it thumbRange. After that, we can just say that the thumb’s bottom limit is the track’s y location plus the thumbRange. Here’s a helpful illustration of that:

Illustrations like this are extremely helpful to guide you in writing your code, and I recommend making little doodles like this to plan your work. They make it clear just exactly what is going on. Turning again to the code, we will give values to these variables, then in our MOUSE_MOVE handler function, we can use the topLimit and bottomLimit variables to restrict the movement of the thumb. If the thumb’s y value is less than topLimit, make it equal topLimit. If the thumb’s y value is greater than bottomLimit, then make it equal bottomLimit. Remember, y values start out at 0 at the top of the screen, and increase as you go further down. Anyway, here’s the new code:

import flash.events.MouseEvent;

var yOffset:Number;
var topLimit:Number = track.y;
var thumbRange:Number = track.height - thumb.height;
var bottomLimit:Number = track.y + thumbRange;

thumb.buttonMode = true;
thumb.addEventListener(MouseEvent.MOUSE_DOWN, thumb_onMouseDown);
function thumb_onMouseDown(event:MouseEvent):void {
	yOffset = mouseY - thumb.y;
	stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.addEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

function stage_onMouseMove(event:MouseEvent):void {
	thumb.y = mouseY - yOffset;
	//restrict the movement of the thumb:
	if(thumb.y < topLimit) {
		thumb.y = topLimit;
	}
	if(thumb.y > bottomLimit) {
		thumb.y = bottomLimit;
	}
	event.updateAfterEvent();
}

function stage_onMouseUp(event:MouseEvent):void {
	stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.removeEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

And here’s how the swf now behaves:

You can drag the thumb up and down, but now you are restricted to the track’s limits, just like in the illustration! And hopefully I haven’t lost anyone so far. The thumb and track, and the way they behave so far, is everything we need to make what’s known as a Slider. On the next page, we’ll fix its appearance by placing the thumb over the track where it belongs, and also make it do some cool stuff by extracting a certain very important number from it in our code!

Extracting the “scroll percent”

I don’t know how your math skills are, but if they are a little rusty, you may still remember learning about something called “percentages.” You may have thought it was only useful for statistics and boring stuff. In fact, I’ll bet roughly 63% of you still think so! Well, if you’re in that group, I hope I can help change that notion for you, because our scrollPercent variable is going to be really cool, and useful!

Anyway, I am so-so at math myself. I mean, I really have to struggle sometimes to think things through. I draw little pictures to help me work out certain formulas. It always takes me longer to work things out than I thought it should! But I do know that math can help you do some fantastic stuff with your programming, so I keep an open mind and keep plugging away at it. Fortunately, the math involved here isn’t too deep.

When you take a number, any number, and divide it by another number that’s greater, your result is always a decimal number that’s less than 1, and yet still greater than 0. This decimal number is also a percentage. Why is that important? Well, because it turns out we can take that calculated result and apply it to something else. We’ll do that with our scrollbar. Consider the following illustration:

In the above drawing, I’ve plugged in some real numbers just so you can get a better idea of how this works. So just suppose that the thumb is dragged to the position shown in the drawing, which is a little more than half way down the track. The thumbRange is a number not dependent on where anything is positioned on the screen. Remember, it’s just the track’s height minus the thumb’s height. It’s just a “traveling” distance for the thumb. But the thumb’s y needs to have the track’s y subtracted from it so that it can become a number that we can compare to the thumbRange. By making this adjustment, we don’t limit ourselves to our “parts” always being located at 0, 0 on the screen. We get a formula that works no matter where the track and thumb are located. That is, as long as they start off at the same y location as each other, which I think is reasonable to expect from a scrollbar.

So the above formula should be pretty clear: The scroll percent can be found by dividing the distance the thumb has traveled by the distance that it can travel. The numbers in the drawing above yield a neat 0.6, or 60%. You won’t always get numbers that divide that evenly. But we don’t have to divide any numbers ourselves, anyway, that’s the computer’s job, remember? So all we have to do is figure out the formula, write it into our code, and let the computer find the answer for us.

Let’s do that. Make a new layer, and using the Text tool, create a dynamic textbox. Just choose Arial font, 20 point, black. Make it 150 pixels wide and 28 pixels high. Click the “show border” button, so the textbox will have an outline. Click the Embed button and just embed Numerals and Punctuation. Choose “Anti-alias for readability.” Give the textbox an instance name of myText.

Next, make yet another new layer. Name the layer circle. Use the Oval tool (hold down the Rectangle tool’s button for about a second and it will reveal the Oval tool as one of the other choices). Draw a red circle on the stage. Hold down shift while you draw and you will get a perfect circle. Then, double click the shape you just drew (to select it), and press F8 to convert it to a MovieClip symbol. Name the symbol Circle. In the properties panel, give it an instance name of circle.

Oh, yeah, one other thing: Let’s move the thumb so that it’s on top of the track and in front of it. Make sure its y location is 75. Now it looks like a real scrollbar (well, kind of, anyway), and we’re all set to return to the actions panel!

We want to make a new variable called scrollPercent. It will be of the Number type. We’ll declare the variable in the top level code (outside of functions), and initialize it to 0. The calculation will take place in the MOUSE_MOVE handler function. Here’s the calculation:

scrollPercent = (thumb.y – track.y) / thumbRange;

The first part must be placed in parentheses, because, as you may know, computers always do division and multiplication first, so if we want the subtraction part done first (and we do), we have to enclose it in parentheses. Now that we have this number being calculated, we’ll convert it to a string and output it to the textbox:

myText.text = String(scrollPercent);

Finally, just to show off what else our freshly calculated scrollPercent can do, let’s use the same number to affect the circle instance’s alpha property:

circle.alpha = scrollPercent;

Here’s the complete code with all those new changes in place:

import flash.events.MouseEvent;

var yOffset:Number;
var topLimit:Number = track.y;
var thumbRange:Number = track.height - thumb.height;
var bottomLimit:Number = track.y + thumbRange;
//new variable: scrollPercent!
var scrollPercent:Number = 0;
myText.text = "0";
circle.alpha = 0;

thumb.buttonMode = true;
thumb.addEventListener(MouseEvent.MOUSE_DOWN, thumb_onMouseDown);
function thumb_onMouseDown(event:MouseEvent):void {
	yOffset = mouseY - thumb.y;
	stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.addEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

function stage_onMouseMove(event:MouseEvent):void {
	thumb.y = mouseY - yOffset;
	//restrict the movement of the thumb:
	if(thumb.y < topLimit) {
		thumb.y = topLimit;
	}
	if(thumb.y > bottomLimit) {
		thumb.y = bottomLimit;
	}
	//calculate scrollPercent and make it do stuff:
	scrollPercent = (thumb.y - track.y) / thumbRange;
	myText.text = String(scrollPercent);
	circle.alpha = scrollPercent;

	event.updateAfterEvent();
}

function stage_onMouseUp(event:MouseEvent):void {
	stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.removeEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

And here’s how the swf behaves:

When you drag the thumb along the track, you get a range of numbers in the textbox that’s between 0 (at the top) and 1 (at the bottom) and all sorts of decimal numbers in between. And this just happens to be the kind of number that properties like alpha expect, so our scrollbar is now being used to “fine-tune” adjust the circle’s alpha property! Possibilities should now be occurring to you! Volume slider, anyone?

Next, we’ll start seeing how to make this scrollPercent number scroll some content for us!

Scrolling right along now…

Next, returning to our fla file again, let’s delete the myText dynamic textbox and the circle MovieClip, as they’ve served their purpose in demonstrating the Slider functionality. Now we’ll get beyond that and make our scrollbar scroll some content!

We are now going to introduce two new objects: a content movie clip and a window movie clip. The window movie clip is just a rectangle that’s been converted to a MovieClip symbol. I made one that’s 300 pixels wide and 250 pixels tall. Its creation should require no further explanation, as we’ve been creating track and thumb movie clips in the same manner (select and press F8).

And, giving you explicit instructions on creating the content MovieClip seems kind of pointless. I’d rather just explain that this clip can be composed of whatever you want to put into it: images, text, other movie clips, interactive buttons, etc, etc. Your imagination is your only limit! Make it the same width as the window (or approximately so), and make it taller, of course. Or just download the fla file provided above and use mine.

The main idea is that the content is too tall for the window in which we would like to display it. I’ve created my content MovieClip by grabbing some text from a recent news article about the Rockefeller Center Christmas tree, and also grabbing an image from a different source (Hopefully I got the same tree from the same year, but I only put it together to serve as an example, anyway).

The window clip will act as a mask for the content clip. Whenever a movie clip instance acts as a mask for another movie clip instance, it becomes the “window” or “viewport” through which the masked movie clip shows, and blocking out everything that’s not currently in the window. The masking doesn’t make any difference to our formula, though, it just makes the viewing of it more pleasant. If you run it without the masking (which I recommend doing at least once), you can see what’s really going on behind the scenes.

The window and the content are analogous to the thumb and the track. Earlier, we created a variable to define the thumbRange as the distance that the thumb is allowed to travel. Similarly, we can define another variable that defines how far the content is allowed to travel. This contentRange variable can be found by subtracting the window’s height from the content’s height.

The above drawing shows what I mean. Bear in mind that the window and content portion (the right hand side) has been shrunk down in order to fit the drawing, and so it’s not in proportion to the thumb and track portion (the left hand side). Also, the thumb is shown alongside the track, and the window is shown alongside the content. This is, again, just for illustrative purposes, and of course in the real application they will be positioned where they really belong. Also, the content is shown as it would appear if it were scrolled all the way down to the bottom, but this is just to illustrate the way we find out the contentRange. It does not reflect the position of the thumb in the drawing on the left.

The next drawing, though, does correlate the two halves. As before, we’ll consider a “for example” case, where the thumb has been dragged 60% of the distance that it can drag. In both sides of the drawing, the first and third dashed lines from the top represent the distance that either the thumb or the content can travel:

The formula for finding the scrollPercent is once again repeated on the left. On the right, we see how this scrollPercent can be used to calculate the new y position of the content, and it should be clear from the drawing just how we come up with the formula on the right:

content.y = window.y – (contentRange * scrollPercent);

Since the scrollPercent was found out by dividing a certain y value by the thumbRange, when we take that same number (scrollPercent) and multiply it by the contentRange, we can find out another y value that’s in the same proportion but applied to a separate thing. The only other thing is to adjust for the y position of the window (similar to what we did on an earlier page when adjusting for the track’s y location). Consider this next drawing, which illustrates the same scenario (thumb dragged to 60%):

In this illustration, I’ve shrunk down the track and thumb section so that the two sides of the drawing are in much better proportion to each other. That is, conventionally, the scrollbar is usually the same height as the window it serves, so I made it that height. I’ve also put the thumb on the track and the content behind the window. Here you can get a better idea of the size difference between the thumbRange and the contentRange. Even though they are completely different, our formula calculates the percentage of the thumb’s progress related to its range, then it applies that same percentage to the relationship between the content and its range.

Consequently, the scrollbar’s track and the thumb can be any size, and the window and the content can likewise be any size, and the formula will still work. The magic ingredient here is the scrollPercent number, which provides a fantastic degree of accuracy. It’s calculated from two objects, then applied to two other objects as a ratio. And I’m not denying that we’re reinventing the wheel here, this has definitely all been figured out before, but maybe sometimes reinventing the wheel is good exercise, keeping us off the street and out of trouble .

Here’s the new code. The code that formerly affected the textbox and the circle has been removed. Our new formula that makes the content scroll has been added. I also added the line that sets the window as a mask for the content (line 11):

import flash.events.MouseEvent;

var yOffset:Number;
var topLimit:Number = track.y;
var thumbRange:Number = track.height - thumb.height;
var bottomLimit:Number = track.y + thumbRange;
//new variable: scrollPercent!
var scrollPercent:Number = 0;
var contentRange:Number = content.height - window.height;

content.mask = window;
thumb.buttonMode = true;
thumb.addEventListener(MouseEvent.MOUSE_DOWN, thumb_onMouseDown);
function thumb_onMouseDown(event:MouseEvent):void {
	yOffset = mouseY - thumb.y;
	stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.addEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

function stage_onMouseMove(event:MouseEvent):void {
	thumb.y = mouseY - yOffset;
	//restrict the movement of the thumb:
	if(thumb.y < topLimit) {
		thumb.y = topLimit;
	}
	if(thumb.y > bottomLimit) {
		thumb.y = bottomLimit;
	}
	//calculate scrollPercent and make it do stuff:
	scrollPercent = (thumb.y - track.y) / thumbRange;
	content.y = window.y - (scrollPercent * contentRange);
	event.updateAfterEvent();
}

function stage_onMouseUp(event:MouseEvent):void {
	stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.removeEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

Here is how the swf file behaves with this new code:

Cool! Now our scrollbar really works! We’re almost finished with our fla file based prototype. On the next page, we’ll see how to add some up and down buttons.

Adding up and down buttons

Next, let’s go ahead and make some up and down buttons in our fla file. First, the Up button: I just drew a green circle, converted it to a MovieClip, then added a static textbox inside that says “Up.” Having done that, I just right clicked its symbol in the library, and chose “duplicate.” Next, I edited the duplicate MovieClip symbol and made its static textbox say “Down.” Once again, you can download the above file to get my buttons, or you can create your own buttons from scratch. The cool thing is that the code we’ll write gives behavior to our buttons, but doesn’t care one bit what they look like or where they are positioned on the screen. So feel free to have some fun!

Give the new Up and Down buttons on the stage instance names in the properties panel. I named them “up” and “down.” We will begin by programming the down button. That way, when we test our movie, we can see something happening, since our content starts out even with the top of the window. But first, one more thing about that down button….

Oh, down is up and up is down…

One issue that can really be confusing is the use of the terms up and down. The thing is, when you are scrolling down in your content, your content clip is actually moving up! So to avoid confusion, when we say up and down, never think of those terms as describing the direction that the content clip is moving! Instead, think of it as being the direction our window is moving. Yeah, I know, the window is not moving at all, it’s sitting perfectly still! But your view of the content (as seen through the window) is moving. And if you consider your content clip as being your document, you are moving up or down in that.

The elevator analogy

Here’s another way to think of it: Imagine your window clip is an elevator, and your document (content) is the building. You start off in your elevator at the top floor (for the moment, nevermind how you got there!!). When your elevator is going down, you are scrolling down in your document. When it’s going up, you are scrolling up. And now, imagine that’s all an illusion, and that instead of the elevator moving up and down, the building moves up and down and the elevator is sitting still. Silly, I know. But it should help you remember that when we are scrolling down, the content is actually moving up, and vice versa.  So when I say “scroll down” I am describing that we are moving down in the document. And when I program a function called scrollDown, that is what I mean, too! And clicking and holding a button called down is going to continually move the content up. Got it?

So let’s begin programming the down button, and for now we won’t worry about the up button. Usually if you take these things in isolation, it’s much easier, and you’ll find that when you get done programming the down button, you can use the same pattern for the up button. Anyway, we start out just setting buttonMode to true. That’s easy!

down.buttonMode = true; 

Next, we want to add an event listener to the down button for the MOUSE_DOWN event. The strategy for this is similar to the drag code we wrote on the thumb. When the user mouses down on the down clip, inside the handler for the MOUSE_DOWN event, add an event listener for some kind of continuous action, and also add an event listener to the stage for MOUSE_UP. In the handler for the continuous action, write code that moves the content, of course. In the handler for the MOUSE_UP, remove both the listener for the continuous action, and the MOUSE_UP listener itself. Once again, by writing it this way, we make sure that the continuous action function isn’t firing unless the button is down, and the MOUSE_UP handler removes that listener and its own listener as well.

Let’s just get down that pattern for now, and leave off the actual (continuous action) code that moves the content. We’ll do that as a second step. So here’s that framework:

down.buttonMode = true;
down.addEventListener(MouseEvent.MOUSE_DOWN, down_onMouseDown);
function down_onMouseDown(event:MouseEvent):void {
	stage.addEventListener(Event.ENTER_FRAME, scrollDown);
	stage.addEventListener(MouseEvent.MOUSE_UP, stopScrollingDown);
}
function scrollDown(event:Event):void {
	//code that does the scrolling will go here

}
function stopScrollingDown(event:MouseEvent):void {
	stage.removeEventListener(Event.ENTER_FRAME, scrollDown);
	stage.removeEventListener(MouseEvent.MOUSE_UP, stopScrollingDown);
}

As you can see, our choice for continuous action is going to be an ENTER_FRAME event. Another choice might have been to use the Timer class. You might think that the Timer would be the best choice, since it’s got that cool updateAfterEvent() method available to its associated TimerEvent class. But I found in my testing that the difference was not very dramatic at all, and that the ENTER_FRAME version seemed just as good. So we’ll go with that. The only small change I made was to increase the framerate of the movie from 24 to 30, just to make it a bit smoother.

Next, let’s move the content!

Now, you might think that you would just start adjusting the y property of the content directly when the button is held down. That’s what I thought, too, a long time ago, when I first worked all this out. And I did it that way at first, too, but I had problems coming up with some kind of inverse formula for updating the thumb’s position. But finally, I arrived at the key to the whole thing, and I’ll be glad to share it with you!

First, let’s consider what happens when we drag the thumb. We have a formula that moves the content at the same time. However, now we are going to be moving the content a totally different way, by clicking the down button. But when we do, we want the thumb to also move by itself to dynamically reflect the new position of the content in relation to the window. So I knew that I would probably have to keep recalculating the scrollPercent continually, and come up with some kind of inverse formula for positioning the thumb.

But thinking about it further, in the first case the thumb is being manipulated manually, and the content is moving, and it works because we are continually calculating a new scrollPercent.  To truly invert that process, I needed to manually manipulate the scrollPercent. Then, both the positions of the content and the thumb could be calculated off of that. And I already had the formula for the content’s y, which we just looked at on the last page, the one that gets used when you drag the thumb.

I just needed to take the original formula that calculated the scrollPercent:

scrollPercent = (thumb.y – track.y) / thumbRange;

and solve the equation for the thumb’s y, when given the scrollPercent!

My algebra comes back to me (through the fog):

Here’s the solution. Remember that in algebra you are allowed to perform whatever operations you need to, but the rule is that you have to always perform them on both sides equally:

First multiply both sides by thumbRange, to get rid of the division on the right:
scrollPercent * thumbRange = (thumb.y – track.y) / thumbRange * thumbRange

which leaves:
scrollPercent * thumbRange = thumb.y – track.y

Next, add track.y to both sides to get rid of the subtraction on the right:
scrollPercent * thumbRange + track.y = thumb.y – track.y + track.y

which leaves:
scrollPercent * thumbRange + track.y = thumb.y

then, swapping the two sides of the equals sign (also legal):
thumb.y = scrollPercent * thumbRange + track.y

Solved! Hey, don’t tell me your high school (grade school?  ??) algebra will never come in handy again! Woohoo!

So, when the up and down buttons are pressed, we’ll continually raise or lower the value of the scrollPercent variable. This will happen at the framerate of the movie. Our formulas will continually calculate new y positions for both the thumb and the content, based on the scrollPercent. The only other thing to decide is how much to increase or decrease the scrollPercent each time. I found that a value of 0.01 works well. This can be saved into a variable called speed.

var speed:Number = 0.01;

Place this line somewhere near the top of the code, in the variables list.

Also, since scrollPercent must always be a number between 0 and 1, we need to put in some checks to make sure it never goes out of those bounds. Here is the finished ENTER_FRAME function for the down button, with those formulas and the boundary check worked in:

function scrollDown(event:Event):void {
	scrollPercent += speed;
	if(scrollPercent > 1) {
		scrollPercent = 1;
	}
	content.y = window.y - (scrollPercent * contentRange);
	thumb.y = scrollPercent * thumbRange + track.y;
}

Now that the down button has been programmed, the movie can be tested. The down button should scroll the content when held down, and the thumb will also move dynamically to its calculated positions. The beauty of this system is that everything is tied in to the scrollPercent in some way, and that number, whether we calculate it or set it manually, can never be more than 1 or less than 0. Consequently, our thumb can never wander off the track’s top or bottom, and the content will always stop at each end exactly where it is supposed to.

Dragging the thumb causes the scrollPercent to get calculated as a hard number. But clicking the down button increments the same variable. In other words, it considers what value it currently already has and adds to it. So the two systems, dragging the thumb, and clicking and holding the down button, both manipulate the same variable, but in different but complementary ways!

Next, let’s go ahead and do the programming for the up button. This is fairly easy: just copy and paste the code block for down, then go through and make the appropriate changes. Read the complete code listing below and note the differences between these two (the differing function names, etc):

import flash.events.MouseEvent;
import flash.events.Event;
import flash.utils.Timer;

var yOffset:Number;
var topLimit:Number = track.y;
var thumbRange:Number = track.height - thumb.height;
var bottomLimit:Number = track.y + thumbRange;
//new variable: scrollPercent!
var scrollPercent:Number = 0;
var contentRange:Number = content.height - window.height;
var speed:Number = 0.01;

content.mask = window;
thumb.buttonMode = true;
thumb.addEventListener(MouseEvent.MOUSE_DOWN, thumb_onMouseDown);
function thumb_onMouseDown(event:MouseEvent):void {
	yOffset = mouseY - thumb.y;
	stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.addEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

function stage_onMouseMove(event:MouseEvent):void {
	thumb.y = mouseY - yOffset;
	//restrict the movement of the thumb:
	if(thumb.y < topLimit) {
		thumb.y = topLimit;
	}
	if(thumb.y > bottomLimit) {
		thumb.y = bottomLimit;
	}
	//calculate scrollPercent and make it do stuff:
	scrollPercent = (thumb.y - track.y) / thumbRange;
	content.y = window.y - (scrollPercent * contentRange);
	event.updateAfterEvent();
}

function stage_onMouseUp(event:MouseEvent):void {
	stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
	stage.removeEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}

down.buttonMode = true;
down.addEventListener(MouseEvent.MOUSE_DOWN, down_onMouseDown);
function down_onMouseDown(event:MouseEvent):void {
	stage.addEventListener(Event.ENTER_FRAME, scrollDown);
	stage.addEventListener(MouseEvent.MOUSE_UP, stopScrollingDown);
}
function scrollDown(event:Event):void {
	scrollPercent += speed;
	if(scrollPercent > 1) {
		scrollPercent = 1;
	}
	content.y = window.y - (scrollPercent * contentRange);
	thumb.y = scrollPercent * thumbRange + track.y;
}
function stopScrollingDown(event:MouseEvent):void {
	stage.removeEventListener(Event.ENTER_FRAME, scrollDown);
	stage.removeEventListener(MouseEvent.MOUSE_UP, stopScrollingDown);
}

up.buttonMode = true;
up.addEventListener(MouseEvent.MOUSE_DOWN, up_onMouseDown);
function up_onMouseDown(event:MouseEvent):void {
	stage.addEventListener(Event.ENTER_FRAME, scrollUp)
	stage.addEventListener(MouseEvent.MOUSE_UP, stopScrollingUp);
}
function scrollUp(event:Event):void {
	scrollPercent -= speed;
	if(scrollPercent < 0) {
		scrollPercent = 0;
	}
	content.y = window.y - (scrollPercent * contentRange);
	thumb.y = scrollPercent * thumbRange + track.y;
}
function stopScrollingUp(event:MouseEvent):void {
	stage.removeEventListener(Event.ENTER_FRAME, scrollUp)
	stage.removeEventListener(MouseEvent.MOUSE_UP, stopScrollingUp);
}

And here’s how the swf behaves:

Our timeline prototyping is finished now, and we are done with math, too! Now we can proceed to turn all of this into a handy, reusable class!

Creating the VerticalScrollbar class

Source File:

Making some heavy decisions

Now we are going to start writing our class. We’ll call it VerticalScrollbar. The first thing to do is to start making decisions about this class based on our best guess as to how we are going to use it, and if we are going to distribute it to others, how we expect that others might want to use it. Naturally, we want it to be as easy to use as possible.

This class can be written any number of different ways, but here’s my vision for it: Users of our class ought to be able to create their own MovieClip instances for thumb, track, window, content, up, and down, then just make an instance of the class, sending it those things as arguments to the constructor. The thumb and the track will be required parameters, because without them, there is no scrollbar anyway. The window and content will both be optional parameters with default values of null. This will allow users of our class to create an instance and just use it as a Slider. Sometimes I’ve seen advice from others that you create a separate Slider class, and make it a property of the Scrollbar class, but I’ve decided against that. Our class will just do double duty, like I just described.

More optional parameters will be the up and down buttons. If they are supplied, the class will program them and define their behavior.

Another question is whether the scrollbar ought to itself be a display object (a display object container, actually). I have opted not to do this, because I see the class as just providing the behavior of a scrollbar, minus the baggage. When you send it all those objects as arguments, the class will just make them all behave like a scrollbar. The class will not actually itself be a scrollbar. That way, inside the class, issues like positioning the parts and adding them to its own display list need not be a consideration. And from the outside, instances of the class need not be positioned or added to the display list either. After all, the parts will already be positioned, and will already be on the display list anyway. The only possible advantage I can see to making the class itself a display object would be when showing and hiding the scrollbar, but we’ll just make show() and hide() public methods to do those things anyway (EDIT: As you’ll see later, I actually wound up doing this in a different, yet better, way–Jody).

Since it won’t itself be a display object, the class will need to be sent a reference to the stage. This will appear as the first parameter.

So the parameter list will look like this (arranged, on purpose, in the order of most required):

VerticalScrollbar(stage, thumb, track, window=null, content=null, up=null, down=null)

Yet another question is whether the window and content ought to be incorporated into another Class that has an instance of the VerticalScrollbar class as a property. I have opted not to do this, but to just get it all done in one class, to make it easier to use.

The class is also going to need to dispatch some events, to accommodate the use of it as a Slider. Since it’s not extending a display object, which would have given it event dispatching abilities, our class is going to need to extend the EventDispatcher class. The custom events that we’ll make it dispatch will let us know (from the outside) when the thumb is pressed, when the thumb is released, and when the scrollbar is actively scrolling. We’ll see how to dispatch these custom events from inside the class, and how to listen for them from the outside.

Another consideration is that the client code may need to access the scrollPercent property. Here I should say that this present tutorial owes a huge debt of gratitude to the Object Oriented Scrollbar  tutorial by Lee Brimelow at gotoAndLearn.com . But in that tutorial, when it comes to accessing the scrollPercent, Lee shows how to create another separate custom class that extends the Event class, and how to send the scrollPercent as a property of that class, so as to access it in your event handler function. I highly recommend Lee’s tutorial. However, I’ve decided that our class will just provide a public getter method for finding out the scrollPercent. So I’ve opted to not create a separate Event class. This just seems easier to me than creating a whole other class when I know that this is probably the only property I will need to access from the outside.

But without Lee’s tutorial, this one wouldn’t exist, and it was his tutorial that provided a milestone in my thinking about OOP, event dispatching, and things like “loose coupling.” So, a thousand thanks, Lee!

Anyway, you can see I put a lot of thought into all of the above. These are all considerations where others might have a different opinion, and that doesn’t bother me a bit. You are welcome to take off in your own direction and code it your own way. I’m just presenting how I did it, and why I did it that way, and I think I’ve got some good reasons. Here is our beginning basic framework for the class:

package com.jessamin.controls {
	import flash.display.DisplayObject;
	import flash.display.InteractiveObject;
	import flash.display.Sprite;
	import flash.display.Stage;
	import flash.events.EventDispatcher;

	public class VerticalScrollbar extends EventDispatcher {
		private var stage:Stage;
		private var thumb:InteractiveObject;
		private var track:DisplayObject;
		private var window:DisplayObject;
		private var content:DisplayObject;
		private var up:InteractiveObject;
		private var down:InteractiveObject;

		public function VerticalScrollbar(  stage:Stage, 
											thumb:InteractiveObject, 
											track:InteractiveObject, 
											window:DisplayObject = null,
											content:DisplayObject = null,
											up:InteractiveObject = null, 
											down:InteractiveObject = null  ) {
			this.stage    = stage;
			this.thumb    = thumb;
			this.track    = track;
			this.window   = window;
			this.content  = content;
			this.up       = up;
			this.down     = down;
		}
	}
}

Taking this line by line, the first thing to note is that this is going into a package at com.jessamin.controls (Jessamin Swearingen  is a teacher friend of mine in NYC, and I just decided to use her website’s name as an example package name).  If you are unfamiliar with packages and classpaths, you might want to read my tutorial Flash: The Big Picture. The import statements mostly just import the classes to support the several differenttypes I used in the constructor (the exception is the one for EventDispatcher, which our class must import because it extends it). In the parameters list, everything is typed either as a display object or an interactive object. It’s an interactive object if we are going to (potentially) allow user interaction with it (example: do you want the scrollbar to allow users to click on the track?). The variables list sets up private variables to match each of the items being passed in by the contructor.

The reason for using these more generic types instead of MovieClip, for example, is that the user of our class ought to be able to use a SimpleButton instance if they want to, for example. SimpleButtons are not MovieClips, but both MovieClip’s and SimpleButtons are InteractiveObject’s. So this way the parameter will accept a broader range of types and still work.

The first three items are required. The final four are optional, because they have an equals sign designating a default value of null. Later (that is, in the other functions in the class), we can test items to see if they still have a null value, so that we can tell whether to perform certain actions.

Now that we have the class’s framework, we can start bringing in the code we developed on the timeline. A good workflow for this is to comment out all the code in the fla file, then write some new code there that makes an instance of the class instead. So turning our attention back to the fla file, go to the actions panel for frame 1. Click once in the actions panel so it has focus. Press CTRL-A to select everything there. Right click the selected code and choose “Apply /* */ Comment” from the context menu. This will comment out the whole block of code.

Select and copy this whole block of code again, but this time include the comment delimiters. Next, go to the window where you are writing the class, and paste it all in at the bottom. Because it’s a comment in both places, it won’t cause any problems in either one when testing. By keeping a copy of the code in both places, we have a safeguard against losing any of it should there be any crashes. After that, you just start transplanting the code from one place to another in the class, and testing, until eventually you get the whole thing working again.

Next, returning to the actions panel, create some space by pressing Enter a couple of times at the top. Let’s write an import statement and also create an instance of the class, passing it all the objects that are on the stage to its parameter list:

import com.jessamin.controls.*;
var scrollbar:VerticalScrollbar = new VerticalScrollbar(stage, thumb, track, window, content, up, down);

Press CTRL-Enter to test the movie. It should do… absolutely nothing! Because we haven’t programmed it to yet! But we are just making sure we don’t get any errors. On the next page, we’ll continue adapting the timeline code to the class.

The VerticalScrollbar class, continued

Source File:

Code listings for the class are going to start getting very long, but much of it is just lists: lists of imports, lists of variables. But here you can see that I’ve begun bringing in the timeline code and adapting it:

package com.jessamin.controls {
	import flash.display.DisplayObject;
	import flash.display.InteractiveObject;
	import flash.display.Sprite;
	import flash.display.Stage;
	import flash.events.EventDispatcher;
	import flash.events.MouseEvent;

	public class VerticalScrollbar extends EventDispatcher {
		private var stage:Stage;
		private var thumb:InteractiveObject;
		private var track:DisplayObject;
		private var window:DisplayObject;
		private var content:DisplayObject;
		private var up:InteractiveObject;
		private var down:InteractiveObject;

		private var yOffset:Number;
		private var topLimit:Number;
		private var thumbRange:Number;
		private var bottomLimit:Number;
		private var scrollPercent:Number;
		private var contentRange:Number;
		private var speed:Number

		public function VerticalScrollbar(  stage:Stage, 
											thumb:InteractiveObject, 
											track:InteractiveObject, 
											window:DisplayObject = null,
											content:DisplayObject = null,
											up:InteractiveObject = null, 
											down:InteractiveObject = null  ) {
			this.stage    = stage;
			this.thumb    = thumb;
			this.track    = track;
			this.window   = window;
			this.content  = content;
			this.up       = up;
			this.down     = down;

			yOffset = 0;
            topLimit = this.track.y;
			thumbRange = track.height - thumb.height;
			bottomLimit = track.y + thumbRange;
			if(content && window) contentRange = content.height - window.height;
			scrollPercent = 0;
			speed = 0.01;

			thumb.addEventListener(MouseEvent.MOUSE_DOWN, thumb_onMouseDown);
		}
		private function thumb_onMouseDown(event:MouseEvent):void {
			yOffset = thumb.parent.mouseY - thumb.y;
			stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
			stage.addEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
		}
		private function stage_onMouseMove(event:MouseEvent):void {
			thumb.y = thumb.parent.mouseY - yOffset;
			//restrict the movement of the thumb:
			if(thumb.y < topLimit) {
				thumb.y = topLimit;
			}
			if(thumb.y > bottomLimit) {
				thumb.y = bottomLimit;
			}
			//calculate scrollPercent and make it do stuff:
			scrollPercent = (thumb.y - track.y) / thumbRange;
			if(content && window) content.y = window.y - (scrollPercent * contentRange);
			event.updateAfterEvent();
		}
		private function stage_onMouseUp(event:MouseEvent):void {
			stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
			stage.removeEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
		}

	}
}

When the timeline code is brought in, the variables should be declared in the variables list, but they should get their initial values in the constructor function. So you have to separate those in each and every case (lines 18-24, and 42-48). It’s a lot of work, but it’s all just editing. Also, you can add the private modifier to the declarations, and also to the functions. One other change I had to make is that the mouseY reference is no longer available, this class not being a display object container. So that was changed to thumb.parent.mouseY (lines 52 and 57). The thumb’s parent (whatever it is) has the mouseY property we’re after.

In lines 45 and 67, we have to allow for the fact that either the content variable or the window variable might be null (the user of our class didn’t supply a value). So we only execute those lines if they are not null.

Line 45 could have been written in three lines like this:

if(content != null && window != null) {
	contentRange = content.height - window.height;
}

But, guess what? We can shorten that to one neat line. In AS3, any if statements with a one line statement “block” can be written on one line, without the curly braces, instead! And testing for null can be done just by testing the object itself for true or false. So the above was shortened to:

if(content && window) contentRange = content.height – window.height;

Either way gets the same job done, the short form is just less verbose and saves some space. That line says if both objects are not null, do the following, otherwise the whole deal is off! Of course, nobody would send our class one reference without the other, now would they? Well, it doesn’t make sense, but you want your code to even allow for edge cases like nonsensical arguments!

The changes so far are enough to make the scrollbar function. One other thing to note: We are going to leave it to the outside code (in the fla) to set the buttonMode for any Sprites. Also, it will be left to the fla file to create the masking.

The reason in the first case (buttonMode) is because the user of the class might not want buttonMode turned on at all. They might prefer the arrow cursor instead. It’s less work for us, too, because this way the class need not detect if it’s being sent a Sprite, a SimpleButton, or what.

The reason in the second case (the masking) is to allow for the possibility that the user of the class might be using the timeline to do manual masking of one layer by another, and wouldn’t in that case want the class making the mask for them. So add these lines to the fla file:

thumb.buttonMode = true;
up.buttonMode = true;
down.buttonMode = true;
content.mask = window;

You can see that we are able to decide which things the class should be responsible for, but if we want the fla file to take care of it instead, well, both code blocks have references to the same set of objects and can tell them what to do. Cool! Now test the movie, and observe that the thumb and track are working and scrolling the content as before.

Next, I continued to add in the timeline code, as before. I didn’t experience any other issues, and when I tested, the code worked just like it did when it was on the timeline. Here’s the entire class listing so far:

package com.jessamin.controls {
	import flash.display.DisplayObject;
	import flash.display.InteractiveObject;
	import flash.display.Sprite;
	import flash.display.Stage;
	import flash.events.EventDispatcher;
	import flash.events.MouseEvent;
	import flash.events.Event;

	public class VerticalScrollbar extends EventDispatcher {
		private var stage:Stage;
		private var thumb:InteractiveObject;
		private var track:DisplayObject;
		private var window:DisplayObject;
		private var content:DisplayObject;
		private var up:InteractiveObject;
		private var down:InteractiveObject;

		private var yOffset:Number;
		private var topLimit:Number;
		private var thumbRange:Number;
		private var bottomLimit:Number;
		private var scrollPercent:Number;
		private var contentRange:Number;
		private var speed:Number

		public function VerticalScrollbar(  stage:Stage, 
											thumb:InteractiveObject, 
											track:InteractiveObject, 
											window:DisplayObject = null,
											content:DisplayObject = null,
											up:InteractiveObject = null, 
											down:InteractiveObject = null  ) {
			this.stage    = stage;
			this.thumb    = thumb;
			this.track    = track;
			this.window   = window;
			this.content  = content;
			this.up       = up;
			this.down     = down;

			yOffset = 0;			
			topLimit = this.track.y;
			thumbRange = track.height - thumb.height;
			bottomLimit = track.y + thumbRange;
			if(content && window) contentRange = content.height - window.height;
			scrollPercent = 0;
			speed = 0.01;

			thumb.addEventListener(MouseEvent.MOUSE_DOWN, thumb_onMouseDown);
			if(down) down.addEventListener(MouseEvent.MOUSE_DOWN, down_onMouseDown);
			if(up) up.addEventListener(MouseEvent.MOUSE_DOWN, up_onMouseDown);
		}
		private function thumb_onMouseDown(event:MouseEvent):void {
			yOffset = thumb.parent.mouseY - thumb.y;
			stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
			stage.addEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
		}
		private function stage_onMouseMove(event:MouseEvent):void {
			thumb.y = thumb.parent.mouseY - yOffset;
			//restrict the movement of the thumb:
			if (thumb.y < topLimit) thumb.y = topLimit;
			if (thumb.y > bottomLimit) thumb.y = bottomLimit;
			//calculate scrollPercent and make it do stuff:
			scrollPercent = (thumb.y - track.y) / thumbRange;
			if(content && window) content.y = window.y - (scrollPercent * contentRange);
			event.updateAfterEvent();
		}
		private function stage_onMouseUp(event:MouseEvent):void {
			stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
			stage.removeEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
		}
		private function down_onMouseDown(event:MouseEvent):void {
			stage.addEventListener(Event.ENTER_FRAME, scrollDown);
			stage.addEventListener(MouseEvent.MOUSE_UP, stopScrollingDown);
		}
		private function scrollDown(event:Event):void {
			scrollPercent += speed;
			if(scrollPercent > 1) scrollPercent = 1;
			if(content && window) content.y = window.y - (scrollPercent * contentRange);
			thumb.y = scrollPercent * thumbRange + track.y;
		}
		private function stopScrollingDown(event:MouseEvent):void {
			stage.removeEventListener(Event.ENTER_FRAME, scrollDown);
			stage.removeEventListener(MouseEvent.MOUSE_UP, stopScrollingDown);
		}
		private function up_onMouseDown(event:MouseEvent):void {
			stage.addEventListener(Event.ENTER_FRAME, scrollUp)
			stage.addEventListener(MouseEvent.MOUSE_UP, stopScrollingUp);
		}
		private function scrollUp(event:Event):void {
			scrollPercent -= speed;
			if(scrollPercent < 0)scrollPercent = 0;
			if(content && window) content.y = window.y - (scrollPercent * contentRange);
			thumb.y = scrollPercent * thumbRange + track.y;
		}
		private function stopScrollingUp(event:MouseEvent):void {
			stage.removeEventListener(Event.ENTER_FRAME, scrollUp)
			stage.removeEventListener(MouseEvent.MOUSE_UP, stopScrollingUp);
		}
	}
}

I also shortened some of the other if statements to the single line form (lines 61, 62, 78, and 92 above). Next, I practiced sending the class various arrangements of arguments, and found that it worked perfectly in all the different variations I tried. If I left off either of the up and down buttons, then that button was deactivated, but there were no errors. If I left off either window or content, then the content wouldn’t scroll, but once again no errors.

Here’s an interesting variation:

var scrollbar:VerticalScrollbar = new VerticalScrollbar(stage, thumb, track, null, null, up, down);

That one caused the content not to scroll in the window, but the thumb could be moved manually or with the up and down buttons. Truly, if someone knows how to use this class, and sends it the proper arguments, they should be able to get out of it whatever behavior they want!

Our class is fully functional at this point, and nearly finished! On the next page, we’ll make it dispatch events, add a few more little enhancements, and test it out.

Dispatching events and more enhancements

Next, we’ll create some custom events and dispatch them from strategic places, so that outside code can (potentially) listen for them. I can imagine that outside code might want to know three basic events from our class: When scrolling is initiated, when scrolling is presently happening, and when scrolling ceases. We’ll create some public static constants for these events named PRESSED, SCROLLING, and RELEASED. So add these lines to the top of the variables list in the class:

public static const PRESSED:String = "pressed";
public static const RELEASED:String = "released";
public static const SCROLLING:String = "scrolling";

As I have written in previous tutorials, public static constants are just simple variables that store a string of text. That’s their only job. Since they are public, they can be accessed by any other code that has this class in its classpath or imports it. Since they are static, they belong to the class, so it’s not even necessary to make an instance of the class to use them. They are accessed by just using the name of the class, a dot, then the name of the constant:

trace(VerticalScrollbar.PRESSED); //outputs “pressed”

I have also written in another tutorial about the good reasons for using the convention of creating formalized constants like this. See this page: Dispatching custom events from our class.

The strategy for placing these event dispatches is simple. Every place where there’s a MOUSE_DOWN event we will dispatch our PRESSED event. Every place where there is continuous action (the MOUSE_MOVE events and the ENTER_FRAME events) we will dispatch our SCROLLING event. Every place where there is a MOUSE_UP event we will dispatch our RELEASED event. Users of our class can attach event listeners to an instance of our class to be notified when these events happen.

One other thing we need to provide is a getter method for the scrollPercent. Before we do this, we should do a search-and-replace on our class file, and change every occurrence of “scrollPercent” to “_scrollPercent.” Underscores are really kind of a pain to put on every private variable, but their use becomes a necessary evil when you want to make getters and/or setters for a variable like this.

Having made all those replacements, at the end of the class, we can just write the getter method like so:

public function get scrollPercent():Number {
	return _scrollPercent;
}

Getter and setter methods are cool. By using the get and/or set keywords, you can create methods that are treated exactly like properties from the outside. I have written about their use in other tutorials, too, so I won’t cover that ground again. Suffice it to say that with the above in place, if outside code made an instance of our class called scrollbar, it could access this method like this:

scrollbar.scrollPercent

…and the instance will run its get scrollPercent() method, and return the value to the calling code. But to all appearances from the outside we just accessed a property.

I am hesitant at this point to paste in the whole code for the class so far, because it’s so long. So, let’s make some other enhancements first. The first thing that would be nice would be some kind of public method to turn the scrollbar’s visibility on or off. My first inclination was to create public methods called show() and hide(). But I think this whole task would be better accomplished by giving our class a visible property, and write some public getter and setter methods for it:

public function set visible(value:Boolean):void {
	thumb.visible = value;
	track.visible = value;
	if (up) up.visible = value;
	if (down) down.visible = value;
}
public function get visible():Boolean {
	return (track.visible && thumb.visible);
}

Now outside code can just simply say:

scrollbar.visible = false;

or even:

scrollbar.visible = !scrollbar.visible; //toggle visibility on and off

But notice, unlike the case of the scrollPercent, there is no actual matching _visible private property in our class at all. We just made a pseudo property by using getter and setter methods alone. And here we see a big advantage to using getters and setters vs. public properties: our setter method above was able to execute four lines of code. You can run as many lines of code as you want, yet the whole thing from the outside still looks like you’re just setting a property!

Next, it would be great for the outside code to be able to call a public reset() method that would instantly put the thumb back to the top of the track, the content to the top of the window, and set the _scrollPercent to 0:

public function reset():void {
	thumb.y = track.y;
	if (content && window) content.y = window.y;
	_scrollPercent = 0;
}

As yet another enhancement, it’s kind of a nice touch to have your scrollbar use the mouse wheel, too! So let’s add in that functionality. We’ll create a public setter method that will turn it on and off called wheelEnabled:

public function set wheelEnabled(value:Boolean):void {
	if (value == true) {
		stage.addEventListener(MouseEvent.MOUSE_WHEEL, stage_onMouseWheel);
	} else {
		stage.removeEventListener(MouseEvent.MOUSE_WHEEL, stage_onMouseWheel);
	}
}

This is yet another pseudo property for which there is no matching private property. Instead of manipulating a private variable, this setter method can be treated like a Boolean variable from the outside, but what it actually does is either adds or removes an event listener depending on whether it gets sent a true or false value from the outside!

We’ll call this new method from the constructor, and turn wheelEnabled on by default:

this.wheelEnabled = true; (line 60 in the finished class, below)

Next, let’s write the event handler function for the MOUSE_WHEEL event:

private function stage_onMouseWheel(event:MouseEvent):void {
	_scrollPercent -= event.delta * wheelSpeed;
	//set boundaries:
	if (_scrollPercent > 1) _scrollPercent = 1;
	if (_scrollPercent < 0) _scrollPercent = 0;
	if(content && window) content.y = window.y - (_scrollPercent * contentRange);
	thumb.y = _scrollPercent * thumbRange + track.y;
	dispatchEvent(new Event(VerticalScrollbar.SCROLLING));
}

We need to also create the private variable called wheelSpeed that the above function is using, so we add wheelSpeed to the variables list, type it as a Number, and then give it its value in the constructor. In testing, I found that scrolling with the wheel ought to be faster than the up or down buttons, and that was the reason I made a separate variable for it. I also found that the delta property of the event object would give me either 3 or -3 depending on the direction of the wheel scrolling. So I wrote a line of code that uses that delta value and multiplies it by the wheelSpeed variable, to yield a change of (plus or minus) 0.06 to the _scrollPercent. This meant that wheelSpeed needed to be 0.02.

If you wanted to, you could provide getter and setter methods so that these speeds could be adjusted from the outside. I’ll leave that as an exercise for you. At this point, if you have been reading and understanding everything so far, you should be able to do it easily.

Here’s the finished class:

package com.jessamin.controls {
	import flash.display.DisplayObject;
	import flash.display.InteractiveObject;
	import flash.display.Sprite;
	import flash.display.Stage;
	import flash.events.EventDispatcher;
	import flash.events.MouseEvent;
	import flash.events.Event;

	public class VerticalScrollbar extends EventDispatcher {
		public static const PRESSED:String = "pressed";
		public static const RELEASED:String = "released";
		public static const SCROLLING:String = "scrolling";

		private var stage:Stage;
		private var thumb:InteractiveObject;
		private var track:DisplayObject;
		private var window:DisplayObject;
		private var content:DisplayObject;
		private var up:InteractiveObject;
		private var down:InteractiveObject;

		private var yOffset:Number;
		private var topLimit:Number;
		private var thumbRange:Number;
		private var bottomLimit:Number;
		private var _scrollPercent:Number;
		private var contentRange:Number;
		private var speed:Number
		private var wheelSpeed:Number;

		public function VerticalScrollbar(  stage:Stage, 
											thumb:InteractiveObject, 
											track:InteractiveObject, 
											window:DisplayObject = null,
											content:DisplayObject = null,
											up:InteractiveObject = null, 
											down:InteractiveObject = null  ) {
			this.stage    = stage;
			this.thumb    = thumb;
			this.track    = track;
			this.window   = window;
			this.content  = content;
			this.up       = up;
			this.down     = down;

			yOffset = 0;
			topLimit = this.track.y;
			thumbRange = track.height - thumb.height;
			bottomLimit = track.y + thumbRange;
			if(content && window) contentRange = content.height - window.height;
			_scrollPercent = 0;
			speed = 0.01;
			wheelSpeed = 0.02;

			thumb.addEventListener(MouseEvent.MOUSE_DOWN, thumb_onMouseDown);
			if(down) down.addEventListener(MouseEvent.MOUSE_DOWN, down_onMouseDown);
			if (up) up.addEventListener(MouseEvent.MOUSE_DOWN, up_onMouseDown);
			//turn on mouse wheel by default (calls the class's public setter method, below)
			this.wheelEnabled = true; 
		}
		private function stage_onMouseWheel(event:MouseEvent):void {
			_scrollPercent -= event.delta * wheelSpeed;
			//set boundaries:
			if (_scrollPercent > 1) _scrollPercent = 1;
			if (_scrollPercent < 0) _scrollPercent = 0;
			if(content && window) content.y = window.y - (_scrollPercent * contentRange);
			thumb.y = _scrollPercent * thumbRange + track.y;
			dispatchEvent(new Event(VerticalScrollbar.SCROLLING));
		}
		private function thumb_onMouseDown(event:MouseEvent):void {
			yOffset = thumb.parent.mouseY - thumb.y;
			stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
			stage.addEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
			dispatchEvent(new Event(VerticalScrollbar.PRESSED));
		}
		private function stage_onMouseMove(event:MouseEvent):void {
			thumb.y = thumb.parent.mouseY - yOffset;
			//restrict the movement of the thumb:
			if (thumb.y < topLimit) thumb.y = topLimit;
			if (thumb.y > bottomLimit) thumb.y = bottomLimit;
			//calculate scrollPercent and make it do stuff:
			_scrollPercent = (thumb.y - track.y) / thumbRange;
			if(content && window) content.y = window.y - (_scrollPercent * contentRange);
			event.updateAfterEvent();
			dispatchEvent(new Event(VerticalScrollbar.SCROLLING));
		}
		private function stage_onMouseUp(event:MouseEvent):void {
			stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_onMouseMove);
			stage.removeEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
			dispatchEvent(new Event(VerticalScrollbar.RELEASED));
		}
		private function down_onMouseDown(event:MouseEvent):void {
			stage.addEventListener(Event.ENTER_FRAME, scrollDown);
			stage.addEventListener(MouseEvent.MOUSE_UP, stopScrollingDown);
			dispatchEvent(new Event(VerticalScrollbar.PRESSED));
		}
		private function scrollDown(event:Event):void {
			_scrollPercent += speed;
			if(_scrollPercent > 1) _scrollPercent = 1;
			if(content && window) content.y = window.y - (_scrollPercent * contentRange);
			thumb.y = _scrollPercent * thumbRange + track.y;
			dispatchEvent(new Event(VerticalScrollbar.SCROLLING));
		}
		private function stopScrollingDown(event:MouseEvent):void {
			stage.removeEventListener(Event.ENTER_FRAME, scrollDown);
			stage.removeEventListener(MouseEvent.MOUSE_UP, stopScrollingDown);
			dispatchEvent(new Event(VerticalScrollbar.RELEASED));
		}
		private function up_onMouseDown(event:MouseEvent):void {
			stage.addEventListener(Event.ENTER_FRAME, scrollUp)
			stage.addEventListener(MouseEvent.MOUSE_UP, stopScrollingUp);
			dispatchEvent(new Event(VerticalScrollbar.PRESSED));
		}
		private function scrollUp(event:Event):void {
			_scrollPercent -= speed;
			if(_scrollPercent < 0)_scrollPercent = 0;
			if(content && window) content.y = window.y - (_scrollPercent * contentRange);
			thumb.y = _scrollPercent * thumbRange + track.y;
			dispatchEvent(new Event(VerticalScrollbar.SCROLLING));
		}
		private function stopScrollingUp(event:MouseEvent):void {
			stage.removeEventListener(Event.ENTER_FRAME, scrollUp)
			stage.removeEventListener(MouseEvent.MOUSE_UP, stopScrollingUp);
			dispatchEvent(new Event(VerticalScrollbar.RELEASED));
		}
		public function get scrollPercent():Number {
			return _scrollPercent;
		}
		public function set visible(value:Boolean):void {
			thumb.visible = value;
			track.visible = value;
			if (up) up.visible = value;
			if (down) down.visible = value;
		}
		public function get visible():Boolean {
			return (track.visible && thumb.visible);
		}
		public function reset():void {
			thumb.y = track.y;
			if (content && window) content.y = window.y;
			_scrollPercent = 0;
		}
		public function set wheelEnabled(value:Boolean):void {
			if (value == true) {
				stage.addEventListener(MouseEvent.MOUSE_WHEEL, stage_onMouseWheel);
			} else {
				stage.removeEventListener(MouseEvent.MOUSE_WHEEL, stage_onMouseWheel);
			}
		}
	}
}

With all these code changes in place, here’s how the swf now behaves. I’ve thrown in a couple of new buttons in the fla file that toggle the visibility and reset the scrollbar. Also, check out the mouse wheel (You’ll have to click the swf at least once to give it “focus” and get the mouse wheel to work)!

While being able to use the mouse wheel is really cool, in the interest of full disclosure I want to say that it’s not without issues, and they are issues that I’ve only researched a little bit. The first issue is that if your flash application is embedded in an HTML web page, and that web page is more full than the browser window (like this page you’re reading now), so that the web page has a scrollbar–when you roll the mouse wheel, your content clip will scroll like always but so will the HTML page, both at the same time! If the embedding HTML page does NOT have a scrollbar, there is no issue. But ideally, there needs to be some sort of solution where just the flash application scrolls when the mouse is over it, and just the HTML page scrolls when the mouse is over it.

Fortunately, in researching this problem, I found a solution here: MouseWheelTrap  by Liam O’Donnell. It’s a utility class that you just arrange into your classpath at the right location, then import it into your fla file, then run its static method setup(), passing it a reference to the stage:

import com.spikything.utils.MouseWheelTrap;
MouseWheelTrap.setup(stage);

Then just compile your project, and it takes care of the rest. If you download the zip file at the top of this page, you’ll see where I placed the class, and how I implemented it. And this present web page is an example of it at work, as it was compiled into the above swf file. I did notice, though, that you have to click on the hmtl page to get it to scroll alone (without the flash also scrolling). This isn’t the precise the behavior I wanted, but it was better than what I had in the first place!

It’s kind of exasperating that Adobe’s included components seem to have this all worked out, apparently without using Javascript. If anyone knows how to do it, please write to me and tell me.

The other issue is the mouse wheel on a mac (apple computer). It apparently has problems giving focus to the flash object instead of the overall web page. Once again, in researching this, I came across several competing solutions, some of them requiring that certain javascript code be implemented by the web page. I implemented a solution that didn’t require this: MacMouseWheel :

import com.pixelbreaker.ui.osx.MacMouseWheel;
MacMouseWheel.setup(stage);

This code was also worked into the above swf, so if you are on a mac, you can use the wheel. Please write to me and let me know if this doesn’t work for you, and what browser you were using. Thanks!

In spite of the above problems, the mouse wheel is really cool! But to choose not to use it is easy, too, as we’ve written the means to turn it off right in the class! From the outside, it’s

scrollbar.wheelEnabled = false;

I hope you enjoyed this tutorial, and that it helps inspire you in your own Flash creations! Stay tuned! –Jody Hall

Bonus Page 1: Making the scrolling smoother

There is yet another nice touch we might add to the scrollbar’s behavior. Many times in Flash it makes a nice touch if, instead of merely moving something to a place, you “tween” it there. So far, the motion of the content is strictly linear. So let’s tween it instead!

First of all, go to http://www.greensock.com/tweenlite/  and download the TweenLite AS3 classes. Copy the com folder to the same folder as your fla file (easiest), or otherwise make sure it resides in a classpath folder (best). See my tutorial on Flash: The Big Picture for a fuller explanation of how classpaths work. The reason the second alternative is best is because you can make all of your projects use the same set of class files instead of copying them around to every new project.

For the sake of saving a little bandwidth, the provided ZIP file above does not include the necessary greensock classes. You’ll want to get these files from the source anyway, as their author, Jack Doyle, updates them frequently. Anyway, since there is already a com folder in our project, all you have to do is download the greensock classes, then just copy the greensock folder to the inside of our existing com folder. In any case, I highly recommend you download these classes, put them in a classpath, and learn how to use them. Have them on tap for all of your projects, not just this one. They are that handy (Note: having these classes in your classpath doesn’t mean they compile into all your projects, it just means that they are available to be imported).

If you attempted to compile the project as is, and got an error message, you just need to place these greensock classes where they should go, and then try again.

That being done, now let’s turn our attention to the VerticalScrollbar class. There are a couple of lines in this file that get repeated in several places. Let’s consider one line in particular, the one that moves the content. It reads:

if(content && window) content.y = window.y – (_scrollPercent * contentRange);

…and it appears in four places, specifically lines 67, 84, 101, and 118. We can refactor it by placing it in a function of its own, and then just calling the function from those other four places. So add the following function toward the end of the class, right after the last existing private function, but before the first public function. (Note: it’s not that the order of the functions makes any difference to our programming, but in the interest of staying organized, it’s not a bad idea to keep public and private functions grouped separately).

private function moveContent():void {
	if(content && window) content.y = window.y - (_scrollPercent * contentRange);
}

Next, go the four places in the code where that line appeared, and replace it with the function call instead:

moveContent();

This will make the class behave identically to how it did before, but now the line that moves the content appears in only one place. When we change the way the content moves, we need only do it in this one place instead of four other places. This not only makes modifying it easier (like we’re about to do), but also reduces the chance of errors.

Now all we need to do is to import the TweenLite class, and then change that one line in the moveContent function so that it uses TweenLite instead. So add this to the list of the class’s imports:

import com.greensock.TweenLite;

Next, let’s change the line that moves the content (this line is in the moveContent function and should replace the line that’s there currently):

if (content && window) TweenLite.to(content, 0.5, { y:window.y – (_scrollPercent * contentRange) } );

This line is really not as complicated as it might at first look. The y destination is still determined in exactly the same way, using the exact same calculation. But instead of directly assigning this calculated number to the y property of the content, this line tells the content to tween there instead. The functions that call moveContent() are all functions that happen with continuous action. Consequently, this line is always being called continually. Fortunately, the TweenLite class automatically handles tween overwriting. Each time a new tween is created, it overwrites and cancels out the previous one. The result is that at any given time, only the very last tween created is still in effect. The result is that the content continues to tween to its most recent destination, for another half second, every time you stop scrolling.

Save the file and compile the project again. You should get noticeably smoother movement when the content moves up and down in the window. Here’s how the swf behaves now (check out the content’s movement):

Tags: , ,

Leave a Comment

You must be logged in to post a comment.