February 25th, 2013

Android Tricks: Supporting Drag and Drop in an App

Since Honeycomb, Android has offered a very straightforward, easy-to-use API for implementing drag and drop in your application. Any View can be dragged and any other View can become a drop zone. You can attach information to a drag operation to be used by the receiver, allowing the application to do more processing when a drop is performed.

The core of the API for drop zones is built around one event and several states. To declare a drop zone in Mono for Android, you simply need to attach a handler to the Drag event of your view (the same handler can be reused).

That handler will receive a View.DragEventArgs as a parameter. It will have an e.Event.Action property that specifies the state of the current drag process. The following is one simple implementation of the method as a state machine, with a comment on each section to describe the nature of the state:

void HandleDrag (object sender, Android.Views.View.DragEventArgs e)
{
	var evt = e.Event;
	switch (evt.Action) {
	case DragAction.Started:
		/* To register your view as a potential drop zone for the current view being dragged
		 * you need to set the event as handled
		 */
		e.Handled = true;
		
		/* An important thing to know is that drop zones need to be visible (i.e. their Visibility)
		 * property set to something other than Gone or Invisible) in order to be considered. A nice workaround
		 * if you need them hidden initially is to have their layout_height set to 1.
		 */

		break;
	case DragAction.Entered:
	case DragAction.Exited:
		/* These two states allows you to know when the dragged view is contained atop your drop zone.
		 * Traditionally you will use that tip to display a focus ring or any other similar mechanism
		 * to advertise your view as a drop zone to the user.
		 */

		break;
	case DragAction.Drop:
		/* This state is used when the user drops the view on your drop zone. If you want to accept the drop,
		 *  set the Handled value to true like before.
		 */
		e.Handled = true;
		/* It's also probably time to get a bit of the data associated with the drag to know what
		 * you want to do with the information.
		 */
		var data = e.Event.ClipData.GetItemAt (0).Text;

		break;
	case DragAction.Ended:
		/* This is the final state, where you still have possibility to cancel the drop happened.
		 * You will generally want to set Handled to true.
		 */ 
		e.Handled = true;
		break;
	}
}

To initiate the drag and tell which View is being dragged, you use the StartDrag method on it. Generally, this is done as part of a Click event handler (but anything else will do). This is also when you create the data that you want to associate with the drag:

void HandleClick (object sender, EventArgs e)
{
	var data = ClipData.NewPlainText ("category", "value");
	StartDrag (data, new MyShadowBuilder (this), null, 0);
}

StartDrag takes as parameters your drag data, a shadow builder (more on that in a sec), a state object (if you want one), and a set of flags (which are currently unused and should always be 0).

The interesting tidbit is the View.DragShadowBuilder parameter, which lets you personalize the look of the drag shadow. By default, you can supply an instance of the class and pass in your view to create a default drag shadow. That approach will essentially be an image snapshot of your view with some transparency—which is actually fine for most cases. Of course, what’s more interesting is that you can create entirely custom drag shadows:

custom-drag-shadow

It’s actually pretty simple to do so, as it’s very similar to normal View painting. First, you need to create your own class deriving from View.DragShadowBuilder. Your constructor should take the dragged View and pass it down to the base constructor.

The two methods you then need to implement are OnProvideShadowMetrics and OnDrawShadow. In the first one, you will give the measurements of your drag shadow. In the second, you will be given a Canvas in which to draw your shadow.

Here is my implementation of the drag shadow builder that produces the image above (with inline explanations):

class MyShadowBuilder : View.DragShadowBuilder
{
	const int centerOffset = 52;
	int width, height;

	public MyShadowBuilder (View baseView) : base (baseView)
	{
	}

	public override void OnProvideShadowMetrics (Point shadowSize, Point shadowTouchPoint)
	{
		width = View.Width;
		height = View.Height;
		
		// This is the overall dimension of your drag shadow
		shadowSize.Set (width * 2, height * 2);
		// This one tells the system how to translate your shadow on the screen so
		// that the user fingertip is situated on that point of your canvas.
		// In my case, the touch point is in the middle of the (height, width) top-right rect
		shadowTouchPoint.Set (width + width / 2 - centerOffset, height / 2 + centerOffset);
	}

	public override void OnDrawShadow (Canvas canvas)
	{
		const float sepAngle = (float)Math.PI / 16;
		const float circleRadius = 2f;

		// Draw the shadow circles in the top-right corner
		float centerX = width + width / 2 - centerOffset;
		float centerY = height / 2 + centerOffset;
		
		var baseColor = Color.Black;
		var paint = new Paint () {
			AntiAlias = true,
			Color = baseColor
		};
		
		// draw a dot where the center of the touch point (i.e. your fingertip) is
		canvas.DrawCircle (centerX, centerY, circleRadius + 1, paint);
		for (int radOffset = 70; centerX + radOffset < canvas.Width; radOffset += 20) {
			// Vary the alpha channel based on how far the dot is
			baseColor.A = (byte)(128 * (2f * (width / 2f - 1.3f * radOffset + 60) / width) + 100);
			paint.Color = baseColor;
			// Draw the dots along a circle of radius radOffset and centered on centerX,centerY
			for (float angle = 0; angle < Math.PI * 2; angle += sepAngle) {
				var pointX = centerX + (float)Math.Cos (angle) * radOffset;
				var pointY = centerY + (float)Math.Sin (angle) * radOffset;
				canvas.DrawCircle (pointX, pointY, circleRadius, paint);
			}
		}

		// Draw the dragged view in the bottom-left corner
		canvas.DrawBitmap (View.DrawingCache, 0, height, null);
	}
}

On a final note, you will notice that I’m painting the dragged View at the end of my OnDrawShadow method using the DrawingCache property. For that property to return something valid, you need to set the DrawingCacheEnabled property to true on the view.