Tuesday, 1 January 2013

Swipe to strike out an item in ListView

I wanted to add some touch functionality to the basic Android's ListView to make it the way they did it in Any.DO app. I wanted to make each list item strike-able by just swiping my finger from left to right.

The first thing I had to do is to extend ArrayAdapter and then hook up the OnTouch event there. I called it SwipeArrayAdapter. Here is the code:

public class SwipeArrayAdapter extends ArrayAdapter implements OnTouchListener {

private static final String TAG = "SwipeArrayAdapter";
private float mLastX;


public SwipeArrayAdapter(Context context, int resource,
int textViewResourceId, List objects) {
super(context, resource, textViewResourceId, objects);
}


@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = super.getView(position, convertView, parent);
v.setOnTouchListener(this);
return v;
}


@Override
public boolean onTouch(View v, MotionEvent event) {
float currentX = event.getX();
TextView tv = (TextView) v;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = currentX;
Log.v(TAG, String.format("onTouch: ACTION_DOWN for %s", tv.getText().toString()));
break;
case MotionEvent.ACTION_MOVE:
Log.v(TAG, "onTouch: ACTION_MOVE " + currentX);
break;
case MotionEvent.ACTION_UP:
if ( currentX & mLastX + v.getWidth() / 6) {
Log.v(TAG, "Swiped!");
tv.setPaintFlags(tv.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}
break;
}
return true;
}
}

As you can see in the code snippet, I extended ArrayAdapter and implemented OnTouchListener. I then override the getView method from ArrayAdapter and by calling the parent method first I guarantee that the view won't be null and wire the OnTouch event. Notice that I pass this to v.setOnTouchListener. Since I implemented OnTouchListener, we can conveniently pass it as an argument.

Now that I got each List Item wired to an OnTouch event, I proceed to the implementation of the OnTouch method. The most important part in this method is the MotionEvent argument which is passed to it. By switching through its getEvent method, I can query for the view's touch state. The most important states which we should look for are: ACTION_DOWN - when user touch the screen, ACTION_MOVE - when user moves the finger, ACTION_UP - when user picks up the finger off the screen.

I use simple calculation to check if the user swiped the finger from left to right to indicate that the item should be crossed out by storing the initial X position of the touch event and then when the user picks up the finger I check if the current X position is greater than the original X position. The formula I use here is probably not the best. I divide the screen to 6 parts and check if the current X is bigger than the original X + 1/6 of the screen. I would be happy to hear any other solutions for this scenario. But, for the purpose of this demo, I assume it will suffice. Finally, I set the paint flags of the TextView to strike out the text.

One more thing that's worth mentioning is that it's important to return true from the OnTouch method. The reason for that is that when we return true, we tell Android to keep sending OnTouch events to this method, therefore making sure that we also receive ACTION_MOVE and ACTION_UP. Otherwise, if we return false the event will be consumed by another event listener down the chain.