Archives for category: Android

A few days ago, I had to create a list that allowed the user to drag elements and rearrange them. It was unexpectedly troublesome, and somewhat more of a challenge than I had initially anticipated, so I thought to document the experience.

If you just want to get your hands on the code, it’s freely available here. Any and all kinds of feedback are welcome!

Rearranging

The functionality can already be seen in dozens of popular applications, but there’s not really a standard, and implementations show minor differences both in behavior and presentation. For that reason, I think I should begin by clarifying what I was expecting to achieve.

I wanted the user to be able to activate an edit mode on a list, hold his finger down on an item, and drag it up or down with immediate visual feedback of each positional swap. The keyword here is immediate: I didn’t want to give the impression that the item was being “lifted” and then “dropped” — I wanted it to look like the item was pushing its neighbors as it moved.

As a side note, I actually find the classic lifted/dropped effect, with a transparent duplicate of the item hovering above the list as you drag it, much more beautiful and comfortable. Only, you know,… work, requirements and all that.

Design Considerations

Long before my hands were anywhere near the keyboard, I noticed the task at hand wasn’t so straightforward. If I wanted to keep it decoupled and independent of implementation details, there were at least three actors in my play:

  1. The adapter, holding access to the dataset and responsible for creating/recycling views.
  2. The views associated with the items, which may want to be grabbed only from certain areas, and/or redraw themselves to give visual feedback when moving around.
  3. The ListView itself, the one that could actually detect the user’s movements

I tried to organize them hierarchically, so that the mechanism would only rely on one of them and automatically handle the rest, but I found no way of doing it without having objects overstepping their boundaries or making dangerous assumptions. The responsibilities are simply too well-defined: the ListView handles the list presentation logic, each View takes care of its own presentation logic, and the Adapter provides the bridge with the dataset. I also felt that “hard-wiring” the components would heavily restrict the code user’s ability to extend or modify the behavior.

After some thinking, I ended up with three definitions: a RearrangeableListView class extending ListView, and two interfaces to externalize all the behavior that I thought should be external, namely RearrangeListener and MovableView.

The RearrangeListener interface is meant to represent the Adapter, and will most likely be implemented by it. Let’s take a look at it:

interface RearrangeListener {
    void onGrab(int index);
    boolean onRearrangeRequested(int fromIndex, int toIndex);
    void onDrop();
}

The object implementing it is notified when an item in the list is grabbed, dragged or dropped. More importantly, it is responsible for allowing or disallowing rearrangements, and for ensuring the backing dataset reflects the changes appropriately.

The notification behavior is as straightforward as any listener’s. The allowing/disallowing is also quite simple: if the listener returns false from onRearrangeRequested, the move is invalidated; while a return value of true indicates the rearrangement was successful and is already accounted for in the dataset. The reason behind asking the listener to validate moves may not be so intuitive, but remember I didn’t want the ListView to hold any kind of information about the data, so that it could be held and organized in any way. The less assumptions made, the better.

Let’s take a look at how an ArrayAdapter could implement this interface to simply allow and reflect all rearrangements within its data’s bounds:

@Override
public void onGrab(int index) {
    getItem(index).doSomething();
    notifyDataSetChanged();
}
 
@Override
public boolean onRearrangeRequested(int fromIndex, int toIndex) {
     
    if (toIndex >= 0 && toIndex < getCount()) {
        Object item = getItem(fromIndex);
         
        remove(item);
        insert(item, toIndex);
        notifyDataSetChanged();
         
        return true;
    }
     
    return false; 
}

@Override
public void onDrop() {
    doSomethingElse();
    notifyDataSetChanged();
}

This is just an example: the calls to notifyDataSetChanged may or may not be necessary, and both onGrab and onDrop() could be empty snippets.

The second interface I mentioned, MovableView, is defined as follows:

interface MovableView {
    public boolean onGrabAttempt(int x, int y);
    public void onRelease();
}

When the user attempts to grab a View for dragging, and it implements this interface, it can be notified of what’s happening and is allowed to reject a grab attempt, just as the listener is allowed to reject a rearrangement. Why? Well, the View might want to define a specific area from which it can be grabbed, or a certain set of circumstances under which that makes sense. Imagine the typical a list item with a grab handle on the right — the interface would be implemented like this:

@Override
public boolean onGrabAttempt(int x, int y) {
    Rect hitRect = new Rect();
    this.mMyHandleView.getHitRect(hitRect);
    
    return hitRect.contains(x, y);
}

@Override
public void onRelease() {}

Benefits of the design

What are the upsides of demanding the implementation of two interfaces? Well, as I see it, there are several assumptions that we don’t need to make, which allows for greater flexibility and reusability. To name a few:

  1. The dataset format, location and organization is completely irrelevant to the RearrangeableListView.
  2. The Adapter can decide on the fly whether a certain movement is valid, enacting changes in the dataset to add/remove/replace items besides the one being moved.
  3. The View children of the list can have any size, behavior and internal arrangement (even varying between items!), since they are fully responsible for their own piece of the mechanism’s logic.
  4. The ListView can handle UI events only when both the listener and the view approve, and leave room for other components to step in otherwise.

Implementation details

I think most of the code is pretty self-explanatory, so I won’t go into full detail, but there is something I think is worth mentioning: scrolling.

When the user drags an item to the visible bottom (or top) of the list, with the intention to keep dragging it down (or up), the list should automatically swap the held item with the first invisible one, scroll a bit, and keep doing that until the user releases his hold on the item. It took a lot of trial and error to get this behavior right, since several methods are available that sound tempting enough to use but end up introducing more problems than they solve.

The easiest way I found of forcing a scroll on the list was the setSelected() method. In pseudo-java-like code:

if (scrollingUp())
    setSelection(getFirstVisiblePosition() - 1);
else
if (scrollingDown())
    setSelection(getFirstVisiblePosition() + 1);

Now, invoking this code will make the list scroll appropriately once. Calling this from the MotionEvent handler would require the user (or us) to generate additional events just to keep the list scrolling. What I wanted was for the list to automatically scroll at a fixed rate until the user released the item, so instead I wrote a Runnable that executes the above logic and reposts itself with a delay. Again, in java-like pseudocode:

final Runnable autoScroll = new Runnable() {
    @Override
    public void run() {
        
        if (scrolling() && moveApproved()) {
            
            if (scrollingUp())
                setSelection(getFirstVisiblePosition() - 1);
            else
            if (scrollingDown())
                setSelection(getFirstVisiblePosition() + 1);
            
            postDelayed(this, AUTO_SCROLL_DELAY);
        }
    }
};

The advantage of this approach is that we don’t need any additional threads or countdown handlers, since we just rely on the UI’s event loop. To start an autoscroll, a simple call to run() is enough.

Final notes

I intended to give a much more in-depth description of the implementation, but as I said above, I think reading the code will do. Again, the full source code for RearrangeableListView is available in the blogActivity repository. I welcome all comments, bug reports, fixes or improvements — do contact me.

Saludos!

Today, I ran into an unexpected, metaphorical wall: apparently, an Activitiy embedded into another through TabHost can’t properly bind services.

I did a little googling research, and found that there’s a lot of confusion in the Interwebs on how to get around this limitation. Alleged solutions were mostly based on completely breaking the separation of concerns between the host Activity and its children, passing references around and defeating the purpose of using tabs instead of just exchanging views.

In the end, I switched back from Chrome to Eclipse. After looking hard at the screen for a couple of minutes, smoking a cigarette and then staring at my code some more, I finally found a way around it (and, as so often happens, wondered why I didn’t try that from the beginning).

How to do it wrong

Let’s say you use the following code to bind a Service to an Activity.

anActivity.bindService(
    new Intent(anActivity, MyService.class),
    aServiceConnection,
    Context.BIND_AUTO_CREATE
);

It will work as long as anActivity is not an embedded child. Note that the bindService() call is made upon the same Activity object that intends to use the connection, also passed as Context to the Intent.

Why doesn’t it work?

I have absolutely no idea. I intend to take a look at the source and figure it out when I have the time, but seeing as it’s Saturday night, a blind fix will just have to do =).

How to do it right

Simple enough: bind the connection to the host Activity (or maybe the global ApplicationContext), instead of the child. There’s no need to change the Context passed to the Intent or to lose control over the ServiceConnection.

anActivity.getParent().bindService(
    new Intent(anActivity, MyService.class),
    aServiceConnection,
    Context.BIND_AUTO_CREATE
);

The only catch is that, even though the ServiceConnection can be 100% handled from the child Activity, the Service is bound to another Context. This means that, when you no longer need it, you need to invoke unbindService() from the same Context:

anActivity.getParent().unbindService(mConnection)

I haven’t fully experimented with this yet, but I like the approach much better than sharing IBinder instances or exchanging references. It preserves the encapsulation around the activities and allows each child to have its own ServiceConnection. Also, the same Activity can be hosted anywhere without modification, or even started independently: just checking the return of getParent() for null should be enough to decide which Context to use.

That’s it for today. Not a great post, but hopefully it will save someone’s time. Have a good weekend!

First, the improvements.

In my last post, I wrote about a small app I uploaded to the Market last week: Drawable Explorer. It’s a simple tool that lets you navigate, filter and display the standard Drawable resources present in the system, by means of reflection on R.drawable

I just got home and made a slight but useful improvement: it’s now possible, with the click of a button, to either save a resource to an external image file or SEND it through another application in the system.

The updated application is available in the Market, as is the source code in its repository.

Now, the excuses.

I've been meaning to upload more utilities to the blogActivity repository, but I just can't find the time. I have several half-baked classes and interfaces I created for my personal use, that could easily be improved, generalized and shared, but I haven't yet managed to extend my day beyond the unfortunate total of 24 hours -- and that's before substracting the time this impractical body of mine needs to sleep.

I promise to try and find the time to write and upload more. Heh, I'm making apologies as if I had thousands of readers. Oh well, since I don't, there's nobody to find it silly, so what the heck.

Have a good week!