Tutorials, extensions, and source files for ActionScript, Flash, and other Adobe products.

 

Dealing With Flash Button Event Capturing

Flash: Flash MX, Flash MX 2004, Flash 8

ActionScript: 1.0, 2.0

Source Files (Flash MX 2004):

Introduction

One of the first issues to come to terms with when dealing with basic Flash button events is how Flash handles event propagation, or the transfer of a button event such as onPress to multiple, nested instances when more than one are suitable for receiving the same event. And coming to terms here means understanding that Flash doesn't propagate these events. In other words, you can've have a button event like onPress work on a movie clip as well as for a movie clip nested within it. This can make many situations involving button events difficult to handle in Flash. This tutorial will provide techniques for working around this issue.

Button Events

First we should examine what exactly is meant by button events. These are events that, originally, were designed to work with Flash buttons and involve interaction with the user's mouse. They include the following:

Event
onRollOver
onRollOut
onDragOver
onDragOut
onPress
onRelease
onReleaseOutside

As of Flash MX, these events are also acceptable for use with movie clips. Since this tutorial is focusing on button events when dealing with nested clips, it will be assumed that all instances involved are movie clip instances and not button instances.

Assigning an onPress event handler (function) to a movie clip instance named box_mc would result in that handler being called when the user moved his mouse over the box_mc and pressed the left mouse button.

box_mc.onPress = function(){
	trace("box_mc was just pressed");
}

In it's basic form, there is no problem with this and with most cases it should work as expected. The problem comes when you want to make use of button events for not just one such movie clip, but for a movie clip and child instances within it.

Event Capturing and Propagation

Flash uses event capturing when handling button events coming from the user. What that means is Flash will look for the first parent instance with any kind of button event handler assigned to it and use that as the starting point for event handling. Since Flash doesn't support event propagation, it also means the first parent instance is also the ending point.

Event propagation is the continuance of an event from one instance to its parent or a child when the event applies to both. With propagation in event capturing, clicking on a child movie clip would invoke onPress first for the child's parent movie clip followed by the child itself. The parent is first to "capture" the event. When done, the parent "propagates" that event down to each child to which the event also applies.

event capturing
Captured events start with parent and propagate to children

Because Flash doesn't support propagation in this manner, when clicking on the child instance, if the parent instance has any button events at all, even if different from that being used by the child, it will capture those events and prevent them from being propagated to the child. So assigning any button event handler to a parent instance will effectively break all button event handlers used in any child instance preventing them from ever being called.

event capturing blocked
Flash event capturing stops propagation

What makes button events work at all for child instances in Flash is the fact that Flash only causes capturing to occur when a button event handler is present for an instance instead of restricting it to the first instance encountered. So if the child movie clip has an onPress event and its parents, immediate or otherwise, do not have any button event handlers assigned to them, that child movie clip will receive the onPress event without problem.

Consider the box_mc movie clip. we can put a plate_mc movie clip within box_mc and assign it an onPress event.

box_mc.onPress = function(){
	trace("box_mc was just pressed");
}
box_mc.plate_mc.onPress = function(){
	trace("plate_mc was just pressed");// never called
}
In source files, see: box_broken.fla

The onPress event for plate_mc, however, will never be invoked. This is because box_mc, a parent instance of plate_mc, captured the event and prevented propagation. Even if box_mc had an onRelease event handler assigned to it instead of an onPress, the problem would still persist. By conventional means, the only way for the onPress for plate_mc to work is if box_mc had no button event handlers assigned for it at all. There is no way to force Flash to propagate events from a parent instance to its children.

Boxes are, in fact, good metaphores for movie clips. Think about packing up for a move, placing all your fine china (or plastic) in cardboard boxes. When you whip out your sharpie to write on the box, you can be assured that where ever you write on that box, you'll just be writing on that box and not on any of the plates inside. The same applies to button events like onPress and movie clips. If you have an onPress on a movie clip containing another movie clip with an onPress, you can click on that first parent movie clip and not be able to reach that child movie clip at all. The only way to reach the inner child movie clip is to open the box. For movie clips that means removing all button events.

Event Bubbling

Event bubbling is basically the opposite of event capturing. With event capturing, your events start with the parent instances as they capture events which eventually propagate down to the child instances (if allowed). With event bubbling, events start with the child instances and "bubbles up" to parent instacnces. Events with event bubbling simply start at the opposite end of the hierarchy.

event bubbling
Event bubbling starts with child

The workarounds discussed to circumvent Flash's default capturing behavior will allow you to use either method of event handling. Which to use will revolve around personal preference.

Workaround 1: Delegation

One workaround for avoiding parent event capturing in Flash is to delegate a parent instance's events to its children. This puts the responsibility of handling a parent events to the children of the parent instance preventing those events from being blocked. For this to work, the parent instance would need not to have any button events associated with it. Only the children of the parent have the events. With the box and plate example, you would have something similar to the following:

box_mc.plate_mc.onPress = function(){
	box_mc.onPress();
	trace("plate_mc was just pressed");
}

The only problem with this is that if box_mc has an onPress event handler, it will capture the event before it reaches plate_mc. Instead, onPress for box_mc would need to be renamed to something else. For example:

box_mc.plate_mc.onPress = function(){
	box_mc.onPressHandler();
	trace("plate_mc was just pressed");
}

Now, plate_mc will receive the event and be able to handle it as needed. Because the onPress for box_mc has been delegated to plate_mc, this time in the form of a function called onPressHandler, it too will be able to receive the event, but as a result of a call from plate_mc's onPress event handler, not from intercepting the event itself.

There are a couple of additional considerations when using this technique. First, you will need to make sure that all child instances for a parent are given an event handler for a delegated event, even if those instances do not require their own events. This is to assure that the parent movie clip receives the event for every part of itself. This may also require that shapes within the parent instance be converted into movie clips to carry that event. Without that, the parent movie clip would have dead areas that won't react properly. For the box example, the shapes used to represent the front and back of the box will need to be converted to movie clips and given events that will handle box_mc.onPressHandler.

In source files, see: box_delegated.fla

Also, you'll want to be sure that the instance receiving the event is the bottom-most instance in the hierarchy of instances that are to receive events. Given a hierarchy of say, interface_mc.window_mc.item_mc.label_mc, if both window_mc and item_mc require a button event, item_mc would be the movie clip responsible for handling the events for both. Even though there exists another child within item_mc (label_mc), it doesn't need to make use of any button events so it doesn't need to be considered.

movie clip hierarchy
Event assignment in a movie clip hierarchy

Pros:

  • Gives you the flexibility needed to handle events for parent and child instances
  • You can easily control the order at which each event occurs; capturing or bubbling
  • You are still using button events so you still have control over things like the hand cursor, enabled, etc.

Cons:

  • Can be difficult or time consuming to implement
  • Requires all child movie clips to be assigned event handlers, even if they do not require their own
  • Requires shapes or other non-instance elements to be converted to instances in order for them to handle events for their parent

Workaround 2: Using HitTest

Using hitTest to detect events manually is another possible solution. The hitTest method in Flash can be used to detect whether or not the user's mouse is over a movie clip instance. When used in combination with the onMouseDown, onMouseUp, and onMouseMove events (which are not button events) you can recreate button events without actually using them.

Recreating the onPress event is easiest with the hitTest method. All you need to do is to run hitTest to see if the mouse is touching an instance in an onMouseDown event handler. If true, then the mouse is over the instance and you effectively have yourself an onPress. Example:

my_mc.onMouseDown = function(){
	if (this.hitTest(_root._xmouse, _root._ymouse, true)) {
		// onPress
	}
}

Unlike onPress, onMouseDown is called for all instances regardless of whether or not the mouse is actually over the instance. To check if the mouse is over it, hitTest is used. When that returns true, you got yourself what would otherwise be an onPress event just without the onPress.

If you're concerned about using up the onMouseDown event for your instance, you can push that responsibility on another object. This object would need to be added as a listener to the Mouse object and hold a reference to the movie clip to which it pertains so that it can be used with hitTest.

var my_event = new Object();
my_event.target = my_mc;
Mouse.addListener(my_event);

my_event.onMouseDown = function(){
	if (this.target.hitTest(_root._xmouse, _root._ymouse, true)) {
		// onPress
	}
}

In doing this, you may want to be sure to use Mouse.removeListener with the event object when removing the event or when the movie clip no longer exists. Movie clip instances are automatically added and removed as listeners to Mouse. Generic objects like box_event, would need to be removed manually when no longer in use.

One problem with using hitTest in this manner is that events are called for all instances regardless of overlapping. With normal button events, clicking on an instance overlapping another would prevent the lower instance from receiving the event. This is not the case when using hitTest. Any instance, regardless of what other instances may be covering it, will receive events just so long as the mouse is positioned over it when using hitTest.

overlapping
Using hitTest, overlapping does not stop events

If a problem, you can take a few extra steps to work around the issue. All you need to do manage the events within the same onMouseDown. Once the topmost hitTest resolves to true, prevent the further checks from continuing. Here's an example using 3 instances all controlled by the same onMouseDown to check for an onPress.

var my_events = new Object();
Mouse.addListener(my_events);

my_events.onMouseDown = function(){
	if (top_mc.hitTest(_root._xmouse, _root._ymouse, true)) {
		// top_mc onPress
	}else if (middle_mc.hitTest(_root._xmouse, _root._ymouse, true)) {
		// middle_mc onPress
	}else if (bottom_mc.hitTest(_root._xmouse, _root._ymouse, true)) {
		// bottom_mc onPress
	} 
}

Because an if-else structure is used, only one instance will receive the onPress event effectively blocking any additional events that may be valid below. In order for this to work correctly, it's important that the stacking order (arrangement) of the instances on the screen relate to their positions in the if-else clause.

In source files, see: overlapping_houses_hittest.fla

Note: HitTest Order

The order at which hitTest is called normally is based on instance creation. Those instances created first receive the onMouseDown event first. When dealing with instances created in the same frame, those on top are created first if your movie's publish settings have Load order set to "Top down." With "Bottom up," those on the bottom are created first and would be first to receive the events instead.

Recreating onPress alone is quite simple. It becomes slightly more difficult when you need to also recreating the onRelease and onReleaseOutside events. To distinguish between the two, as well as to check to see whether or not they occur at all, a flag needs to be set within "onPress" to mark whether or not the instance was pressed the last time the mouse was pressed. If so, hitTest can be used to determine whether or not the instance receives an onRelease or onReleaseOutside event the next time the mouse is released within onMouseUp.

my_mc.isPressed = false; // flag

my_mc.onMouseDown = function(){
	if (this.hitTest(_root._xmouse, _root._ymouse, true)) {
		this.isPressed = true;
		// onPress
	}else{
		this.isPressed = false;
	}
}

my_mc.onMouseUp = function(){
	if (this.isPressed){
		if (this.hitTest(_root._xmouse, _root._ymouse, true)) {
			// onRelease
		}else{
			// onReleaseOutside
		}
	}
	this.isPressed = false;
}

The other button events, onRollOver, onRollOut, onDragOver, or onDragOut, take still more effort. They too need a flag variable, an additional flag variable, to determine whether or not the mouse was previously over the instance or not, this time without the mouse being pressed but as the mouse moves with onMouseMove. The isPressed flag will still be used to differentiate onRollOver and onRollOut from onDragOver and onDragOut.

my_mc.isPressed = false; // flag
my_mc.isOver = false; // flag

my_mc.onMouseDown = function(){
	if (this.hitTest(_root._xmouse, _root._ymouse, true)) {
		this.isPressed = true;
		// onPress
	}else{
		this.isPressed = false;
	}
}

my_mc.onMouseUp = function(){
	if (this.isPressed){
		if (this.hitTest(_root._xmouse, _root._ymouse, true)) {
			// onRelease
		}else{
			// onReleaseOutside
		}
	}
	this.isPressed = false;
}

my_mc.onMouseMove = function(){
	var lastIsOver = this.isMouseOver;
	if (this.hitTest(_root._xmouse, _root._ymouse, true)) {
		this.isMouseOver = true;
		if (this.isMouseOver != lastIsOver){
			if (this.isPressed){
				// onDragOver
			}else{
				// onRollOver
			}
		}
	}else{
		this.isMouseOver = false;
		if (this.isMouseOver != lastIsOver){
			if (this.isPressed){
				// onDragOut
			}else{
				// onRollOut
			}
		}
	}
}

Though the onMouseMove used above usually suffices, sometimes it is not enough. If the instances receiving the events don't move on their own, it should suffice. However, you could run into a situation where an instance moves under the mouse without the mouse moving at all. In that case, an onRollOver event should be invoked but wouldn't because onMouseMove has not. For that, you may want to change from using onMouseMove to an onEnterFrame. This, as you might imagine, may become processor intensive when dealing with many movie clips. Of course the same could be said with onMouseMove.

The code required to pull this off can be a little excessive, especially if assigned to instances on an individual basis. A single function can be created to assign these methods dynamically with a little more ease. It will set up an instance to receive button events in the form of [eventName]+"Handler" by using hitTest. So, for example, onPress becomes onPressHandler.

function enablePseudoButtonEvents(target, enableRollAndDrag, checkEvent){

	if (enableRollAndDrag == undefined){
		enableRollAndDrag = false;
	}
	if (checkEvent == undefined){
		checkEvent = "onMouseMove";
	}
	
	target.isPressed = false;
	
	target.onMouseDown = function(){
		if (this.hitTest(_root._xmouse, _root._ymouse, true)) {
			this.isPressed = true;
			this.onPressHandler();
		}else{
			this.isPressed = false;
		}
	}
	
	target.onMouseUp = function(){
		if (this.isPressed){
			if (this.hitTest(_root._xmouse, _root._ymouse, true)) {
				// onRelease
				this.onReleaseHandler();
			}else{
				// onReleaseOutside
				this.onReleaseOutsideHandler();
			}
		}
		this.isPressed = false;
	}
	
	if (enableRollAndDrag){
		
		target.isOver = false;
		
		target[checkEvent] = function(){
			var lastIsOver = this.isOver;
			if (this.hitTest(_root._xmouse, _root._ymouse, true)) {
				
				this.isOver = true;
				if (this.isOver != lastIsOver){
					if (this.isPressed){
						// onDragOver
						this.onDragOverHandler();
					}else{
						// onRollOver
						this.onRollOverHandler();
					}
				}
				
			}else{
				
				this.isOver = false;
				if (this.isOver != lastIsOver){
					if (this.isPressed){
						// onDragOut
						this.onDragOutHandler();
					}else{
						// onRollOut
						this.onRollOutHandler();
					}
				}
				
			}
		}
	}
}

The first parameter, target, is the instance you want to have look for events using hitTest. Following that is a true or false parameter, enableRollAndDrag, which lets you specify whether or not a continuous check is to be used to detect the onRollOver, onRollOut, onDragOver, and onDragOut events. If so, you can specify what event you want to use to look for that with the third parameter, checkEvent. This is expected to be either "onMouseMove" or "onEnterFrame". The default is "onMouseMove".

Here's how it might be used:

enablePseudoButtonEvents(my_mc, true, "onEnterFrame");
my_mc.onPressHandler = function(){
	// onPress
}
my_mc.onReleaseOutsideHandler = function(){
	// onReleaseOutside
}
my_mc.onRollOverHandler = function(){
	// onRollOver
}
my_mc.onDragOutHandler = function(){
	// onDragOut
}
In source files, see: pseudo_hittest_buttons.fla

Pros:

  • Doesn't require event handler on many instances to manage one event for one parent instance as with delegation
  • Provides more flexibility in how events can be handled

Cons:

  • Some implementations make it difficult to control execution order (defaults to instance creation - those created first receive the event first)
  • Can be difficult to manage events between overlapping instances since overlapping does not stop instances below the topmost from receiving events
  • Can be processor intensive if using to check for onRollOver, onRollOut, onDragOver, or onDragOut for many instances at once
  • No hand cursor

Workaround 2.5: Delegation-hitTest Combination

Some of the deficiencies of these workarounds can be avoided if the two are used together as a single solution. How this is applied depends mostly on circumstance. More often than not, the parent instance is given button events that it accepts and relays to child instances identified using hitTest. In such a case, the children's events are delegated to the parent which is opposite of that used with delegation before where the parent's events were delegated to the children.

This solution gives you the ease and versatility provided with the use of hitTest but also gives you back your hand cursor, an often desirable indicator for the user that actions for your instance exist in the first place. In many cases, it also means that nested instances can receive onRollOver, onRollOut, onDragOver, and onDragOut events without the need for a consistent onMouseMove or onEnterFrame check. This is because the parent movie clip can just relay those button events to its children using hitTest only when they occur to see which child instance they also apply.

This, however, does break when you're dealing with overlapping child instances. Then, there's no way to tell when the cursor moves from one child to another child it overlaps since it would never have rolled out or dragged out of the parent. Also, for events like onRollOut and onDrag out, hitTest will obviously not return a valid result since the events indicate that the mouse is no longer over any instances. Steps will need to be taken to for these considerations.

There are actually many, many different ways this can be implemented. How you handle the implementation is up to you. Different circumstances may require different implementations.

In source files, see: delegate_with_hittest_buttons.fla

Pros:

  • Flexibility
  • Hand cursor

Cons:

  • Overlapping can still cause problems
  • Using enabled for a parent with button events assigned to it will affect all children as well
  • Can be more difficult to implement compared to the other solutions alone

An ActionScript 2.0 Solution

The ButtonEventHandler class is an ActionScript 2.0 solution that uses the hitTest method of event detection. It basically takes the code within the enablePseudoButtonEvents function, wraps it into a class supporting features such as event bubbling, optional depth management for dealing with overlapping, and a new onMouseWithin event.

In source files, see: ButtonEventHandler/eventbubblebuttons.fla

Documentation for the classes used is provided in the Documentation folder within that directory.

Conclusion

It would be nice if Flash had more options when dealing with nested button events. Sadly, at this point in time, it does not and we are left to deal with its default event capturing, non-propagating behavior. That doesn't mean we still can't get done what we need to get done. All it takes is a little leg work and the occasional work around when things won't work as well as you'd like. Whether it be through delegation, the manual detection of events with hitTest, or both, you now know what can be done about it.