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!