Source code
You can find the latest source code over at github: the source at github.
Introduction
There is already a lot of information available about touch and multi-touch on Android. Consequently, I don’t have the illusion to be providing anything completely new here.
So then why did I write this article?
What I want to try is bundle some information dispersed on the net into a single article and also learn myself by explaining the concepts of touch and multi-touch to you. Also, in the provided source code I have made the execution of the code configurable so you can experiment with touch and multi-touch on your Android phone, or the emulator if you don’t have a phone.
So, without any further ado:
Background
Single touch
Receiving touch events is done in the view by implementing the overridable method onTouchEvent:
@Override
public boolean onTouchEvent(MotionEvent event) {
}
Whenever one or a series of touch events are produced, your method will be called with the parameter event providing details on what exactly has happened. Mind that I have written “or a series of touch events” and not “a single touch event”. Android can buffer some touch events and then call your method providing you with the details of the touch events which have happened. I will give more information about this in the section Historic Events.
So, you created the above method, now how do you know what happened?
The type MotionEvent
of the argument to your method has the method getAction
giving you the kind of touch-action which happened. The main values explained in this article and concerning touch actions are:
ACTION_DOWN
: You’ve touched the screen ACTION_MOVE
: You moved your finger on the screen ACTION_UP
: You removed your finger from the screen ACTION_OUTSIDE
: You’ve touched the screen outside the active view (see Touch outside the view)
Thus, in your code you use a case statement to differentiate between the various actions
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
Mind that the ACTION_OUTSIDE
has nothing to do with moving your finger of the screen. In that case you simply get an ACTION_UP
event.
The normal sequence of events is of course ACTION_DOWN
when you put your finger down, optionally ACTION_MOVE
if you move your finger while touching the screen and finally ACTION_UP
when you remove your finger from the screen.
However, if you return false from the onTouchEvent
override in response to an ACTION_XXX
event, you will not be notified (your method will not be called) about any subsequent events. Android asserts that by returning false you did not process the event and thus are not interested in any further events. Thus you get following table:
Return false on notification of | Receive notification of |
| ACTION_DOWN | ACTION_MOVE | ACTION_UP |
ACTION_DOWN | N.A. | NO | NO |
ACTION_MOVE | N.A. | N.A. | NO |
ACTION_UP | N.A. | N.A. | N.A. |
As an example: say you returned false from the ACTION_DOWN
event, then you will not be notified the ACTION_MOVE
and ACTION_UP
events.
Other data of MotionEvent
The MotionEvent
class has some more methods which provide you with additional information about the event. Those currently supported by the sample application are the screen coordinates of the touch event and an indication of the pressure with which you pressed on the screen.
Touch events and click and longclick
The basics of click and long-click are of course also touch events and they are implemented in de View implementation of onTouchEvent
. This means that if you don’t call the base class implementation, your implementations of onClick
and onLongClick
will not get called.
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
}
Alternatively, if you want to do everything yourself, then don’t call the base class implementation.
Multiple Touch
Multitouch is a little more complex than single touch because with single touch the sequence of events is always the same: down, optionally move and eventually up. With multitouch however, you can get multiple consecutive down or up events and the order of the down events, meaning which finger they represent, is not necessary the same as the order of the move and up events.
You can for example put your forefinger, middle finger and ring finger down, but lift them in the order middle finger, ring finger and forefinger. In order to keep track of “your finger” Android assigns a pointer ID to each event which is constant for the sequence down, move and up.
If your implementation of onTouchEvent
is called, Android provides you for each pointer/finger what happened. For multi-touch Android does not use the ACTION_DOWN
and ACTION_UP
codes but instead the ACTION_POINTER_DOWN
and ACTION_POINTER_UP
. You also must use the method getActionMasked
to get the action. You can get the pointer ID with the following code:
int action = event.getActionMasked();
int pointerIndex = event.getActionIndex();
int pointerId = event.getPointerId(pointerIndex);
As such, you will not receive any events for these actions containing the data for multiple pointers. Thus when you touch with two fingers at what you think is the same time, Android will produce two calls and therefore there will always be one pointer which is first.
For the ACTION_MOVE
however, you can have a single move event for multiple pointers. To get the correct pointer ID you must iterate through the provided pointers using the following code:
for(int i = 0; i < event.getPointerCount(); i++)
{
int curPointerId = event.getPointerId(i);
}
For ACTION_MOVE
, there is not only a list of the events for each pointer, but also a list of ACTION_MOVE
events since the last call of your method. Android caches the events which occurred during subsequent calls of your onTouchEvent
method for ACTION_MOVE
events. To get at these events you must use the following code:
for(int j = 0; j < event.getHistorySize(); j++)
{
for(int i = 0; i < event.getPointerCount(); i++)
{
int curPointerId = event.getPointerId(i);
int x = event.getHistoricalX(i, j);
}
}
To receive the ACTION_OUTSIDE
event, you must set two flags for your window:
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
: Indicates that any touches outside of your view will be send to the views behind it. WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
: Indicates that you want to receive the ACTION_OUTSIDE
event when touched outside your view.
When these two flags are set on your window, then you will receive the ACTION_OUTSIDE
event when a touch happens outside your view.
Do try this at home: the code
The code has four views which allow you to experiment with the various use cases. When you start the application you will see the following screen:
Each entry corresponds with a view allowing you to experiment with that feature. Following is an explanation of what entry corresponds with what view/java file and the configurations possible in that view
Graphics Single: TouchVisualizerSingleTouchGraphicView
@Override
public void onDraw(Canvas canvas) {
if(downX > 0)
{
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(downX, downY, touchCircleRadius, paint);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(downX, downY, touchCircleRadius + pressureRingOffset +
(pressureRingOffset * pressure), paint);
}
}
The onDraw
method simply draws two concentric circles at the position where the last event happened. This position is set on the onTouchEvent
method shown beneath. The radius of the outer circle is dependent on the pressure with which you touched the screen.
@Override
public boolean onTouchEvent(MotionEvent event) {
if(callBaseClass)
{
super.onTouchEvent(event);
}
if(!handleOnTouchEvent)
{
return false;
}
int action = event.getAction();
pressure = event.getPressure() * pressureAmplification;
boolean result = false;
switch (action) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
if (returnValueOnActionDown)
{
result = returnValueOnActionDown;
}
break;
case MotionEvent.ACTION_MOVE:
downX = event.getX();
downY = event.getY();
if (returnValueOnActionMove)
{
result = returnValueOnActionMove;
}
break;
case MotionEvent.ACTION_UP:
downX = -1;
downY = -1;
if (returnValueOnActionUp)
{
result = returnValueOnActionUp;
}
break;
case MotionEvent.ACTION_OUTSIDE:
break;
}
invalidate();
return result;
}
@Override
public void onClick(View v) {
Toast msg = Toast.makeText(TouchVisualizerSingleTouchGraphicView.this.getContext(),
"onClick", Toast.LENGTH_SHORT);
msg.setGravity(Gravity.CENTER, msg.getXOffset() / 2, msg.getYOffset() / 2);
msg.show();
}
@Override
public boolean onLongClick(View v) {
Toast msg = Toast.makeText(TouchVisualizerSingleTouchGraphicView.this.getContext(),
"onLongClick", Toast.LENGTH_SHORT);
msg.setGravity(Gravity.CENTER, msg.getXOffset() / 2, msg.getYOffset() / 2);
msg.show();
return returnValueOnLongClick;
}
As you can see there are a whole bunch of variables which enable you to configure what the behaviour of the activity. The config menu option of the view allow you to configure these variables
The following table maps these variables to the config setting
Configuration | Variable | What it does |
Call base class | callBaseClass | If set the base class will be called first. It allows to test OnClick and OnLongClick behaviour. |
Handle touch events | handleOnTouchEvent | If set the rest of the method will be executed. It allows to test the behaviour as if you didn’t override, for this set the variable callBaseClass to true. |
True on ACTION_DOWN | returnValueOnActionDown | The returnvalue of the onTouchEvent method when ACTION_DOWN is received. This allows you to see what other actions you receive when setting this to true or false. |
True on ACTION_MOVE | returnValueOnActionMove | The returnvalue of the onTouchEvent method when ACTION_MOVE is received. This allows you to see what other actions you receive when setting this to true or false. |
True on ACTION_UP | returnValueOnActionUp | The returnvalue of the onTouchEvent method when ACTION_UP is received. This allows you to see what other actions you receive when setting this to true or false. |
True on onLongClick | returnValueOnLongClick | The value returned from the onLongClick method |
Pressure amplification | pressureAmplification | The diameter of the circles shown when putting your finger on the screen is influenced by the pressure with which you press on the screen. This variable allows to amplify this influence. |
Graphics Multi: TouchVisualizerMultiTouchGraphicView
@Override
public void onDraw(Canvas canvas) {
for(EventData event : eventDataMap.values())
{
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(event.x, event.y, touchCircleRadius, paint);
paint.setStyle(Paint.Style.STROKE);
if(event.pressure <= 0.001)
{
paint.setColor(Color.RED);
}
canvas.drawCircle(event.x, event.y, touchCircleRadius + pressureRingOffset +
(pressureRingOffset * event.pressure), paint);
}
}
The onDraw
method iterates through a list which maintains for each pointer (thus finger) what happened last and draws two concentric circles at the position of each event. This list is maintained in the onTouchEvent
method shown beneath. Again, the radius of the outer circle is dependent on the pressure with which you touched the screen.
@Override
public boolean onTouchEvent(MotionEvent event) {
if(callBaseClass)
{
super.onTouchEvent(event);
}
if(!handleOnTouchEvent)
{
return false;
}
int action = event.getActionMasked();
int pointerIndex = event.getActionIndex();
int pointerId = event.getPointerId(pointerIndex);
boolean result = false;
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
EventData eventData = new EventData();
eventData.x = event.getX(pointerIndex);
eventData.y = event.getY(pointerIndex);
eventData.pressure = event.getPressure(pointerIndex) * pressureAmplification;
eventDataMap.put(new Integer(pointerId), eventData);
if (returnValueOnActionDown)
{
result = returnValueOnActionDown;
}
break;
case MotionEvent.ACTION_MOVE:
for(int i = 0; i < event.getPointerCount(); i++)
{
int curPointerId = event.getPointerId(i);
if(eventDataMap.containsKey(new Integer(curPointerId)))
{
EventData moveEventData = eventDataMap.get(new Integer(curPointerId));
moveEventData.x = event.getX(i);
moveEventData.y = event.getY(i);
moveEventData.pressure = event.getPressure(i) * pressureAmplification;
}
}
if (returnValueOnActionMove)
{
result = returnValueOnActionMove;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
eventDataMap.remove(new Integer(pointerId));
if (returnValueOnActionUp)
{
result = returnValueOnActionUp;
}
break;
case MotionEvent.ACTION_OUTSIDE:
break;
}
invalidate();
return result;
}
@Override
public void onClick(View v) {
Toast msg = Toast.makeText(TouchVisualizerMultiTouchGraphicView.this.getContext(),
"onClick", Toast.LENGTH_SHORT);
msg.setGravity(Gravity.CENTER, msg.getXOffset() / 2, msg.getYOffset() / 2);
msg.show();
}
@Override
public boolean onLongClick(View v) {
Toast msg = Toast.makeText(TouchVisualizerMultiTouchGraphicView.this.getContext(),
"onLongClick", Toast.LENGTH_SHORT);
msg.setGravity(Gravity.CENTER, msg.getXOffset() / 2, msg.getYOffset() / 2);
msg.show();
return handleOnLongClick;
}
The same configuration variables reappear as in the TouchVisualizerSingleTouchGraphicView
. You can look in de table there for what they mean.
History Multi: TouchVisualizeMultiTouchHistoricView
@Override
public void onDraw(Canvas canvas) {
for(List<EventData> path : eventDataMap.values())
{
boolean isFirst = true;
EventData previousEvent = null;
for(EventData event : path)
{
if (isFirst)
{
previousEvent = event;
isFirst = false;
continue;
}
paint.setColor(Color.WHITE);
if(event.historical)
{
paint.setColor(Color.RED);
}
canvas.drawLine(previousEvent.x, previousEvent.y, event.x, event.y, paint);
previousEvent = event;
}
}
}
The onDraw
method again iterates a list of events captured in the onTouchEvent
method. However, all events originate from a single touch down, move and up sequence. If it is a historical event, the line is drawn in red, otherwise the line is white.
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
boolean result = handleOnTouchEvent;
int action = event.getActionMasked();
int pointerIndex = event.getActionIndex();
int pointerId = event.getPointerId(pointerIndex);
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
EventData eventData = new EventData();
eventData.x = event.getX(pointerIndex);
eventData.y = event.getY(pointerIndex);
eventData.pressure = event.getPressure(pointerIndex);
List<EventData> path = new Vector();
path.add(eventData);
eventDataMap.put(new Integer(pointerId), path);
break;
case MotionEvent.ACTION_MOVE:
if(handleHistoricEvent)
{
for(int j = 0; j < event.getHistorySize(); j++)
{
for(int i = 0; i < event.getPointerCount(); i++)
{
int curPointerId = event.getPointerId(i);
if(eventDataMap.containsKey(new Integer(curPointerId)))
{
List<EventData> curPath =
eventDataMap.get(new Integer(curPointerId));
EventData moveEventData = new EventData();
moveEventData.x = event.getHistoricalX(i, j);
moveEventData.y = event.getHistoricalY(i, j);
moveEventData.pressure = event.getHistoricalPressure(i, j);
moveEventData.historical = true;
curPath.add(moveEventData);
}
}
}
}
for(int i = 0; i < event.getPointerCount(); i++)
{
int curPointerId = event.getPointerId(i);
if(eventDataMap.containsKey(new Integer(curPointerId)))
{
List<EventData> curPath = eventDataMap.get(new Integer(curPointerId));
EventData moveEventData = new EventData();
moveEventData.x = event.getX(i);
moveEventData.y = event.getY(i);
moveEventData.pressure = event.getPressure(i);
moveEventData.historical = false;
curPath.add(moveEventData);
}
}
if(pauseUIThread != 0)
{
try {
Thread.sleep(pauseUIThread);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
eventDataMap.remove(new Integer(pointerId));
break;
case MotionEvent.ACTION_OUTSIDE:
break;
}
invalidate();
return result;
}
To simplify things a bit here, I removed the configuration variables from the previous views and just left in two variables which allow you to experiment with the historical events.
Configuration | Variable | What it does |
Handle historic events | handleHistoricEvent | If set, historical events will also be added to the list of events |
Pause UI Thread | pauseUIThread | A value indicating, in milliseconds, how long the UIThread will be paused during processing of onTouchEvent . If you set this longer, more events should be cached as historical events by Android. |
Dialog: TouchVisualizerSingleTouchDialog
public TouchVisualizerSingleTouchDialog(Context context) {
super(context);
registerForOutsideTouch =
((TouchVisualizerSingleTouchDialogActivity)context).getRegisterForOutsideTouch();
handleActionOutside =
((TouchVisualizerSingleTouchDialogActivity)context).getHandleActionOutside();
if(registerForOutsideTouch) {
Window window = this.getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
window.setFlags(LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
}
this.setContentView(R.layout.custom_dialog);
this.setTitle("Custom Dialog");
}
public boolean onTouchEvent(MotionEvent event) {
if (handleActionOutside) {
if(event.getAction() == MotionEvent.ACTION_OUTSIDE){
this.dismiss();
}
}
return false;
}
Here also there are configuration variables which allow you to play with this use case.
Configuration | Variable | What it does |
Register outsidetouch | registerForOutsideTouch | To receive ACTION_OUTSIDE events, you must register for them in the constructor of your view. This variable enables you to do this. |
Handle ACTION_OUTSIDE | handleActionOutside | Of course, you must also handle the ACTION_OUTSIDE event. |
Conclusion
A lot has been written already about multi-touch in Android. Although the article does not aim at providing any new information, the application in the accompanying source code gives the user the possibility to experiment with different scenario’s and see how Android response.
External references