Drawing in Android

November 28, 2012

Source code

You can find the latest source code over at github: the source at github.

Introduction

In my previous post I tried to provide a kind of one stop shop for things about multi touch on the Android platform. My initial intention was to do the same for drawing in 2D.

However, while documenting myself I quickly came to the conclusion that this subject is big. So I have reset my target. My main concern was with how to define shapes in XML resources, draw shapes and then to apply some transformations on these objects.

And that is what I will cover in this post: define drawing objects in XML resources and then apply transformations, like roating, scaling, etc… on them.

So, without aby further ado:

Background

Drawing from XML resources

Drawing from XML resources requires 3 steps:

  1. Define the resource in XML
  2. Load the resource
  3. Draw the resource on the screen

Step 1: Defining the shape as a resource

You define the resource in the resource directory of your project. You will notice you have several resource folders:

  • drawable
  • drawable-ldpi
  • drawable-mdpi
  • drawable-hdpi

This allows you to specify specific resources according to the screen resolution of the targetted Android device. The important word in previous sentence is “resolution”. Mind that this is not the same as screensize! Stackoverflow has a very nice postabout this.

Defining the shape in the xml goes like this:

<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
    <stroke android:width="2px" android:color="#ffffff" />
    <solid android:color="#ffffd300"/>
</shape>

Step 2: Loading a resource as a shape

Resources res = context.getResources();
try {
	rectangleDrawable = (Drawable) res.getDrawable(R.drawable.rectangle_drawable);
} catch (Exception ex) {
	Log.e("Error", "Exception loading drawable: " + ex.getMessage());
}

In the above code, the context variable is the context as given in the constructor of your view class, or if you are loading the resources inside your activity, then you can use the this variable

rectangle_drawable is the name of the xml file created in Step 1

Step 3: Drawing the shapes

protected void onDraw(Canvas canvas) {
	rectangleDrawable.setBounds(x, y, x + width, y + height);
	rectangleDrawable.draw(canvas);
}

Drawing shapes is simple: you set the size of the shape and call the draw method of the Drawable class providing it the canvas on wich to draw.

Draw shapes created in code

Drawing shapes in code requires 2 steps:

  1. Create the shape in code
  2. Draw the shape

Step 1: Create the shape in code

rectangleDrawable = new ShapeDrawable(new RectShape());
ovalDrawable = new ShapeDrawable(new OvalShape());
arcDrawable = new ShapeDrawable(new ArcShape(45, 300));

Creating shapes in code is not at all difficult, but if you’re used to creating shapes in XML you might be in for a surprise: allthough you may think that providing a shape specific class to the constructor of the ShapeDrawable is similar as defining the shape of the drawable, this is NOT the same as choosing the shape to draw in the XML file. The class of the shapes created by defining them in XML is GradientDrawable and NOT ShapeDrawable.

Step 2: Drawing the shape

arcDrawable.getPaint().setColor(0xff74AC23);
arcDrawable.setBounds(x, y, x + width, y + height);
arcDrawable.draw(canvas);

Drawing the shapes is the same is when drawing shapes created from resources.

Applying transformations: using the Matrix class

I will not explain the basics of transformations and how matrix algebra fits in. There are enough references on the internet explaining this.

Using the matrix class,you can chain several types of tranformations together. The four supported types which you can apply are:

  1. Translation
  2. Rotation
  3. Scaling
  4. Skew

Each of these transformations has 3 methods in the Matrix class:

  1. A method to apply the transformation in front of all current transformations of the Matrix. This method starts with pre followed by the name of the tranformationtype
  2. A method to apply the transformation in the end of all current transformations of the Matrix. This method starts with post followed by the name of the tranformationtype
  3. A method to apply the tranformation instead of all current transformations of the Matrix. This method starts with set followed by the name of the tranformationtype

An important remark: a transformation is always made with repect to a point (except for a translation). This point is the origin of the drawing canvas, and in Android this point is at the upper left corner of the screen.

Translation

A translation in the xy-plane is defined by movement along the x-axis and along the y-axis.

A translation is done by any of the methods preTranslate, postTranslate and setTranslate

Rotation

A rotation in the xy-plane is defined by an angle expressed in degrees. The rotation is around the origin of the screen which is, as allready mentioned above, at the upper left corner of the screen.

Scaling

A scaling in the xy-plane, which in the case of Android is anisotropic, is defined by a scaling along the x-axis and a scaling along the y-axis. The scaling is again with respect to the origin of the screen, thus the upper left corner. As a result also the distance of the object you draw on the screen is multiplied by the scaling factor.

Skew

A skew, or in mathematics known as shear in the xy plane is also defined by two variables: kx and ky.

Applying transformations

Applying transformations in code is done as follows:

//create a matrix
Matrix matrix;
matrix = new Matrix();

//apply the transformations
//notice that by using the preScale method, the
//scaling will be applied before the translation
matrix.postTranslate(dx, dy);
matrix.preScale(sx, sy);

//concatenate the transformation to any 
//transformations allready applied to the canvas
canvas.concat(matrix);

//do your thing
rectangleDrawable.draw(canvas);

Applying transformations: using the Camera class

While the Matrix class allows you to define simple tranformations, the Camera class allows you to define transformations as they would happen when looking at the drawing plane from a certain point of view in space.

As such, it defines methods allowing you to position the camera in space:

  1. rotationX: rotation around the X-axis
  2. rotationY: rotation around the Y-axis
  3. rotationZ: rotation around the Z-axis
  4. translate: movement along the X, Y and Z-axis.

Rotation around the main axises

Rotations around the X-axis and Y-axis result in a type of deformation of the object you are transforming which can not be reproduced by a transformation defined by using a Matrix transformation. A rotation around the Z-axis is comparable with a regular rotation as defined by a Matrix tranformation.

Translation along the main axises

Translation along the X and Y axis corresponds to a regular translation using a Matrix transformation. A translation along the Z-axis can be seen as a scaling using a regular Matrix tranformation.

Applying transformations

Applying transformations in code is done as follows:

//create a camera
Camera camera;
camera = new Camera();

//apply the desired transformations
camera.rotateX(rotationX);

//get the transformation matrix
Matrix cameraMatrix = new Matrix();
camera.getMatrix(cameraMatrix);

//concatenate the transformation to any 
//transformations allready applied to the canvas
canvas.concat(matrix);

//do your thing
rectangleDrawable.draw(canvas);

Do try this at home: the code

The code has four views allowing you to experiment with what has been explained above. 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:

Drawable in XML: CustomDrawableFromXMLView

public CustomDrawableFromXMLView(Context context) {
	super(context);

	Resources res = context.getResources();
	try {
		rectangleDrawable = (Drawable) res.getDrawable(R.drawable.rectangle_drawable);
		ovalDrawable = (Drawable) res.getDrawable(R.drawable.oval_drawable);
		lineDrawable = (Drawable) res.getDrawable(R.drawable.line_drawable);
		ringDrawable = (Drawable) res.getDrawable(R.drawable.ring_drawable);
	} catch (Exception ex) {
	   Log.e("Error", "Exception loading drawable: " + ex.getMessage());
	}

}

protected void onDraw(Canvas canvas) {
	if(fSetBounds)
	{
		rectangleDrawable.setBounds(x, y, x + width, y + height);
		ovalDrawable.setBounds(x, y, x + width, y + height);
		lineDrawable.setBounds(x, y, x + width, y + height);
		ringDrawable.setBounds(x, y, x + width, y + height);
	}
	if(fDrawRectangle)
		if(fCastToShapeDrawable)
			((ShapeDrawable)rectangleDrawable).draw(canvas);
		else if(fCastToGradientDrawable)
			((GradientDrawable)rectangleDrawable).draw(canvas);
		else
			rectangleDrawable.draw(canvas);
	if(fDrawOval)
		ovalDrawable.draw(canvas);
	if(fDrawLine)
		lineDrawable.draw(canvas);
	if(fDrawRing)
		ringDrawable.draw(canvas);
}

As is the intention of these posts, some variables allow you to experiment with a few things hich have been stated in the theory above. The config menu of the screen allows you to configure these variables.

Following table maps the configuration with a variable in the code and what it allows you to experiment with:

Configuration Variable What it does
Draw rectangle fDrawRectangle Allows to choose if you want to draw a rectangle. The variables fDrawLine, fDrawOval and fDrawRing allow you to do the same for an line, oval and ring
Set bounds fSetBounds If set to true, the bounds of the drawables will be set in the code.
As ShapeDrawable fCastToShapeDrawable If set to true, the shape loaded from the resource will first be casted to an object of type ShapeDrawable before its draw-method is called. This will result in an exception being thrown. This variable only has effect when a rectangle is choosen to be drawn.
As GradientDrawable fCastToGradientDrawable If set to true, the shape loaded from the resource will first be casted to an object of type GradientDrawable before its draw-method is called. This variable only has effect when a rectangle is choosen to be drawn.

Drawables in code: CustomDrawableView

public CustomDrawableView(Context context) {
	super(context);

	rectangleDrawable = new ShapeDrawable(new RectShape());
	ovalDrawable = new ShapeDrawable(new OvalShape());
	arcDrawable = new ShapeDrawable(new ArcShape(45, 300));
// Line and Ring are not available. In XML, these come from the GradientDrawable class
//    	lineDrawable = new ShapeDrawable(new OvalShape());
//    	ringDrawable = new ShapeDrawable(new OvalShape());
}

protected void onDraw(Canvas canvas) {
	arcDrawable.getPaint().setColor(0xff74AC23);

	if(fSetBounds)
	{
		rectangleDrawable.setBounds(x, y, x + width, y + height);
		ovalDrawable.setBounds(x, y, x + width, y + height);
		arcDrawable.setBounds(x, y, x + width, y + height);
//    	lineDrawable.setBounds(x, y, x + width, y + height);
//	    ringDrawable.setBounds(x, y, x + width, y + height);
	}
	if(fDrawRectangle)
		rectangleDrawable.draw(canvas);
	if(fDrawOval)
		ovalDrawable.draw(canvas);
	if(fDrawArc)
		arcDrawable.draw(canvas);
//  if(fDrawLine)
//  	lineDrawable.draw(canvas);
//  if(fDrawRing)
//    	ringDrawable.draw(canvas);
}

Again, the config menu allows to experiment with a few settings:

Following variables allow you to experiment with this view.

Configuration Variable What it does
Draw rectangle fDrawRectangle Allows to choose if you want to draw a rectangle. The variables fDrawOval and fDrawArc allow you to do the same for an oval and arc
Set bounds fSetBounds If set to true, the bounds of the drawables will be set in the code.

Transformations using the Matrix class: CustomDrawableMatrixTransformationView

protected void onDraw(Canvas canvas) {
	int rectX = this.getWidth()/2 - (width/2);
	int rectY = this.getHeight()/2 - (height/2);
	rectangleDrawable.setBounds(rectX, rectY, rectX + width, rectY + height);

	//do the same for the oval drawable

	Matrix matrix;
	matrix = new Matrix();

	if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER) && pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER))
		matrix.postTranslate(-1 * this.getWidth() / 2, -1 * this.getHeight() / 2);

	int sequence = 0;
	while(pipeline.containsKey("MTX_" + Integer.toString(sequence)))
	{
		Bundle currTransform = (Bundle)pipeline.getParcelable("MTX_" + Integer.toString(sequence));
		String transformationType = currTransform.getString(CustomDrawableMatrixTransformationConfigActivity.TRANSFORMATION_TYPE);
		if(transformationType.equals(MatrixTransformationRotateConfigActivity.TRANSFORMATION_ROTATE))
		{
			float rotation = currTransform.getFloat(MatrixTransformationRotateConfigActivity.ROTATE_ANGLE);
			String transformationOrderType = currTransform.getString(CustomDrawableMatrixTransformationConfigActivity.TRANSFORMATIONORDER_TYPE); 
			if(transformationOrderType.equals(CustomDrawableMatrixTransformationConfigActivity.TRANSFORMATIONORDER_SET))
			{
				matrix.setRotate(rotation);
			}
			if(transformationOrderType.equals(CustomDrawableMatrixTransformationConfigActivity.TRANSFORMATIONORDER_PRE))
			{
				matrix.preRotate(rotation);
			}
			if(transformationOrderType.equals(CustomDrawableMatrixTransformationConfigActivity.TRANSFORMATIONORDER_POST))
			{
				matrix.postRotate(rotation);
			}
		}

		//similar code for translation, scale and skew tranformations

		sequence++;
	}

	if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER) && pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER))
		matrix.postTranslate(this.getWidth() / 2, this.getHeight() / 2);

	final Matrix currentMatrix = canvas.getMatrix();

	canvas.concat(matrix);
	if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM) && !pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM))
		ovalDrawable.draw(canvas);
	rectangleDrawable.draw(canvas);

	canvas.setMatrix(currentMatrix);
	if(!pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM) || pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM))
		ovalDrawable.draw(canvas);

}

In the above code extract, I only show the code for a rotation. I left out the code for the translation, scaling and skew because it is very similar to that of rotation. You can see the complete code in the attached files.

In the configuration menu you can configure the following:

Configuration Variable What it does
Add New Transform none Allows you concatenate various transformations. If you click this button you will get the following screen allowing you to select the type transformation:

Translate to center TRANSLATETOCENTER Allows you to set the center of the tranformations to the center of the screen, instead of the upper-left corner. This is done by prepending all transformations with a translation to the center of the screen. Afterward, the same translation in the opposite direction is appended.
Translate to center TRANSLATETOCENTER Allows you to set the center of the tranformations to the center of the screen, instead of the upper-left corner. This is done by prepending all transformations with a translation to the center of the screen. Afterward, the same translation in the opposite direction is appended.
Oval out of transform OVALOUTTRANSFORM If set, the drawing of the oval is done outside the transformation. Therefore, the oriiginal transformation matrix of the canvas is saved and restored before drawing the oval.

When selecting a type of transformation you will get the chance to configure other variables:

Configuration Screen What it does
Rotation Rotation has two variables to play with:

  1. order: if the transformation is prepended (pre-method), appended (post-method) or replaces (set-method).
  2. rotation: the angle of rotation around the origin of the screen.
Translation Translation has three variables to play with:

  1. order: if the transformation is prepended (pre-method), appended (post-method) or replaces (set-method).
  2. dX: the translation along the X-axis
  3. dY: the translation along the Y-axis
Scaling Scaling has three variables to play with:

  1. order: if the transformation is prepended (pre-method), appended (post-method) or replaces (set-method).
  2. sX: the scaling along the X-axis
  3. sY: the scaling along the Y-axis
Skew Skew has three variables to play with:

  1. order: if the transformation is prepended (pre-method), appended (post-method) or replaces (set-method).
  2. kX: the skew along the X-axis
  3. kY: the skew along the Y-axis

By using the button it is possible to build a list of transformations to apply, and by selecting the appropriate order of each transformation you can experiment with the order in which the transformations are applied.

Transformations using the Camera class: CustomDrawableCameraTransformationView

protected void onDraw(Canvas canvas) {
	//final Matrix currentMatrix = canvas.getMatrix();

	int rectX = this.getWidth()/2 - (width/2);
	int rectY = this.getHeight()/2 - (height/2);
	rectangleDrawable.setBounds(rectX, rectY, rectX + width, rectY + height);

	//do the same for the oval

	Matrix matrix;
	matrix = new Matrix();

	Camera camera;
	camera = new Camera();

	if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER) && pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER))
		//camera.translate(-1 * this.getWidth() / 2, -1 * this.getHeight() / 2, 0);
		matrix.postTranslate(-1 * this.getWidth() / 2, -1 * this.getHeight() / 2);

	int sequence = 0;
	while(pipeline.containsKey("MTX_" + Integer.toString(sequence)))
	{
		Bundle currTransform = (Bundle)pipeline.getParcelable("MTX_" + Integer.toString(sequence));
		String transformationType = currTransform.getString(CustomDrawableCameraTransformationConfigActivity.TRANSFORMATION_TYPE);
		if(transformationType.equals(CameraTransformationRotateXConfigActivity.TRANSFORMATION_ROTATEX))
		{
			float rotationX = currTransform.getFloat(CameraTransformationRotateXConfigActivity.ROTATEX_ANGLE);
			camera.rotateX(rotationX);
		}

		//similar code for roation around Y and Z and for tranlation

		sequence++;
	}   

	Matrix cameraMatrix = new Matrix();
	camera.getMatrix(cameraMatrix);
	matrix.postConcat(cameraMatrix);

	if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER) && pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER))
		//camera.translate(this.getWidth() / 2, this.getHeight() / 2, 0);
		matrix.postTranslate(this.getWidth() / 2, this.getHeight() / 2);

	final Matrix currentMatrix = canvas.getMatrix();

	//camera.applyToCanvas(canvas);
	canvas.concat(matrix);
	if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM) && !pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM))
		ovalDrawable.draw(canvas);
	rectangleDrawable.draw(canvas);

	canvas.setMatrix(currentMatrix);
	if(!pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM) || pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM))
		ovalDrawable.draw(canvas);

}

Again, the above code is a stripped down version of the code you can find in the attached files.

The configuration menu allows to experiment with following:

Configuration Variable What it does
Add New Transform none Allows you concatenate various transformations. If you click this button you will get the following screen allowing you to select the type transformation:

Translate to center TRANSLATETOCENTER Allows you to set the center of the tranformations to the center of the screen, instead of the upper-left corner. This is done by prepending all transformations with a translation to the center of the screen. Afterward, the same translation in the opposite direction is appended.
Oval out of transform OVALOUTTRANSFORM If set, the drawing of the oval is done outside the transformation. Therefore, the oriiginal transformation matrix of the canvas is saved and restored before drawing the oval.

When selecting a type of transformation you will get the chance to configure other variables:

Configuration Screen What it does
Rotation around the X-axis Rotation around the X-axis has one variable to play with:

  1. rotation: the angle of the rotation.
Rotation around the Y-axis Rotation around the Y-axis has one variable to play with:

  1. rotation: the angle of the rotation.
Rotation around the Z-axis Rotation around the Z-axis has one variable to play with:

  1. rotation: the angle of the rotation.
Translation Translation has three variables to play with:

  1. dX: the translation along the X-axis
  2. dY: the translation along the Y-axis
  3. dZ: the translation along the Z-axis

Conclusion

As stated in the beginning, the subject of drawing in Android is big. Although the article does not aim at providing all possible information, it does show some basic scenarios and the application in the accompanying source code gives the user the possibility to experiment with these different scenario?s and see how Android response.

Touch handling in Android

September 12, 2012

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

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: