Previous Page
Next Page

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:

  1. Pick up the red rectangle.

  2. Move it over the white rectangle.

  3. Move the mouse further to the right so the red rectangle is outside the area of the white rectangle.

  4. 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


Previous Page
Next Page
Converted from CHM to HTML with chm2web Pro 2.85 (unicode)