Advertisement
  1. Code
  2. JavaScript

AS3 101: Events - Basix

Scroll to top
Read Time: 37 min
This post is part of a series called AS3 101.
AS3 101: OOP - Introducing Design Patterns
AS3 101: Quick Tip - Dispatching Events Without Extending EventDispatcher

For this chapter of AS3 101, we will be diving into the mechanics of the Flash event system. If you've been following along so far, you'll have seen events in use, dating all the way back to the first episode of the series. The editor and I felt that it was time to write up something to be formally included in the curriculum, so if you've ever seen those lines of code about adding event listeners or dispatching events, and not quite caught on, then this is the tutorial for you.

There already exists an Activetuts+ tutorial on the basics of Events, so the focus of this tutorial will be on dispatching events from your own classes, including creating custom event types and objects.

To be successful with this tutorial, you should feel comfortable with writing and using your own classes in ActionScript 3, as well as feeling secure with using the existing events provided by Flash, such MouseEvent.CLICK or Event.ENTER_FRAME. We will be focusing primarily on dispatching custom events from custom classes.


Preview

We'll spend a lot of time on the theory for this tutorial, but in the end we'll build a simple slider control that dispatches its own events:


Step 1: Why Use Event Dispatching?

This example is actually a rather simple example of why you'd want to dispatch your own events. When you write your own classes, you are ideally keeping them in their own black boxes and encapsulated. But you still need to have the various objects interoperate in order to create a useful program.

The event model provided by ActionScript 3 is a rather good and convenient way to facilitate communication between your classes, while maintaining a separation of responsibilities in your classes. So if we write our own custom class, such as the ActiveSlider class showcases above, we have a need to enable other objects to be aware of when the slider changes its value. If the slider can dispatch its own change event, then other objects that need to know that information can easily subscribe and be notified.

Personally, I find the need to dispatch my own events in my classes so common that my class template sets up each new class with the boilerplate it needs to be able to do so. As you learn to keep objects discrete, you'll turn to event dispatching as the most common technique to do so.


Step 2: How to Dispatch Your Own Events

I have good news: dispatching your own events is actually very simple. It is one of the core tenets of ActionScript 3, built in to the Flash Player, and as such there is only one thing you need to do in order to gain the ability to dispatch events. This one thing is:

Extend EventDispatcher

That's it: when writing your class, use the line:

1
2
public class MyClass extends EventDispatcher {

Of course, you need to import EventDispatcher, which is in the flash.events package. You will most likely need other classes in the package, so it might be most convenient to simply import the package with a wildcard.

1
2
import flash.events.*;

Step 3: Dispatch

Now you're set up to dispatch an event. All you need to do now is call a method provided by EventDispatcher named dispatchEvent. Easy to grasp, no?

When you call dispatchEvent, you must provide at least one argument, an Event object. All built-in Event objects are in the flash.events package, so here's where that wildcard import comes in handy. Each type of Event object will have its own requirements, but most often you simply need to pass it just one argument, as well. This argument is the event type, which is a String that names the event, like "click" or "complete". These are more commonly written as MouseEvent.CLICK or Event.COMPLETE, but the end result is the same; it's an identifier that separates one type of event from another, and allows one Event object to manage multiple event types.

So, putting it all together, if you wanted to dispatch a "complete" event, you could do so like this:

1
2
dispatchEvent(new Event(Event.COMPLETE));

Just drop that line (or one like it) in whichever method it's appropriate in your class. Your class will then utilize its inherited event dispatching system and any listeners will be notified for you. Speaking of listeners, let's take a brief look at those, as well.


Step 4: Listen

Any other object in your application can listen for your custom events now. More good news: this is no different than registering for events for the built-in classes. In the previous step, we set up our hypothetical class to dispatch a COMPLETE event. To listen for that event, we could write this line somewhere else in our program:

1
2
var myObject:MyClass = new MyClass();
3
myObject.addEventListener(Event.COMPLETE, myCompleteHandler);
4
function myCompleteHandler(e:Event):void {
5
    trace("My object completes me.");
6
}

And that's it. This should look familiar to anyone who has hooked up a COMPLETE listener to a Loader, for instance, so I won't dwell any further on this.


Step 5: Where to Dispatch

Where you actually place the dispatchEvent line of code requires some consideration. Typically, it should be the last line of code of the method in which it is written. This is so that any other code that also runs in that method can set properties or otherwise update the internal state of the object. By dispatching after this internal update is complete, the object is in a "clean" state at the time of the dispatch.

Consider, for example, our working example. Let's say the COMPLETE event is all about the processing of some data; a bunch of data so large that it will take several seconds to completely process, so the object's purpose is to handle the processing asynchronously so as not to block the UI. And we're dispatching the COMPLETE event as a way of saying that the data has been processed.

Now suppose that the main method in question looks something like this:

1
2
private function processDataChunk():void {
3
    _data += someDataProcess();
4
    if (done()) {
5
        closeData();
6
    }
7
}

OK, not very realistic, but it illustrates the point. We keep building up the internal data until some other internal logic determines we're done, at which point we then write out some final bits of data to close the operation.

Now, let's add the dispatchEvent call:

1
2
private function processDataChunk():void {
3
    _data += someDataProcess();
4
    if (done()) {
5
        dispatchEvent(new Event(Event.COMPLETE));
6
        closeData();
7
    }
8
}

What's the issue with this approach? Any code that executes within listeners for the COMPLETE event will run before the closeData method is called. Therefore, the state of the dispatcher changes more than once within the span of the processDataChunk method, and is not "stable" until after the closeData call. Yet, we tell all of our listeners that we're complete before that call. This could lead to some hard-to-track-down bugs where one object claims to be COMPLETE but really isn't. The obvious solution is to switch some lines around:

1
2
private function processDataChunk():void {
3
    _data += someDataProcess();
4
    if (done()) {
5
        closeData();
6
        dispatchEvent(new Event(Event.COMPLETE));
7
    }
8
}

Step 6: Custom Events

You're all set up to dispatch your own events. Now, what should you dispatch? There are a few options to consider:

  1. Simply reuse an event object and event type already provided by the Flash Player
  2. Reusing an existing event object, but provide a custom event type
  3. Re-Dispatch an existing event
  4. Create a custom event object
  5. Push vs. pull

This first option we've already seen in our previous examples. We have a need to dispatch an event related to the completion of some process, and as it happens Flash provides an event type (COMPLETE) associated with an event object (Event) that fits our criteria. We have no need to provide additional data with the event. Dispatching an Event.COMPLETE event is all we need.

We'll explore these other options in the forthcoming steps.


Step 7: Custom Event Types

As hinted at back in the "Dispatch" step, event types are simply String identifiers. They can technically be any String you like. It's usually sufficient to make it a single word (like "complete" or "click") or very short phrase (like "ioError" or "keyFocusChange"); it only has to be unique within a given event dispatcher's realm of available events.

In addition, Event objects (including subclasses, like MouseEvent or ProgressEvent) don't really care what event type they are given when instantiated. An EventDispatcher will gladly dispatch events of any type identifier and of any class (as long as it's the Event class or a subclass).

The upshot of this is that you can make up your own event type String, dispatch it, and set up listeners with it, and everything will be just fine. This is helpful when you want to dispatch an event but can't necessarily find a good representation of the nature of the event in the built-in classes.

As an example, you might have a class that acts as a coordinator for several things at once: load some XML, load some images based on the XML data, and create a layout based on the loaded images' sizes, ready for an initial animation, at which point you'd like to dispatch an event. While the COMPLETE event might be suitable, you may feel that a "ready" event more appropriately encapsulates the meaning.

This is as simple as deciding on the String to use, and then using it. Use it both when adding listeners, and when dispatching the event. If the String matches, the event will get to where it needs to go. For example, this is a partial listing of a hypothetical class:

1
2
public class MyClass extends EventDispatcher {
3
4
    // ... A bunch of other stuff not shown.

5
6
    private function determineReadiness():void {
7
        if (everythingIsReady) {
8
            dispatchEvent(new Event("ready"));
9
        }
10
    }
11
12
}

And code from elsewhere in the same program:

1
2
var myObject:MyClass = new MyClass();
3
myObject.addEventListener("ready", onObjectReady);
4
5
function onObjectReady(e:Event):void {
6
    // Do stuff now that it's ready.

7
}

And that would work.

However, it's worth mentioning while we're here that typing in matching Strings all over the place is not a good practice. The possibility for error is high, and the event system won't tell you that you've typed "raedy" instead of "ready". The fact that the event system is flexible and simple to use - just pass it any old String for the event type - is also something of a weakness. Your dispatcher will gladly accept a listener for anything, even a "raedy" event. It doesn't really reconcile what event types are registered against what event types are actually dispatched.

To help prevent this, the standard approach is to simply put the String you want to use in a static constant somewhere, and then never use that literal String again. Only use the constant. Of course, the possibility of typos is just a great as before, but if you're using a READY constant and not the "ready" literal String, a mistype will trigger a compiler warning. You'll be able to correct your error quickly and easily. A mistype with the literal Strings produces no compiler error, nor does it produce a run-time error. The only thing that happens is that the SWF does not seem to work properly, because the event listener doesn't fire.

With this in mind, it's most common to store these constants on the related Event class. We'll get to custom Event classes in just a few steps. But in the situation outlined in this step (i.e., we're reusing an Event class but not an event type), I find it more convenient to simply store that constant on the dispatcher class. So we might choose to do this:

1
2
public class MyClass extends EventDispatcher {
3
4
    public static const READY:String = "ready";
5
6
    // etc.

7
8
    private function determineReadiness():void {
9
        if (everythingIsReady) {
10
            dispatchEvent(new Event(READY));
11
        }
12
    }
13
14
}

And:

1
2
var myObject:MyClass = new MyClass();
3
myObject.addEventListener(READY, onObjectReady);
4
5
function onObjectReady(e:Event):void {
6
    // Do stuff now that it's ready.

7
}

This gives us the safety of storing event types in constants, while not forcing the inconvenience of creating a whole Event class that we don't need. I must stress that this is a stylistic choice, and you may feel free to use this technique or not. I felt it warranted explanation, so that you could make your own informed decision. In either regard, you should definitely store your custom event type Strings in static constants. Where those static constants are defined is up to you.


Step 8: Re-Dispatching Events

There are several times when one class (let's call it ClassX) owns a property that is typed as another class (we'll call that one ClassY), while itself being owned by a third class (how about ClassZ?). ClassX is listening for an event from ClassY, but not only do we want to have ClassX respond to the event, we also want to consider that ClassX should dispatch a similar (or even the same) event so that ClassZ can also take further action.

As a more concrete example, we have a class (this will be "ClassX") that is a sort of data manager. It loads an XML document, parses it, and stores data from the XML in its own properties. So it has an URLLoader object (this would be "ClassY"), and listens for the Event.COMPLETE event for when the XML document is loaded.

Then we have a main document class that owns the data manager (the document class is "ClassZ"). It's coordinating the data loading with other UI elements, so it wants to know when the data is loaded and ready, so it can proceed to create and layout UI elements based on the data.

1
2
public class DataManager extends EventDispatcher {
3
    private var _xmlLoader:URLLoader;
4
    public function DataManager() {
5
        _xmlLoader = new URLLoader(new URLRequest("some.xml"));
6
        _xmlLoader.addEventListener(Event.COMPLETE, onLoadComplete);
7
    }
8
    // ... A bunch of other stuff

9
    private function onLoadComplete(e:Event):void {
10
        // ... XML parsing

11
        // With the XML parsed, we'd like to dispatch another event to signal being done.

12
    }
13
}

We could do this:

1
2
dispatchEvent(new Event(Event.COMPLETE));

But we could also do this:

1
2
dispatchEvent(e);

Here we simply re-dispatch the existing event. Not only do we reuse the event type and the Event class, but we're actually reusing the entire Event object as it was passed in to our own listener.

It's not rocket science, but it is a handy little technique that is surprisingly not so obvious.

"But wait," you must be thinking, "if we redispatched an event that originated from the URLLoader object, wouldn't the target of the event still be _xmlLoader when it gets back out to the document class?" And you would have a very good and thoughtful point, and I would be proud of you for thinking so studiously, but you would be wrong.

A rather magical thing happens when redispatching events. The target property gets set to the current dispatcher. You can find a working example of the code in this step in the download package, entitled redispatch.

Actually, it's not all that magical. When calling dispatchEvent, if the Event object that is passed in already has a target set, then the clone method is called on the Event, creating an identical but discreet copy of the original Event, except for the value contained in target.


Step 9: Custom Event Objects

Everything mentioned so far is something you will want to know. But there will come a point when the best thing to do is to dispatch your own custom event. Not just a custom event type, but a whole custom Event class.

The process for doing this is straightforward, you just need to follow a few steps. We'll discuss these in due course. Note that quite a bit of this is boilerplate code, and you could easily create a template for an Event subclass and change just a few key pieces and be off and running. The general steps, in shortened-as-if-you-knew-what-I-was-talking-about form:

  1. Subclass Event
  2. Call super(...)
  3. Store event types in public static constants
  4. Declare private properties to hold custom data
  5. Create public getters to provide read-only access to the custom info
  6. (Optional) Override the clone method
  7. (Optional) Override the toString method

To explain these process more deeply, we'll get a start on our slider project and create the SliderEvent we will need for that. So we need to start our project before we can write some code, so a quick diversion in the next step, then we'll start writing a custom Event class.


Step 10: Create the Project Structure

We'll keep things pretty simple for this one, but we'll nonetheless create packages for our classes.

Start by creating a folder for the entire project. Mine will be called slider.

Inside of this, create a com folder, and inside of that, an activetuts folder.

Now create two folders inside of activetuts: events and ui. Your final folder structure should look something like this:

  • slider
    • com
      • activetuts
        • events
        • slider

Now back to our Event class.


Step 11: Subclass Event

First, create a new text file in the slider/com/activetuts/events folder, and call it SliderEvent.as. We'll pop in the boilerplate for any class first:

1
2
package com.activetuts.events {
3
4
    public class SliderEvent {
5
6
        public function SliderEvent() {
7
8
        }
9
10
    }
11
12
}

There should be nothing surprising here, and if you have ActionScript templates for your text editor, you shouldn't even have to type that much in.

Now, we'll modify this so that it extends Event.

1
2
package com.activetuts.events {
3
4
    import flash.events.Event;
5
6
    public class SliderEvent extends Event {
7
8
        public function SliderEvent() {
9
10
        }
11
12
    }
13
14
}

As you can see, we simply import the Event class, add extends Event to the class definition.


Step 12: Call super

Our superclass can handle a lot for us, which is great, but we do need to make sure we initialize the superclass properly when we initialize our subclass. We need to set up the constructor with arguments matching those found on Event's constructor, and pass those along with a call to super.

1
2
package com.activetuts.events {
3
4
    import flash.events.Event;
5
6
    public class SliderEvent extends Event {
7
8
        public function SliderEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=true) {
9
            super(type, bubbles, cancelable);
10
        }
11
12
    }
13
14
}

This is, so far, basic subclassing techniques. In fact, depending on your editor's prowess with templates, you may be able to specify a super class when you create the file and have all of this done for you. Flash Builder, for example, is able to do this.


Step 13: Store Event Types in Public Static Constants

Presumably, there will be one or more event types associated with this event class. Just like the COMPLETE event is associated with the Event class, and the CLICK even with the MouseEvent class, our custom event class will likely have custom event types.

This is as simple as writing a line like the following for every event type you would like to add:

1
2
public static const EVENT_TYPE:String = "eventType";

Let's do that now for the SliderEvent class.

1
2
package com.activetuts.events {
3
4
    import flash.events.Event;
5
6
    public class SliderEvent extends Event {
7
8
        public static const CHANGE:String = "sliderChange";
9
10
        public function SliderEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=true) {
11
            super(type, bubbles, cancelable);
12
        }
13
14
    }
15
16
}

We could theoretically use our class now. We can use the SliderEvent in dispatchEvent, and listen for and create events with the SliderEvent.CHANGE event type.

But we won't stop there. There is more to consider. But before we do more code-writing, we need to take another detour into theory.


Step 14: Push vs. Pull

When an event is dispatched, sometimes it's enough to simply know that the event has occurred. For example, most times that you are interested in Event.ENTER_FRAME, Event.COMPLETE, or TimeEvent.TIMER events, you probably just want to know that the event happened. There are other times, though, when you probably want to know more. When listening to MouseEvent.CLICK, you might be interested in whether the Shift key was held down, or the coordinates of the mouse at the time of the click. If you're listening to ProgressEvent.PROGRESS, you will most likely want to know the actual progress of the load; that is, how many bytes have loaded and how many there are to load in total.

The difference between these two methodologies is sometimes known as "push" and "pull." Those terms refer to how the event listener gets data related to the event. If the data is "pushed" then there is data stored within the event object, and in order to get the data the listener merely need to use properties on the event object. If data is to be "pulled," though, generally the event object has very little information contained within - just the necessities: the type, the target, etc. This target, though, is indispensable, as it provides access to the event dispatcher to the event listener, allowing the listener to get the data it needs from the dispatcher.

In other words, you can either push a bunch of data to the listener inside the event object, or you can require the listener to pull the data out of the dispatcher as needed.

The pros and cons of each technique are somewhat balanced, in my opinion, and the path you choose for your event object depends on the situation at hand, and not a little bit on personal preference.

Exhibit A:

  Pros Cons
Push
  • Data is easily accessible in the event object
  • You may be pushing data that is unneeded. Bloating the event object with a bunch of data that is rarely used could lead to memory and/or performance issues.
Pull
  • Very easy to write. You probably don't need a custom Event class to execute a pull event.
  • Very easy to use. If it's just an Event class, the only required argument is the event type.
  • If data commonly pulled out of the dispatcher is expensive to compute and return, you may be taking a hit on performance by requiring the dispatcher to continually hand out that information.
  • Some data might be difficult to pull, e.g. the KeyboardEvent has a keyCode property to detail the key that was pressed to trigger the event.

This might be a good discussion for the comments; I'm sure many of you reading have passionate feelings about which methodology is better. Personally, I try to find the method that works best for the situation.

Having said that, it's worth noting that to this point our SliderEvent class is rather "pull-ish." For the sake of illustration, and because it's not a terrible idea (though I did come up with several of those), we'll continue on with making this an event that pushes data along with it; namely the value of the slider when it was changed.


Step 15: Declare Private Properties to Hold Custom Data

To implement a push event, we need to have a place to store the data being pushed. We'll add a private property for that purpose.

You should still have SliderEvent open (if not... what are you waiting for?). Add the highlighted line:

1
2
package com.activetuts.events {
3
4
    import flash.events.Event;
5
6
    public class SliderEvent extends Event {
7
8
        public static const CHANGE:String = "sliderChange";
9
10
        private var _sliderValue:Number;
11
12
        public function SliderEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=true) {
13
            super(type, bubbles, cancelable);
14
        }
15
16
    }
17
18
}

Next we'll modify the constructor so that we can accept a value parameter, and set the private property with that:

1
2
public function SliderEvent(type:String, sliderValue:Number, bubbles:Boolean=false, cancelable:Boolean=true) {
3
    _sliderValue = sliderValue;
4
    super(type, bubbles, cancelable);
5
}

This way we can easily create the SliderEvent and set up its push data in one line.

Why use private properties? In this case, we want to protect the data. In my opinion the data related to an event is immutable, so long as it's associated with the event. Being able to change the data of an event object is like editing a history textbook. To be fair, this is my opinion and the standard used by Adobe with their built-in classes is to use writable properties (technically they use private properties and public getters and setters, but the end result is the same).


Step 16: Create Public Getters for the Custom Data

So the next step would be to make sure we can access the data being pushed. A private property by itself is not useful to this purpose. Therefore, we need to write a public getter for the _sliderValue property. We will choose to not write a setter, so that the property becomes read-only (as discussed in the last step).

Add this method to the class:

1
2
public function get sliderValue():Number {
3
    return _sliderValue;
4
}

This adds a getter so we can access the sliderValue in property-like fashion. I'm choosing not to add the matching setter. You can add one if you feel it's worthwhile.


Step 17: Override the clone method

I mentioned the clone method a little while ago. You probably won't call clone much yourself, but it's not a bad idea to override the clone method so that your custom event plays nicely with the event system.

Add this method to your class:

1
2
override public function clone():Event {
3
    return new SliderEvent(type, sliderValue, bubbles, cancelable);
4
}

First, notice the signature of this method. We're using override because this method is declared in Event, and we're inheriting it. It also returns an object of type Event. Make sure, when writing your clone override, that you put the correct return type in. It's easy to forget and put the type of your class there, but that will cause an incompatible override error, because the return types need to match.

All we're really doing in the meat of the event is creating a new SliderEvent and passing in the same values that we have stored in the current event object. This creates an identical but discreet copy: a clone.

This is an optional step, but it's a quick win and ensures that your custom event plays well with the rest of the event system.


Step 18: Override the toString method

One last thing, and again this is optional. But it's also very useful as a debug tool, so it usually pays for itself within a few uses.

In case you haven't been told yet, the toString method exists on all objects (it's declared and defined in Object, the über-class from which all other classes inherit, whether you like it or not). It can be called explicitly, but the handy thing is that it get called automatically in a number of cases. For example, when you pass object to the trace function, any object that is not already a String will have toString called on it to make sure it is formatted nicely for the Output panel. It even gets called when working with Objects together with Strings, as with concatenation. For example, if you write this:

1
2
"The answer to life, the universe, and everything is " + 42;

ActionScript is smart enough to convert 42 into a String representation of the Number before concatenating the String. Trying to add a String and a Number is bad news, but converting a Number to a String and then concatenating it with another String is just fine.

So when you write your own classes, you can provide a toString method, which takes no arguments and returns a String, and return whatever String you like.

In the case of Event objects, Adobe helpfully provides a formatToString method to help all Events look similar when traced. We'll use it in the method we're about to add to our class:

1
2
override public function toString():String {
3
    return formatToString("SliderValue", "type", "sliderValue");
4
}

First, note the method signature. Again, it's an override so we have that keyword. It's public, it takes no parameters, and returns a String (which should be obvious).

Next, note the single line in the method body. We call formatToString, which is defined in Event, so it's easy to use. The first argument we pass to it is the String name of the class. After that, the arguments are open-ended. You can pass in one, 15, or none. We're passing in two. No matter how many you pass in, they should all be Strings, and they should match property names on your class. "type" is defined by Event, but "sliderValue" is defined by our own class. Either way, what happens is that the name of the property is printed, followed by an equals sign, which is followed by the actual value of that property. In short, it will end up looking like this:

1
2
[language="text"]
3
[SliderValue type="sliderChange" sliderValue=0.34146341463414637]

This is entirely non-functional but very useful. It can provide a quick glimpse into the event when things aren't working the way you think they should.


Step 19: Building the Slider

At this point, we've been through the key concept of this tutorial: writing a custom Event class. But we really need to put it to the test. We'll spend the remainder of our time building the simple slider application that was previewed at the beginning of the tutorial.

We already have a project folder structure; we just need a few more files. We'll start with the FLA file.

Create a new Flash file (ActionScript 3.0, of course) and save it as ActiveSlider.fla in the root of your project folder. I'm going to assume that you don't need step-by-step details on how to put this simple FLA together, and instead I'll lay out the key elements. You can use the FLA file found in the start folder of the download package for reference, as well, or even just copy that FLA to your project folder and call this step done.

There are three main objects on the stage.

  1. The slider track. This is a long, narrow strip that indicates to where the slider can be moved. The slider moves "in" the track.
    • Needs to be a Movie Clip
    • For easiest math, should have artwork arranged so that the registration point is at the top left corner
    • Name it track_mc
    • Place in the upper center; it should take up most of the width of the stage and have space below it.
  2. The slide grip. This is a button-sized element that indicates the current position of the slider. It's the piece that moves along the track and responds to the mouse.
    • Needs to be a Movie Clip.
    • Again, for math, should have the registration point at the top left
    • Name it grip_mc
    • Place it over the track, so that it is vertically centered with the track, and somewhere within the left and right ends of the track
    • It should be stacked on top of the track, so that the grip obscures the track (put it on a layer higher)
  3. The output field. This is a text field that, for our own demonstration purposes, displays the current value of the slider.
    • Needs to be a dynamic Text Field.
    • Name it output_tf
    • Fonts are inconsequential; set it to whatever you like and embed as necessary
    • Place it in the lower portion of the stage, so that it doesn't conflict with the space required by the slider.
The stage of our FLAThe stage of our FLAThe stage of our FLA

Other than hooking up the document class, which we'll write in two steps, the FLA is ready for business.


Step 20: The ActiveSlider Class

The main UI class with which we'll work is the ActiveSlider class. This will extend EventDispatcher, target the two Movie Clips on the stage, and set up mouse interactivity for slider behavior. Most exciting of all, it will dispatch a SliderEvent.

Start by making a new class file called ActiveSlider.as in the com/activetuts/slider folder of your project. This class isn't too intense (at least, not for our purposes here. A slider class could get much more involved), and I'll just present the code in full and discuss as we go:

1
2
package com.activetuts.slider {
3
4
    import flash.display.*;
5
    import flash.events.*;
6
    import flash.geom.*;

Nothing exciting, just setting up the package and imports.

1
2
    public class ActiveSlider extends EventDispatcher {
3
4
        private var _track:Sprite;
5
        private var _grip:Sprite;
6
        private var _grabOffset:Point;

We'll need these three properties. The first two keep track of the Sprites (or MovieClips) that make up the slider. The third is used while dragging the slider grip; it helps keep the grip's position offset from the mouse by an amount relative to where the grip was clicked in the first place.

1
2
        public function ActiveSlider(track:Sprite, grip:Sprite) {
3
            _track = track;
4
            _grip = grip;
5
            if (_grip.parent != _track.parent) {
6
                throw new Error("The track and the grip Sprites are not in the same container.")
7
            }
8
9
            _grip.addEventListener(MouseEvent.MOUSE_DOWN, onDown);
10
11
            if (_grip.stage) {
12
                addStageListener();
13
            } else {
14
                _grip.addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
15
            }
16
        }

This is the constructor. It accepts two Sprite arguments, which get passed on to the first two of those properties for storage. It then does a simple check to make sure the two Sprites are in the same coordinate space by checking that their parent properties reference the same object. If they don't, then our math for placing the grip might be unreliable, so we throw an error as a way of alerting the developer. The rest of the constructor is devoted to adding two event listeners. The first is a MOUSE_DOWN event and straight forward. But the second is trying to add a MOUSE_UP event to the stage, which might or might not exist depending on whether the grip Sprite is on the display list or not. The next two methods might make this a little clearer:

1
2
        private function onAddedToStage(e:Event):void {
3
            _grip.removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
4
            addStageListener();
5
        }
6
7
        private function addStageListener():void {
8
            _grip.stage.addEventListener(MouseEvent.MOUSE_UP,   onUp);
9
        }

The onAddedToStage method is an event listener for the ADDED_TO_STAGE event, which was set up in the constructor, but only if the grip Sprite did not already have a reference to the stage. The addStageListener method simply adds the MOUSE_UP event listener to the stage. So, if there is a stage reference in the constructor, we call addStageListener directly. If there is not a stage reference, we set up the ADDED_TO_STAGE event, and when the grip is added to the display list, and thus has a stage reference, the onAddedToStage method fires which then in turn calls addStageListener. It also removes the ADDED_TO_STAGE event listener, because we only need to do this once.

1
2
        private function onDown(e:MouseEvent):void {
3
            _grabOffset = new Point(e.localX, e.localY);
4
            _grip.addEventListener(Event.ENTER_FRAME, onFrame);
5
        }
6
7
        private function onUp(e:MouseEvent):void {
8
            _grip.removeEventListener(Event.ENTER_FRAME, onFrame);
9
        }

Next we have our two mouse event listeners. In onDown, the key line is to then add an ENTER_FRAME event listener. In onUp, we remove that listener. Also in onDown, we make note of where on the grip the mouse click actually happened, and store that in _grabOffset. This will play into our onFrame method next.

1
2
        private function onFrame(e:Event):void {
3
            _grip.x = _grip.parent.mouseX - _grabOffset.x;
4
5
            var gripBounds = _grip.getBounds(_grip.parent);
6
            var trackBounds = _track.getBounds(_grip.parent);
7
8
            if (gripBounds.x < trackBounds.x) {
9
                _grip.x = _track.x;
10
            } else if (gripBounds.right > trackBounds.right) {
11
                _grip.x = trackBounds.right - gripBounds.width
12
            }
13
14
            trace(this.value);
15
        }

This is the main meat of our slider logic. This method fires repeated on an ENTER_FRAME event, but only when the mouse has been pressed down on the grip, and only for as long as the mouse remains pressed.

First, we set the grip's x property to match the mouse position, only we need to offset it based on the original mouse position, so that it moves smoothly and doesn't jump so that its left edge is at the mouse.

The next two lines calculate the bounding rectangles of both the grip and track Sprites. We'll use these rectangles in the upcoming math, so we'll keep things tidier by pre-calculating the rectangles and storing them in variables.

Then there is the if block. This just constrains our slider to within the track. It's a simple check to see if the x of the grip, as calculated in the first line of the method, is lower than (to the left of) the x of the track. If it is, it would be too far, so we need to move the grip to that minimum value. Similarly, we check to see if the grip's right edge is greater than (to the right of) the track's right edge, and if so we need to reel it back in to that maximum value.

Finally, we have a reliable grip position, and for now we just trace the current slider value, which is calculated in the final bit of code for the class:

1
2
        private function get value():Number {
3
            return (_grip.x - _track.x) / (_track.width - _grip.width);
4
        }
5
6
    }
7
8
}

This is a simple getter, although the math being used for the return value might be a little confusing. It determines the current grip position as a percentage of the range of motion of the grip. It would be more obvious if it were just this:

1
2
return _grip.x / _track.width;

...which is reasonable but doesn't take into account that the grip doesn't actually travel the entire width of the track. It goes all the way to the left edge, but only as far right as the right edge of the track minus the width of the grip. So this is more accurate:

1
2
return _grip.x / (_track.width - _grip.width);

This makes the width by which we divide the range of motion. However, there may still be a gotcha in that the track may be offset to the left or right from its container, meaning that the values we get aren't quite right. We need to neutralize that by subtracting the x position of the track from the grip's x, and we end up with this:

1
2
return (_grip.x - _track.x) / (_track.width - _grip.width);

For reference, here is the complete class, with my ramblings left out:

1
2
package com.activetuts.slider {
3
4
    import flash.display.*;
5
    import flash.events.*;
6
    import flash.geom.*;
7
8
    public class ActiveSlider extends EventDispatcher {
9
10
        private var _track:Sprite;
11
        private var _grip:Sprite;
12
        private var _grabOffset:Point;
13
14
        public function ActiveSlider(track:Sprite, grip:Sprite) {
15
            _track = track;
16
            _grip = grip;
17
            if (_grip.parent != _track.parent) {
18
                throw new Error("The track and the grip Sprites are not in the same container.")
19
            }
20
21
            _grip.addEventListener(MouseEvent.MOUSE_DOWN, onDown);
22
23
            if (_grip.stage) {
24
                addStageListener();
25
            } else {
26
                _grip.addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
27
            }
28
        }
29
30
        private function onAddedToStage(e:Event):void {
31
            _grip.removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
32
            addStageListener();
33
        }
34
35
        private function addStageListener():void {
36
            _grip.stage.addEventListener(MouseEvent.MOUSE_UP, onUp);
37
        }
38
39
        private function onDown(e:MouseEvent):void {
40
            _grabOffset = new Point(e.localX, e.localY);
41
            _grip.addEventListener(Event.ENTER_FRAME, onFrame);
42
        }
43
44
        private function onUp(e:MouseEvent):void {
45
            _grip.removeEventListener(Event.ENTER_FRAME, onFrame);
46
        }
47
48
        private function onFrame(e:Event):void {
49
            _grip.x = _grip.parent.mouseX - _grabOffset.x;
50
51
            var gripBounds = _grip.getBounds(_grip.parent);
52
            var trackBounds = _track.getBounds(_grip.parent);
53
54
            if (gripBounds.x < trackBounds.x) {
55
                _grip.x = _track.x;
56
            } else if (gripBounds.right > trackBounds.right) {
57
                _grip.x = trackBounds.right - _grip.width
58
            }
59
60
            trace(this.value);
61
        }
62
63
        private function get value():Number {
64
            return (_grip.x - _track.x) / ((_track.width - _grip.width) - _track.x);
65
        }
66
67
    }
68
69
}

We have not added in our SliderEvent class yet; we'll take a separate step to do that. But first, we need our document class so we can actually use the ActiveSlider.


Step 21: The Document Class

We need one more file to make it work: our document class. Make a new class file named SliderDemo in the project root folder. Add in the following code:

1
2
package  {
3
4
    import flash.display.*;
5
    import flash.events.*;
6
    import com.activetuts.slider.ActiveSlider;
7
8
    public class SliderDemo extends Sprite {
9
10
        private var _slider:ActiveSlider;
11
12
        public function SliderDemo() {
13
            _slider = new ActiveSlider(track_mc, grip_mc);
14
        }
15
16
    }
17
18
}

This is much simpler that our ActiveSlider class. It really just sets up an ActiveSlider into the property named _slider.

Go back to the FLA file, and enter SliderDemo into the document class field, and you should be able to try this out. The slider should be able to move back and forth, and constrain itself to the width of the track.

Now for one last task. We need to dispatch, and listen for, a SliderEvent.CHANGE event. We'll do that next.


Step 22: Dispatching the SliderEvent

Go back to the ActiveSlider class, and make a single-line change to the onFrame method:

1
2
private function onFrame(e:Event):void {
3
    _grip.x = _grip.parent.mouseX - _grabOffset.x;
4
5
    var gripBounds = _grip.getBounds(_grip.parent);
6
    var trackBounds = _track.getBounds(_grip.parent);
7
8
    if (gripBounds.x < trackBounds.x) {
9
        _grip.x = _track.x;
10
    } else if (gripBounds.right > trackBounds.right) {
11
        _grip.x = trackBounds.right - _grip.width
12
    }
13
14
    dispatchEvent(new SliderEvent(SliderEvent.CHANGE, this.value));
15
}

We've removed the trace and replaced it with a real, live event dispatch. For this to work, though, we need to import the SliderEvent class:

1
2
package com.activetuts.slider {
3
4
    import flash.display.*;
5
    import flash.events.*;
6
    import flash.geom.*;
7
    import com.activetuts.events.SliderEvent;

Step 23: Listening For the SliderEvent

Finally, go back to the SliderDemo class, and change it so it listens for and reacts to the SliderEvent:

1
2
package  {
3
4
    import flash.display.*;
5
    import flash.events.*;
6
    import com.activetuts.slider.ActiveSlider;
7
    import com.activetuts.events.SliderEvent;
8
9
    public class SliderDemo extends Sprite {
10
11
        private var _slider:ActiveSlider;
12
13
        public function SliderDemo() {
14
            _slider = new ActiveSlider(track_mc, grip_mc);
15
            _slider.addEventListener(SliderEvent.CHANGE, onSliderChange);
16
        }
17
18
        private function onSliderChange(e:SliderEvent):void {
19
            trace(e);
20
            output_tf.text = e.sliderValue.toString();
21
        }
22
23
    }
24
25
}

We're again importing the SliderEvent class, and after creating the ActiveSlider, adding a listener called onSliderChange to the slider. That method is the biggest addition, but still is a regular event listener. It's much like any other event listener, only we're making sure to type the event argument as SliderEvent, because that's what we're getting.

The first line of code is a bit superfluous, but I wanted to see what happens when you trace our SliderEvent object. You'll see, when you run this, a typical-for-Flash formatting of the event object.

The event object represented as a StringThe event object represented as a StringThe event object represented as a String

The second line does what we were after to begin with. It simply grabs the sliderValue property, turns it into a String, then sticks that String into the TextField on the stage, so we can see the slider value in the movie.


Step 24: Wrapping Up

When you start rolling your own custom events, you start working with ActionScript 3 the way it was meant to be used. Events help you decouple classes from each other, and a well structured event flow in your application can really make the difference between something that is easy to work with and something that is buggy and temperamental. With this (theoretically) final instalment of AS3 101, you should be well on your way to becoming a ninja.

Thanks for reading, and I'll see you back here on Activetuts+ before you know it!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.