In preparation for a five hours flight with my year and a half daughter Hagar, I thought of all kind of toys I could bring along so the confined space wont drive her nuts; I brought a dull (a.k.a Julia), and a toy stroller for Julia (fun fact: in the UK, stroller is called 'pushchair') , some books, and a rattle. I also wanted to bring papers and crayons for her to paint with, but didn't want to totally vandalize the airplane :)
So, what better than a fingerpainting app for Android to play with while on the flight?
A free, fun, simple and multi-touch supporting app in the Market is not an easy thing to find, and after noticing that Google provides sample code for such app, I decided to build one by myself.
It took an hour to improve Google's sample code to include multi-touch. And about five minutes of Hagar's playing with it to notice that I had a bug there! I didn't handle the pointers ID correctly. Fixing wasn't too difficult, and pretty much straight-forward, but I wanted to share it, mostly because non of the posts I found about multi-touch noted it.
Technical stuff*
*For simplicity, I'm only talking about API 8.
Android's MotionEvent will be delivered to the onTouchEvent function on event Up, Down, and Move events; where Up and Down will
provide information about the relevant pointer only, and Move will provide information about ALL active pointers.
In other words, when you get a MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP action from getActionMasked
function you should only handle the specific pointer (given by getActionIndex) , and when you get a MotionEvent.ACTION_MOVE action, you should handle ALL pointers in the event.
Also, it is important to notice that each pointer has an index (the index of the pointer in the event's data list - this may change!),
and an ID (which will not change for the life time of touch point).
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
touch_start(event.getX(event.getActionIndex()), event.getY(event.getActionIndex()), event.getPointerId(event.getActionIndex()));
invalidate();
break;
case MotionEvent.ACTION_MOVE:
for(int pointerIndex=0; pointerIndex<event.getPointerCount(); pointerIndex++)
{
final int pointerId = event.getPointerId(pointerIndex);
float pointerX = event.getX(pointerIndex);
float pointerY = event.getY(pointerIndex);
touch_move(pointerX, pointerY, pointerId);
invalidate();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
touch_up(event.getPointerId(event.getActionIndex()));
invalidate();
break;
}
return true;
}
In the code above:
- On ACTION_DOWN: I created a new Path object associated with the pointer's ID (event.getPointerId(event.getActionIndex()))
- On ACTION_MOVE: I added a path point for ALL pointers.
- On ACTION_UP: I removed the pointer (by its ID) from the tracked pointers map.
Notes:
- Always reference a pointer to its ID! Index is only used for retrieving pointer information (like its X, Y coordinates) from the MotionEvent object.
- Up and Down actions should be used to start/stop tracking code (I'm using it to create a Path object and to associate a color to the pointer)
- Move action's object contain information about ALL active pointers. You can get the count of active pointers using the getPointerCount function. You should iterate ALL active pointers since Android will not tell you which pointer has moved, and furthermore, the event will be called at a regular intervals, so sometimes you may notice that there are no changes in the pointers' coordinates and some times more than one pointer will have new coordinates.