Archive for January, 2011

Moving Views In Android – Part 2, Drag and Drop

This is part 2 of my investigation into how to get views on an Android screen that a user can move by touching them and dragging them. For Moving Views in Android – Part 1, I had a couple of images positioned on the screen and got the images to move on the screen. It turns out that the views themselves were not moving, just the images within the bounds of the view. In the work that I describe here, I actually have the views moving. The key was to have a custom ViewGroup to coordinate drag and drop events for all views within the layout.

The figures below show what the app looks like in the emulator. Figure 1 shows the initial state of the app. Two images and a text view are in the upper left of the screen. The other figures show the views being dragged around the screen. Red highlighting indicates which view is moving.

Figures 1-2

Dragging an image

Figures 3-4:

Dragging the second image Moving the text view

The solution feels pretty good — and it should, considering it is derived from the Android Launcher code. I had searched a long time for good touch and drag-drop examples, and never found any that were as complete as what the Android developers had done. What I like is the good object model and the clear separation of responsibilities among the classes. More on that follows in the next section.

(For more on drag-drop, see “Drag-Drop for Android GridView V4“.) 

How It Works

The Launcher is the activity that manages the workspace you see on your Android phone. Besides being able to launch apps, it allows you to change the layout of icons on the screen. You touch an icon, wait for the feedback (vibration and the change in size of the icon) that you have touched a moveable icon, and then you reposition the icon on the screen.

I made a few simplifying assumptions as I built my example activity object.

  1. The example app allows you to move views, but only on a single screen. The Launcher allows you to place app icons on many screens that you get to by scrolling left and right.
  2. The example app supports only one place where you can drop views. The Launcher supports multiple DropTargets.
  3. The example app allows views to be dropped anywhere, even on top of another view. The Launcher maintains a grid of icons and icons can be dropped only in the grid.
  4. The Launcher has animation and scaling code that is used to show a bitmap of the view being moved. The example app simply shows the bitmap.

The following classes have been adapted for my example DragView application.

DragActivity – The Launcher class was turned into my DragActivity class. It is the main activity of the app.

DragLayer – implements a custom ViewGroup, which coordinates movement of views on the screen. The Launcher DragLayer is a subclass of FrameLayout. Mine is a subclass of MyAbsoluteLayout, which is a clone of the recently deprecated AbsoluteLayout class. AbsoluteLayouts allow views to be positioned to specific absolute x and y positions.

DragController – This object is the controller that does most of the work to support dragging and dropping.

DragSource – defines where a drag operation begins.

DropTarget – defines where an object can be dropped.

DragView – This is the view that you see moving around on the screen during a drag operation. It is not the actual view you want to move, but simply a bitmap that looks like the view.

In this object model, here is how an object moves on the screen. The user touches a view on the screen. The view touched signals an event, onLongClick, to its listener, which is the DragActivity. The activity turns it over to its DragController object. The controller initiates a drag operation by first getting a bitmap copy of the view, storing it in a DragView, and displaying it on the screen. As the user moves the view, the DragLayer fields all the touch events in its onTouchEvent method. It relays those events to the DragController. It watches for the end of the touch (ACTION_UP), but in the mean time, it handles the motion events by shifting the DragView to the new touch position. When the touch finally ends, the controller checks its set of DropTargets to see which one is active and if it will accept the view being dropped there. If it does, the controller calls the onDrop method of the target. In this application, the DragLayer also implements the DropTarget interface. So the onDrop call goes to the DragLayer and it takes the view being dragged and repositions it to the drop location.

The full source code is available below. A few code sections are worth noting now.

Each view that can be moved relays its onLongClick events to the DragActivity. The connection is made in the setupViews method of DragActivity.

private void setupViews() {
...
ImageView i1 = (ImageView) findViewById (R.id.Image1);
ImageView i2 = (ImageView) findViewById (R.id.Image2);
i1.setOnLongClickListener(this);
i2.setOnLongClickListener(this);
...
}

The onLongClick handler calls on the DragController to initiate the drag sequence.

public boolean onLongClick(View v) {
    trace ("onLongClick in view: " + v);
    // Let the DragController initiate a drag-drop sequence.
    // Use the dragInfo to pass along the object being dragged.
    Object dragInfo = v;
    mDragController.startDrag (v, mDragLayer, dragInfo, DragController.DRAG_ACTION_MOVE);
    return true;
}

While the drag is happening, the DragLayer passes all touch events to its DragController. The onTouchEvent method in DragController moves the DragView and its bitmap. That creates the appearance of the view moving on the screen. The move method in DragView uses updateViewLayout, which causes the layout for the screen to be redone, using whatever new x-y positions are in effect.

void move(int touchX, int touchY) {
        // This is what was done in the Launcher code.
        WindowManager.LayoutParams lp = mLayoutParams;
        lp.x = touchX - mRegistrationX;
        lp.y = touchY - mRegistrationY;
        mWindowManager.updateViewLayout(this, lp);
}

At the end of the drag, the onDrop method of DragLayer is called. It repositions the actual view. Note that it too calls to updateViewLayout. That is what causes the DragLayer view to recalculate its layout and redraw all its views.

public void onDrop (DragSource source,
         int x, int y, int xOffset, int yOffset,
        DragView dragView, Object dragInfo)
{
    View v = (View) dragInfo;
    int w = v.getWidth ();
    int h = v.getHeight ();
    int left = x - xOffset;
    int top = y - yOffset;
    DragLayer.LayoutParams lp = new DragLayer.LayoutParams (w, h, left, top);
    this.updateViewLayout(v, lp);
}

Potential Improvements

My adaptation is not quite as clean as it should be. Roughly speaking, in MVC (model-view-controller) terms, the DragActivity is the model, the DragLayer is the view, and the DragController is the controller. It’s not clear to me that I have stayed true to MVC in this example. I think the handlers for click and long click events might be better off in the controller.

This example uses a clone of the deprecated AbsoluteLayout class. Now that I understand the Launcher code better, I see that it might be better in a real application to have the DragLayer view determine exactly where views are allowed to be dropped. That would be similar to the way it was done in the Launcher app. Its Workspace class allows drops only at empty spaces in a grid. (Note: I did something for GridViews. See “Drag-Drop for an Android GridView“.) 

If you want to see an example where views are added dynamically to the DragLayer, see “Moving Views In Android – Part 3: Drop Zones“. It also shows how to give users more visual feedback as objects are being dragged around the screen.

Acknowledgements

Thanks go to the Android development team. With all the code being open source, it makes it easy for all of us to learn from their designs. A post in the Android Developers group about doing drag and drop without AbsoluteLayout pointed me in the right direction.

Source Code

The source code is available as an Eclipse project. it is available in two places:  (1) download from Wglxy.com. (2) download from Google Docs.  Do a “clean build” in Eclipse if it reports errors immediately after importing the project.

If you find that the app does not build in Eclipse, be sure to do a clean build by using the “Clean” item on the Project menu. 

This work was done on a MacBook running Eclipse Galileo and Android 2.2 (API 8).  



Follow

Get every new post delivered to your Inbox.

Join 73 other followers