If you are thinking about developing an Android tablet app, one of the most exciting things has got to be the drag and drop capabilities that were added to Android as part of Honeycomb. No wait a minute, aren't there a lot more interesting features than drag and drop in Honeycomb? There are definitely a lot of great features, but drag and drop is one of the most interesting in my opinion. It's a perfect fit for tablets, where you can have this nice big graphical objects to drag and drop. I think it really challenges mobile designers and developers to rethink our interaction models. If you read through that tutorial I just linked to, you will learn about many of the intricacies of drag and drop Android style. I thought I'd provide an even simpler example for those of you who prefer the Cliff Notes versions of things.
First we will start with a simple UI, a 2x2 grid with a cute little image that we will drag and drop around this grid. Here is what it looks like:
I used a simple TableLayout for this, along with some basic border background Drawables. Here's the XML for it:
<TableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TableRow>
<LinearLayout android:layout_width="640px"
android:layout_height="345px"
android:id="@+id/topLeft"
android:background="@drawable/border">
<ImageView android:id="@+id/droid"
android:src="@drawable/bugdroid"
android:layout_width="150px"
android:layout_height="150px"
/>
</LinearLayout>
<LinearLayout android:layout_width="640px"
android:layout_height="345px"
android:id="@+id/topRight"
android:background="@drawable/border2"
/>
</TableRow>
<TableRow>
<LinearLayout android:layout_width="640px"
android:layout_height="345px"
android:id="@+id/bottomLeft"
android:background="@drawable/border2"
/>
<LinearLayout android:layout_width="640px"
android:layout_height="345px"
android:id="@+id/bottomRight"
android:background="@drawable/border"
/>
</TableRow>
</TableLayout>
Nothing special so far. All of the magic happens in the code. Here's the basics of the Activity that uses this XML.
public class DndActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.grid);
View droid = findViewById(R.id.droid);
droid.setOnLongClickListener(new OnLongClickListener(){
@Override
public boolean onLongClick(View v) {
ClipData data = ClipData.newPlainText("foo","bar");
DragShadowBuilder shadowBuilder = new DragShadowBuilder();
v.startDrag(data, shadowBuilder, v, 0);
return true;
}
});
findViewById(R.id.topLeft).setOnDragListener(new BoxDragListener());
findViewById(R.id.bottomLeft).setOnDragListener(new BoxDragListener());
findViewById(R.id.topRight).setOnDragListener(new BoxDragListener());
findViewById(R.id.bottomRight).setOnDragListener(new BoxDragListener());
}
}
Here we just set the contentView, then we get a handle on the droid image. We set its onLongClickListener. I think the long click will become the de facto way to initiate drag and drop in Android apps. You could use some other event of course, but long click feels right. The key thing we need to do to initiate drag and drop is invoke the startDrag method. To do that, we have to create a ClipData object and DragShadowBuilder. You can use the ClipData to put all kinds of interesting data/metadata that will be passed around to the drop zones (Views) in your app. In this case I didn't need anything fancy, so I put something minimal and arbitrary. For the DragShadowBuilder, you might want to do something fancy to make the dragging look cool. I used a default one and it doesn't do much at all. You also need to set a "localData" object. This can be anything. Since my local data is the image itself, I chose to pass the ImageView that will be passed in to the onLongClick method. Now you can call startDrag. Finally, make sure you return true to let the OS know that drag and drop is a go.
Next in the code you will see that for each of the cells in my table, I have set an OnDragListener object. I've set this to a new instance of BoxDragListener for each of the cells. This is an inner class of DndActivity:
class BoxDragListener implements OnDragListener{
boolean insideOfMe = false;
Drawable border = null;
Drawable redBorder = getResources().getDrawable(R.drawable.border3);
@Override
public boolean onDrag(View self, DragEvent event) {
if (event.getAction() == DragEvent.ACTION_DRAG_STARTED){
border = self.getBackground();
self.setBackgroundDrawable(redBorder);
} else if (event.getAction() == DragEvent.ACTION_DRAG_ENTERED){
insideOfMe = true;
} else if (event.getAction() == DragEvent.ACTION_DRAG_EXITED){
insideOfMe = false;
} else if (event.getAction() == DragEvent.ACTION_DROP){
if (insideOfMe){
View view = (View) event.getLocalState();
ViewGroup owner = (ViewGroup) view.getParent();
owner.removeView(view);
LinearLayout container = (LinearLayout) self;
container.addView(view);
}
} else if (event.getAction() == DragEvent.ACTION_DRAG_ENDED){
self.setBackgroundDrawable(border);
}
return true;
}
}
This guy will get a stream of DragEvents fired at its onDrag method. You can determine what kind of DragEvent it is by calling getAction on the event. The first one you will get is an ACTION_DRAG_STARTED event. For this we want to change the border of our cell to red to let the user know that they can drop the image in this cell. Here's what that looks like:
Next we want to keep track if the user's finger has entered into a drop zone. So we look for the ACTION_DRAG_ENTERED and toggle a local boolean. Similarly, we look for ACTION_DRAG_EXITED in case the user changed their mind and picked a different zone to drop the image into. Next we look for the ACTION_DROP. If the object is inside our drop zone, then we update the UI by removing it from its previous parent and adding it to the drop zone. Finally, we listen for ACTION_DRAG_ENDED to know when the drag and dropping is finished. We then restore the borders to their original color. That's it!
Update: The code above will not draw a shadow when you drag the image around the screen. There is a very easy way to get a shadow and that is to pass the View to it the DragShadowBuilder constructor, i.e. new DragShadowBuilder(v); in the above code. That will cause the image in the sample app to be used as the shadow. Be careful using this technique though. If the View that you are dragging is big/complex, then you might take a performance hit (UI becomes sluggish.) In that case you might want to subclass DragShadowBuilder and override its onDragShadow method to draw something simpler for the shadow. I've been playing around with this and StackViews however, and it has worked great with no need to subclass DragShadowBuilder. Be sure to set the android:hardwareAccelerated="true" attribute in your AndroidManifest.xml either on the Activity or the whole Application.