Recipe 6.9.
Dragging and Dropping Objects with the Mouse
Problem
You
want to provide a
drag-and-drop-style interface.
Solution
Use startDrop( ), stopDrag( ) and dropTarget from the
Sprite class to implement drag-and-drop behavior.
Alternatively, extend the ascb.display.DraggableSprite class
for visually smoother dragging behavior using the drag( )
and drop( ) methods.
Discussion
Creating drag-and-drop behavior is not as
difficult as you might think. The Sprite class includes
methods specifically for the purpose of drag and drop, namely
startDrag( ) and stopDrag( ).
The startDrag( ) method can be called on
any Sprite instance to have it follow the mouse around the
screen, creating the dragging effect. To stop dragging, call the
stopDrag( ) method on the Sprite instance. After the
drag operation is complete, you can examine the dropTarget
property of the Sprite to determine the object that the
Sprite was dropped on. The value of dropTarget is
useful for determining if a drop operation is valid (such as
dropping a folder icon on a trashcan to delete it).
When calling startDrag( ), you don't have
to specify any parameters; however, the method accepts up to two
parameters. The parameters are:
lockCenter
-
When TRue the center of the
Sprite is locked to the mouse position regardless of where
the user pressed the mouse. When false the Sprite
follows the mouse from the location where the user first clicked.
The default value is false.
bounds
-
The Rectangle region where you want
to constrain dragging. The Sprite is not capable of being
dragged outside of this region. The default value is null,
meaning there is no area constraint.
The following code example uses these methods to
set up a simple drag-and-drop behavior. There are three rectangles
on the left capable of being dragged: red, green, and blue. The
rectangle on the right is white, and serves as the target area
where you drop the color rectangles. Dragging and dropping a
colored rectangle onto the white rectangle colorizes the white
rectangle the same color of the rectangle that was dropped onto
it:
package {
import flash.display.Sprite;
import flash.display.DisplayObject;
import flash.events.MouseEvent;
import flash.geom.Point;
import flash.filters.DropShadowFilter;
public class ColorDrop extends Sprite {
private var _red:Sprite;
private var _green:Sprite;
private var _blue:Sprite;
private var _white:Sprite;
// Saves the starting coordinates of a dragging Sprite so
// it can be placed back
private var startingLocation:Point;
// Create the rectangles that comprise the interface
// and wire the mouse events to make them interactive
public function ColorDrop( ) {
createRectangles( );
addEventListeners( );
}
private function createRectangles( ):void {
_red = new Sprite( );
_red.graphics.beginFill( 0xFF0000 );
_red.graphics.drawRect( 0, 10, 10, 10 );
_red.graphics.endFill( );
_green = new Sprite( )
_green.graphics.beginFill( 0x00FF00 );
_green.graphics.drawRect( 0, 30, 10, 10 );
_green.graphics.endFill( );
_blue = new Sprite( );
_blue.graphics.beginFill( 0x0000FF );
_blue.graphics.drawRect( 0, 50, 10, 10 );
_blue.graphics.endFill( );
_white = new Sprite( );
_white.graphics.beginFill( 0xFFFFFF );
_white.graphics.drawRect( 20, 10, 50, 50 );
_white.graphics.endFill( );
addChild( _red );
addChild( _green );
addChild( _blue );
addChild( _white );
}
private function addEventListeners( ):void {
_red.addEventListener( MouseEvent.MOUSE_DOWN, pickup );
_red.addEventListener( MouseEvent.MOUSE_UP, place );
_green.addEventListener( MouseEvent.MOUSE_DOWN, pickup );
_green.addEventListener( MouseEvent.MOUSE_UP, place );
_blue.addEventListener( MouseEvent.MOUSE_DOWN, pickup );
_blue.addEventListener( MouseEvent.MOUSE_UP, place );
}
public function pickup( event:MouseEvent ):void {
// Save the original location so you can put the target back
startingLocation = new Point( );
startingLocation.x = event.target.x;
startingLocation.y = event.target.y;
// Start dragging the Sprite that was clicked on and apply
// a drop shadow filter to give it depth
event.target.startDrag( );
event.target.filters = [ new DropShadowFilter( ) ];
// Bring the target to front of the display list so
// it appears on top of everything else
setChildIndex( DisplayObject( event.target ), numChildren - 1 );
}
public function place( event:MouseEvent ):void {
// Stop dragging the Sprite around and remove the depth
// effect (i.e., the drop shadow) from the filter
event.target.stopDrag( );
event.target.filters = null;
// Check to see if the Sprite was dropped over the white
// rectangle, and if so, update the color
if ( event.target.dropTarget == _white ) {
// Determine which color was dropped, and apply that color
// to the white rectangle
var color:uint;
switch ( event.target ) {
case _red: color = 0xFF0000; break;
case _green: color = 0x00FF00; break;
case _blue: color = 0x0000FF; break;
}
_white.graphics.clear( );
_white.graphics.beginFill( color );
_white.graphics.drawRect( 20, 10, 50, 50 );
_white.graphics.endFill( );
}
// Place the dragging Sprite back to its original location
event.target.x = startingLocation.x;
event.target.y = startingLocation.y;
}
}
}
Breaking down this code a bit, all of the
rectangles are added to the display list and then the appropriate
mouseDown and mouseUp listeners are defined.
Every time a mouseDown is received over one of the colored
rectangles, the pickup process starts.
First, the original location of the rectangle is
saved. This allows the rectangle's location to be restored after
the drop operation. Next, the startDrag( ) method is called
on the rectangle to start dragging it around the screen. After
that, a DropShadowFilter is applied to provide depth during
the drag and make it appear as if the rectangle were held above the
others in the display list. Finally, the rectangle is moved to the
front of the display list via setChildIndex( ) so that it is
drawn on top of all of the others as it follows the mouse.
When the mouseUp event is detected, the
drop operation commences via the place( ) method. First, the
rectangle has stopDrag( ) called on it to stop the mouse
follow behavior, and the filters are removed to reverse the depth
effect. Next, the rectangle's dropTarget property is
examined to determine if it was dropped over the white rectangle.
If the white rectangle is indeed the dropTarget, the white
rectangle is given the same color as the rectangle that was dropped
onto it. Finally, because the rectangle is out of position now from
following the mouse around, the original starting location is
restored.
The previous code works alright, but there are
two small problems with it: the dropTarget property isn't
always reliable and the dragging is choppy.
The dropTarget property continually
changes during movement after startDrag( ) is issued. This
is good because it allows for feedback to be provided as the object
is moved over different possible drop targets; you can indicate
whether a drop is currently allowed based on whatever
dropTarget currently is. However, dropTarget only
changes when the pointer passes over a new display object, and not when the pointer leaves a display object.
This presents a problem when you move over an object and then leave
that object without moving over a new one. In such a case, the
dropTarget property still points to the last object that
the mouse moved over, even though the mouse may have moved outside
of that object without ever moving over a new object. This means
that the mouse is not guaranteed to actually be over the display
object that dropTarget refers to.
To see this effect in action:
-
Pick up the red rectangle.
-
Move it over the white rectangle.
-
Move the mouse further to the right so the red
rectangle is outside the area of the white rectangle.
-
With the red rectangle outside the white rectangle, release it.
You can see that the white rectangle is colored
red because the dropTarget is still referring to the white
rectangle, even though the red rectangle is dropped outside of the
white rectangle bounds.
To fix this behavior, use the hitTestPoint( ) method to
determine if the mouse location is within the bounds of the
dropTarget display object. The hitTestPoint( )
method takes an x and y location and returns a
true or false value, indicating if the location
falls within the bounds of the object. An optional third
Boolean parameter can
be used to specify how the hit test area is calculated. Specifying
false as the third parameter will use the bounding box
rectangle of the object, whereas TRue uses the actual
shape of the object itself. The default value is
false.
Inside of the place( ) method that tests if the
colored rectangle was dropped correctly, add a call to
hitTestPoint( ) inside the conditional checking the
dropTarget. This makes sure the mouse cursor still is
within the bounds of the white rectangle before allowing the
drop:
if ( event.target.dropTarget == _white
&& _white.hitTestPoint( _white.mouseX, _white.mouseY ) ) {
Another problem with the code is the choppy
screen updating during mouse movement. This is because mouse events
happen independently of the rendering process. The movie's frame
rate determines how often the screen is updated, so if the mouse
changes the display, the updated display won't appear until the
screen is normally refreshed (as specified by the frame rate).
To combat this problem, the MouseEvent class includes the
method updateAfterEvent( ) . Typically called when the
mouseMove event is handled, updateAfterEvent( )
notifies the Flash Player that the screen has changed and instructs
it to redraw. This avoids the delay that occurs when waiting for
the frame rate to update the screen normally after mouse
movement.
Unfortunately, updateAfterEvent( ) does
not play nice with startDrag( ). Even if a
mouseMove event handler is added for the sole purposes of
calling updateAfterEvent( ) to handle the rendering updates,
calling updateAfterEvent( ) has no effect. Another problem
with startDrag( ) is that you are able to drag only one
Sprite at a time. Although this isn't necessarily a major
problem, it is rather limiting.
To address these issues, a new custom visual
class, named DraggableSprite, was created as
part of the ActionScript 3.0
Cookbook Library (found at http://www.rightactionscript.com/ascb);
it can be found in the ascb.display package.
The DraggableSprite class inherits from
Sprite, and adds two aptly named methods: drag( ) and
drop( ). The
drag( ) method takes
the same parameters and is used the same way as startDrag(
). The drop( ) method behaves the same as stopDrag(
).
The primary difference is that the drag-and-drop
functionality available in DraggableSprite is implemented by
custom mouse tracking code, versus having the Flash Player track
the mouse internally. Because of this, both negative aspects of
startDrag( ) are overcome. Multiple DraggableSprite
instances are able to move with the mouse at the same time, and the
rendering delay issue is eliminated because updateAfterEvent(
) works as expected.
However, when switching to
DraggableSprite, the dropTarget property is no
longer applicable. Instead, you have to use the
getObjectsUnderPoint( ) method to return the objects beneath
the mouse and determine if a drop is valid based on the information
returned.
The getObjectsUnderPoint( ) method
returns an array of display objects that are children of the
container the method was called on. The item at the end of the
array, in position length 1, is the top-most item
(the object directly underneath the mouse). The item at position 0
is the very bottom item underneath the mouse. By testing to see if
the white rectangle is in the list of objects under the mouse
location at the time of the drop, you can determine if the drop was
valid or not.
The following code is the same drag-and-drop
behavior as before, but updated to use DraggableSprite
instead of Sprite:
package {
import flash.display.Sprite;
import flash.display.DisplayObject;
import flash.events.MouseEvent;
import flash.geom.Point;
import flash.filters.DropShadowFilter;
import ascb.display.DraggableSprite;
public class ColorDrop extends Sprite {
private var _red:DraggableSprite;
private var _green:DraggableSprite;
private var _blue:DraggableSprite;
private var _white:Sprite;
// Saves the starting coordinates of a dragging Sprite so
// it can be placed back
private var startingLocation:Point;
// Create the rectangles that comprise the interface
// and wire the mouse events to make them interactive
public function ColorDrop( ) {
createRectangles( );
addEventListeners( );
}
private function createRectangles( ):void {
_red = new DraggableSprite( );
_red.graphics.beginFill( 0xFF0000 );
_red.graphics.drawRect( 0, 10, 10, 10 );
_red.graphics.endFill( );
_green = new DraggableSprite( )
_green.graphics.beginFill( 0x00FF00 );
_green.graphics.drawRect( 0, 30, 10, 10 );
_green.graphics.endFill( );
_blue = new DraggableSprite( );
_blue.graphics.beginFill( 0x0000FF );
_blue.graphics.drawRect( 0, 50, 10, 10 );
_blue.graphics.endFill( );
_white = new DraggableSprite( );
_white.graphics.beginFill( 0xFFFFFF );
_white.graphics.drawRect( 20, 10, 50, 50 );
_white.graphics.endFill( );
addChild( _red );
addChild( _green );
addChild( _blue );
addChild( _white );
}
private function addEventListeners( ):void {
_red.addEventListener( MouseEvent.MOUSE_DOWN, pickup );
_red.addEventListener( MouseEvent.MOUSE_UP, place );
_green.addEventListener( MouseEvent.MOUSE_DOWN, pickup );
_green.addEventListener( MouseEvent.MOUSE_UP, place );
_blue.addEventListener( MouseEvent.MOUSE_DOWN, pickup );
_blue.addEventListener( MouseEvent.MOUSE_UP, place );
}
public function pickup( event:MouseEvent ):void {
// Save the original location so you can put the target back
startingLocation = new Point( );
startingLocation.x = event.target.x;
startingLocation.y = event.target.y;
// Start dragging the Sprite that was clicked on and apply
// a drop shadow filter to give it depth
event.target.drag( );
event.target.filters = [ new DropShadowFilter( ) ];
// Bring the target to front of the display list so
// that it appears on top of everything else
setChildIndex( DisplayObject( event.target ), numChildren - 1 );
}
public function place( event:MouseEvent ):void {
// Stop dragging the Sprite around and remove the depth
// effect from the filter
event.target.drop( );
event.target.filters = null;
// Get a list of objects inside this container that are
// underneath the mouse
var dropTargets:Array = getObjectsUnderPoint( new Point( mouseX, mouseY ) );
// The display object at position length - 1 is the top-most object,
// which is the rectangle that is currently being moved by the mouse.
// If the white rectangle is the one immedialy beneath that, the
// drop is valid
if ( dropTargets[ dropTargets.length - 2 ] == _white ) {
// Determine which color was dropped, and apply that color
// to the white rectangle
var color:uint;
switch ( event.target ) {
case _red: color = 0xFF0000; break;
case _green: color = 0x00FF00; break;
case _blue: color = 0x0000FF; break;
}
_white.graphics.clear( );
_white.graphics.beginFill( color );
_white.graphics.drawRect( 20, 10, 50, 50 );
_white.graphics.endFill( );
}
// Place the dragging Sprite back to its original location
event.target.x = startingLocation.x;
event.target.y = startingLocation.y;
}
}
}
See Also
Recipes 6.4
and 6.8
|