Touch handling in Android

Sourcecode

You can find the sourcecode over at github:
The Source at github

Introduction

There is allready a lot of information available about touch and multitouch 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 multitouch to you. Also, in the provided sourcecode I have made the execution of the code configurable so you can experiment with touch and multitouch 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) {
	// Do your stuff here
}

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:
		// Do your stuff here
    		break;
    	case MotionEvent.ACTION_MOVE:
		// Do your stuff here
    		break;
    	case MotionEvent.ACTION_UP:
		// Do your stuff here
    		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 respons 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 screencoordinates of the touchevent and an indication of the pressure with which you pressed on the screen.

Touch events and click and longclick

The basics of click and longclick are af 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) {
	// call the base class implementation so that other touch dependent methods get called
	super.onTouchEvent(event);
	// Do your stuff here
}

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, middlefinger and ring finger down, but lift them in the order middlefinger, ring finger and forefinger. In order to keep track of “your finger” Android assigns a pointerid 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 multitouch 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 pointerid with following code:

int action = event.getActionMasked();
int pointerIndex = event.getActionIndex();
int pointerId = event.getPointerId(pointerIndex);
// do your stuff

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 pointerid you must iterate through the provided pointers using the following code:

for(int i = 0; i < event.getPointerCount(); i++)
{
	int curPointerId = event.getPointerId(i);

	// do your stuff
}

Historic events

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 occured 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);

		// in order to get the historical data of the event
		//	you must use the getHistorical... methods
		int x = event.getHistoricalX(i, j);

	}
}

Touch outside the view

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 4 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 happend. This position is set on the onTouchEvent method shown beneath. The radius of the outer circle is dependend 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 = true;
		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 maintainces 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 dependend 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 = true;
		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 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 tocuh 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 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 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 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) {
					// TODO Auto-generated catch block
					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 simplefy 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 multitouch in Android. Allthough the article does not aim at providing any new information, the application in the accompaning sourcecode gives the user the possibility to experiment with different scenario’s and see how Android respons.

External references

How to use Multi-touch in Android 2

Leave a comment