Part 3: Adding the scrollbar

The last addition to our script, and certainly the most noteworthy is the scrollbar. With it, we can properly emulate an everyday operating system scroller. Unsurprisingly, it is also the toughest part yet, so buckle your seatbelts, it's going to be one hell of a ride.

A scrollbar requires several elements in order to work. First, it must be draggable within certain limits. Its position will dictate the scroll of our text box.

It also must scale depending on the length of the text; this will give the viewer an idea of just how long the text is. For example, if the text is 2 times longer than the view area, then the scrollbar should be half its maximum height. People instinctively know that a tiny scrollbar means lots and lots of text.

Finally, it must move when the user clicks the up and down buttons. Let's see how each of these elements can be accomplished in Flash.

In order to drag something, we use the startDrag() action. This enables us to drag a clip around with the mouse within certain bounds that can be specified. We can then stop the dragging with stopDrag().

We'll need to know the position of the scrollbar in order to scroll the text; this means we'll have to play with clip properties. This may be your first time using clip properties, so I'll give you a quick crash course on reading and writing them.

Remember when we used daTextBox.scroll in order to retrieve and set the scroll of our textbox? Clip properties work in much the same way. In order to read a clip's property, you can use the following syntax:

value = clipName._property;

To set a property, use this:

clipName._property = value;

There are several properties you can use, and all start with an underscore (_). The most common are _x, _y, _xscale, _yscale, _width, _height, _rotation and _alpha. I'm sure you can guess what most of these do.

Every property you read or write is calculated depending on the position of the center cross of the clip in question. This may be a bit hard to fully understand, so I made a movie that allows you to play with the _xscale of three similar rectangles, where each of their center crosses are placed differently:

Now do you understand what I mean? The movieclips are scaled in relation to the location of their center cross, and once again, this is very important to understand.

The _x and _y properties go one step further. Say you have a setup where a child clip is inside of a parent clip. If you get the child's y position, you will actually get the y distance between the parent clip's center cross and the child clip's center cross.

Scaling the scrollbar and moving it with the arrow buttons also involves playing with properties.

Okay, enough with the blablabla-ing, let's get to work.

Create a new movie clip and name it scrollbarMC. Place a graphic of a scrollbar in it. Make sure the top of the scrollbar is on the center cross. This way, when we'll scale the movie clip, its bottom side will move up, which is what we want.

Place your scrollbar inside of containerMC, in between the up and down arrows. Name it "scrollbar" in the instance properties panel. You will need to make sure that the scrollbar fits exactly in between of the two arrows, like so:

In order to fit the scrollbar in between the two buttons, do not scale it from controllerMC; otherwise, we'll have problems when we'll scale the scrollbar in actionscript. Instead, go into scrollbarMC itself and change the height of the scrollbar from there.

You will need to place the scrollbar at a 0 y position in controllerMC. If you look at the screenshot, you will see that both containerMC and scrollbarMC's crosses are aligned horizontally. If we read scrollbarMC's y position in actionscript, we'll see that it's 0, and this will help us later on.

Okay, it's time for some ActionScript already! Once again, old code is grayed out.

onClipEvent (load) {
    this.loadVariables("text.txt");
    scrolling = 0;
    frameCounter = 1;
    speedFactor = 3;
    numLines = 7;
    resetOnNewFile = true;
    origHeight = scrollbar._height;
    origX = scrollbar._x;
    refreshRate = 12;
    refreshCounter = 0;
    refreshlastMaxscroll = 0;
    loaded = false;
    function refreshScrollBar() {
        daTextBox.scroll = resetOnNewFile ? 1 : Math.min(daTextBox.maxscroll, daTextBox.scroll);
        var totalLines = numLines+daTextBox.maxscroll-1;
        scrollbar._yscale = 100*(numLines)/totalLines;
        deltaHeight = origHeight-scrollbar._height;
        lineHeight = deltaHeight/(daTextBox.maxScroll-1);
        updateScrollBarPos();
    }
    function updateScrollBarPos() {
        scrollbar._y = lineHeight*(daTextBox.scroll-1);
    }
}
onClipEvent (enterFrame) {
    if (loaded) {
        if (refreshCounter%refreshRate == 0 && daTextBox.maxscroll != refreshLastMaxScroll) {
            refreshScrollBar();
            refreshLastMaxScroll = daTextBox.maxscroll;
            refreshCounter = 0;
        }
        refreshCounter++;
    }
    if (frameCounter%speedFactor == 0) {
        if (scrolling == "up" && daTextBox.scroll>1) {
            daTextBox.scroll--;
            updateScrollBarPos();
        } else if (scrolling == "down" && daTextBox.scroll<daTextBox.maxscroll) {
            daTextBox.scroll++;
            updateScrollBarPos();
        }
        frameCounter = 0;
    }
    frameCounter++;
}
onClipEvent (mouseDown) {
    if (up.hitTest(_root._xmouse, _root._ymouse)) {
        scrolling = "up";
        frameCounter = speedFactor;
        up.gotoAndStop(2);
    }
    if (down.hitTest(_root._xmouse, _root._ymouse)) {
        scrolling = "down";
        frameCounter = speedFactor;
        down.gotoAndStop(2);
    }
    if (scrollbar.hitTest(_root._xmouse, _root._ymouse)) {
        scrollbar.startDrag(0, origX, deltaHeight, origX);
        scrolling = "scrollbar";
    }
    updateAfterEvent();
}
onClipEvent (mouseUp) {
    scrolling = 0;
    up.gotoAndStop(1);
    down.gotoAndStop(1);
    stopDrag();
    updateAfterEvent();
}
onClipEvent (mouseMove) {
    if (scrolling == "scrollbar") {
        daTextBox.scroll = Math.round((scrollbar._y)/lineHeight+1);
    }
    updateAfterEvent();
}
onClipEvent (data) {
    loaded = true;
}

Oh, the humanity! Take a deep breath, make yourself a coffee, and get ready for some serious explanations.

In order to decipher this, we need to look at how the script is executed chronologically. The load clip event is the first one that's fired. We've added a few variable definitions. The numLines variable is the only one you'll need to change; it defines the visible number of lines in the textbox. The resetOnNewFile variable is used to indicate the behaviour of the scroller when we load a new file: if false, the scroller stays there, and if true, the scroller resets to the beginning of the text. We also store the original height and x position of the scrollbar, which we'll need later on.

We also added two custom functions. The load clip event is an ideal place for custom functions, since it only gets fired once. We'll talk about what these two functions do later on.

Once the text file is loaded, we need to change the height of the scrollbar according to the length of the text, and so we use the custom refreshScrollBar() function.

Let's take a look at the initScrollBar function line by line:

1. function refreshScrollBar(){
 
2.  daTextBox.scroll = resetOnNewFile ? 1 : Math.min(daTextBox.maxscroll, daTextBox.scroll);

3.  var totalLines = numLines + daTextBox.maxscroll - 1;
4.  scrollbar._yscale = 100*(numLines)/totalLines;
5.  deltaHeight = origHeight - scrollbar._height;
6.  lineHeight = deltaHeight/(daTextBox.maxScroll - 1);
7.  updateScrollBarPos();
 
8. }

The second line deals with loading multiple text files. Depending on the value of resetOnNewFile, the movie will either reset the scroller when it encounters a new text file, or it will stay where it used to be. This line uses a shorthand if function. The way it works is simple:

valueOfVariable = condition ? valueIfTrue : valueIfFalse;

It is simply a means of saving space when using very simple ifs. The Math.min(daTextBox.maxscroll, daTextBox.scroll) is used to correct a bug in Flash where the maxscroll property of a textbox can be greater than the scroll property. This would mean that the scrollbar would be bigger than the space between the arrows.

Next, in the third line, we simply store the total number of text lines in our scroller in the totalLines variable.

The fourth line does the real job here. It sets the y scale of the scrollbar in proportion to the number of lines of text. Let's see how this works with an example.

Say we have 20 lines of text and a viewable area of 10 lines; hence, the scrollbar should be half of its maximum height. In that case, numLines is 10, and maxscroll is 11, so totalLines = 10 + 11 - 1 = 20. Our _yscale calculation would give us 100*10/20 = 50, and this would set the scrollbar's y scale to 50%. This is exactly what we wanted.

Moving on, on line 5, we define the deltaHeight variable, which is the available space for the drag. That available height is the difference the original height of the scrollbar, and the new, adjusted size.

Now, in order to know where the scrolled text should be in relation to the scrollbar, we need to define a lineHeight variable, which we will reuse later. This lineHeight variable is the amount of pixels by which we should move the scrollbar in order to scroll the text by 1. We finish this off by asking another function to update the scrollbar's position, aptly named updateScrollBarPos(). We'll take a look at this function later.

Well, that was the hard part. The rest is pretty simple, and essentially builds on what we have just seen. In the mouseDown clip event, we have added an action to account for clicks on the scrollbar:

1. if(scrollbar.hitTest(_root._xmouse,_root._ymouse)){
2.     scrollbar.startDrag(0,origX,deltaHeight,origX);
3.     scrolling = "scrollbar";
4. }

The first line is simply a hitTest to ensure that it was indeed the scrollbar that was clicked, much the same way we tested this with the up and down buttons.

If we clicked the scrollbar, then we should start dragging it; hence we use the startDrag() action. The arguments for startDrag() are, in order, top, left, bottom and right; they are the coordinates of the bounds of the imaginary rectangle in which the caller clip can be dragged. Don't worry, that order confuses me too!

Vertically, we should drag from 0 to deltaHeight, which, as mentioned earlier, is the available space between the scrollbar and the up and down arrows. Horizontally, the scrollbar should stay in the same spot, so we drag it from its original X to its original X, essentially locking it in place horizontally. We finish this up by setting the scrolling variable to "scrollbar", so that other parts of the script know we're dragging.

The updating of the text box's scrolling is handled by the mouseMove clip event:

1. onClipEvent (mouseMove){
2.    if(scrolling == "scrollbar"){
3.       daTextBox.scroll = Math.round((scrollbar._y)/lineHeight + 1);
4.    }
5.    updateAfterEvent();
6. }

Yep, that's yet another clip event. The mouse move clip event is fired every single time the mouse is moved. This is very useful if we want to adjust the properties of a clip depending on the mouse position, for example.

On the second line, we check that we are indeed dragging the scrollbar. Otherwise, there's no need to update the text box.

The third line is the real engine of the scrollbar. It sets the scroll of the text box according to the y position of the scrollbar. For example, say our lineHeight is 10, and the y position of the scrollbar is 30. That means that we should scroll by 3 lines of text. Since the scroll property is one-based, we add 1 to to these three lines of text, giving us a scroll property of 4. We surround everything with a Math.round function, which rounds our number to the closest integer, because, you guessed it, the scroll property only allows integer values.

We end this clip event with an updateAfterEvent(). Using updateAfterEvent() is particularly useful with mouseMove, as the screen will get updated every time the mouse is moved. This is great if you have drag actions, which are normally jerky, but will be silky smooth if you add an updateAfterEvent() in a mouseMove clip event.

In our mouseUp clip event, we added a stopDrag() action so that the scrollbar stops dragging when the user releases the mouse.

All that we need now is a way to move the scrollbar when we click the up or down buttons. You'll notice that in our enterFrame clip event, we added a call to the updateScrollBarPos() custom function if we are scrolling up or down. Let's dig inside this function:

function updateScrollBarPos(){
 scrollbar._y = lineHeight*(daTextBox.scroll - 1);
}

The reasoning on this one is the exact inverse of what we had with the mouseMove clip event earlier. Instead of figuring out the scroll of the text box from the y position of the scrollbar, we do the inverse, figuring out the y position of the scrollbar from the scroll of the textbox. Both equations are mathematically equivalent.

That's it?

Yes, that's it. You just deciphered a hefty 100 lines of code using only your wits and willpower. That's right, you, who always said "I don't understand coding, I think I'm thickheaded". Now go into your bathroom, look at yourself in the mirror, and smile. Now look at your watch. It's 4 AM.

Unbeknownst to you, you just learned the fundamental elements of highly complex interactivity. You now know 6 of the 7 clip events, how to manipulate properties, custom functions, loading variables, and how to create un-button buttons. Next thing you'll know, you'll change your username to Flashguru2 on the boards.

I'm happy if this tutorial helped you in any way. If you have any suggestions for upcoming tutorials, or have any questions/technical problems, please e-mail me at . If you want to be notified of upcoming tutorials, simply write a blank e-mail to with "subscribe" in the subject line.

Until next time, happy flashing!