Thursday, October 24, 2013

Handling single and multi touch on Android- Tutorial

Single and multi touch in Android
This tutorial describes how to use the touch API in Android applications.

1. Android Touch

1.1. Android touch basics

The Android standard View class support touch events. You can react to touch events in your custom views and your activities. Android supports multiple pointers, e.g. fingers which are interacting with the screen.
Android touch
The base class for touch support is the MotionEvent class which is passed to Views via theonTouchEvent() method. To react to touch events you override the onTouchEvent() method.
The MotionEvent class contains the touch related information.e.g. the number of pointers, the X/Y coordinates and size and pressure of each pointer.
This method returns true if the touch event has been handled by the view. Android tries to find the deepest view which returns true to handles the touch event. If the view is part of another view (parent view), the parent can claim the event by returning true from the onInterceptTouchEvent()method. This would send an MotionEvent.ACTION_CANCEL event to the view which received previously the touch events.
To react to touch events in an activity, register an OnTouchListener for the relevant Views.

1.2. Single touch

If single input is used you can use the getX() and getY() methods to get the current position of the first finger.
Via the getAction() method you receive the action which was performed. The MotionEvent class provides the following constants to determine the action which was performed.
Table 1. Touch Events
EventDescription
MotionEvent.ACTION_DOWNNew touch started
MotionEvent.ACTION_MOVEFinger is moving
MotionEvent.ACTION_UPFinger went up
MotionEvent.ACTION_CANCELCurrent event has been canceled, something else took control of the touch event
MotionEvent.ACTION_POINTER_DOWNPointer down (multi-touch)
MotionEvent.ACTION_POINTER_UPPointer up (multi-touch)

1.3. Multi touch

Multi-touch is available since Android 2.0 and has been improved in the version 2.2. This description uses the API as of version 2.2.
The MotionEvent.ACTION_POINTER_DOWN and MotionEvent.ACTION_POINTER_UP are send starting with the second finger. For the first finger MotionEvent.ACTION_DOWN andMotionEvent.ACTION_UP are used.
The getPointerCount() method on MotionEvent allows you to determine the number of pointers on the device. All events and the position of the pointers are included in the instance ofMotionEvent which you receive in the onTouch() method.
To track the touch events from multiple pointers you have to use theMotionEvent.getActionIndex() and the MotionEvent.getActionMasked() methods to identify the index of the pointer and the touch event which happened for this pointer.
This pointer index can change over time, e.g. if one finger is lifted from the device. The stable version of a pointer is the pointer id, which can be determined with the getPointerId(pointerIndex)method from the MotionEvent object.
The usage if demonstrated in the following code snippet.
@Override
public boolean onTouchEvent(MotionEvent event) {

// get pointer index from the event object
int pointerIndex = event.getActionIndex();

// get pointer ID
int pointerId = event.getPointerId(pointerIndex);

// get masked (not specific to a pointer) action
int maskedAction = event.getActionMasked();

switch (maskedAction) {

case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
// TODO use data
break;
}
case MotionEvent.ACTION_MOVE: { // a pointer was moved
// TODO use data
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL: {
// TODO use data
break;
}
}
invalidate();

return true;
}

Tip

Multitouch can not be tested on the emulator. You need a real Android device as input device.

1.4. GestureDetectors

Android provide the GestureDetector class which allow to consume MotionEvents and to create higher level gesture events to listeners.
For example the ScaleGestureDetector class allows to determine the predefined gesture of increasing and decreasing the size of the object via two fingers.

2. Prerequisites

The following assumes that you have already basic knowledge in Android development.

3. Exercise: Singletouch Example

3.1. Draw via touch

We will demonstrate Singletouch with an custom View.
Create an Android project called com.vogella.android.touch.single with the activity calledSingleTouchActivity.
Create the following SingleTouchEventView class which implements a View which supports single touch.
package de.vogella.android.touch.single;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class SingleTouchEventView extends View {
private Paint paint = new Paint();
private Path path = new Path();

public SingleTouchEventView(Context context, AttributeSet attrs) {
super(context, attrs);

paint.setAntiAlias(true);
paint.setStrokeWidth(6f);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
}

@Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(path, paint);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
float eventX = event.getX();
float eventY = event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(eventX, eventY);
return true;
case MotionEvent.ACTION_MOVE:
path.lineTo(eventX, eventY);
break;
case MotionEvent.ACTION_UP:
// nothing to do
break;
default:
return false;
}

// Schedules a repaint.
invalidate();
return true;
}
}
Add this view to your activity.
package de.vogella.android.touch.single;

import android.app.Activity;
import android.os.Bundle;

public class SingleTouchActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new SingleTouchEventView(this, null));
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_single_touch, menu);
return true;
}
}
If you run your application you will be able to draw on the screen with your finger (or with the mouse in the emulator).
Change your coding so that you use a layout definition based on XML. Hint: to use your own view in an XML layout definition you have to use the full-qualified class name (class including package information).

3.2. Tracking

Add code to your drawing example so that the current position of a finger is marked via a circle. To draw a circle you can use the addCircle(x, y, 50, Path.Direction.CW) method call on aPath or use the canvas element directly.
Make sure that only the current position is highlighted with a circle. The circle should appears as soon as the finger goes down and vanish once the finger goes up.
The result should look like the following.
Activity showing the drawing View

4. Exercise: Multitouch

In this exercise you create a view which support multitouch and allows you to track several fingers on your device. On the Android emulator you can only simulate singletouch with the mouse.
Create an Android project called com.vogella.android.multitouch with the >activity called MainActivity.
Create the following MultitouchView class.
package com.vogella.android.multitouch;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;

public class MultitouchView extends View {

private static final int SIZE = 60;

private SparseArray<PointF> mActivePointers;
private Paint mPaint;
private int[] colors = { Color.BLUE, Color.GREEN, Color.MAGENTA,
Color.BLACK, Color.CYAN, Color.GRAY, Color.RED, Color.DKGRAY,
Color.LTGRAY, Color.YELLOW };

private Paint textPaint;


public MultitouchView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}

private void initView() {
mActivePointers = new SparseArray<PointF>();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// set painter color to a color you like
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextSize(20);
}

@Override
public boolean onTouchEvent(MotionEvent event) {

// get pointer index from the event object
int pointerIndex = event.getActionIndex();

// get pointer ID
int pointerId = event.getPointerId(pointerIndex);

// get masked (not specific to a pointer) action
int maskedAction = event.getActionMasked();

switch (maskedAction) {

case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
// We have a new pointer. Lets add it to the list of pointers

PointF f = new PointF();
f.x = event.getX(pointerIndex);
f.y = event.getY(pointerIndex);
mActivePointers.put(pointerId, f);
break;
}
case MotionEvent.ACTION_MOVE: { // a pointer was moved
for (int size = event.getPointerCount(), i = 0; i < size; i++) {
PointF point = mActivePointers.get(event.getPointerId(i));
if (point != null) {
point.x = event.getX(i);
point.y = event.getY(i);
}
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL: {
mActivePointers.remove(pointerId);
break;
}
}
invalidate();

return true;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

// draw all pointers
for (int size = mActivePointers.size(), i = 0; i < size; i++) {
PointF point = mActivePointers.valueAt(i);
if (point != null)
mPaint.setColor(colors[i % 9]);
canvas.drawCircle(point.x, point.y, SIZE, mPaint);
}
canvas.drawText("Total pointers: " + mActivePointers.size(), 10, 40 , textPaint);
}

}
Add this view to the layout of your activity.
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.vogella.android.multitouch.MultitouchView
android:layout_width="match_parent"
android:layout_height="match_parent"
/>


</RelativeLayout>
Your generated activity can remain the same.
package com.vogella.android.multitouch;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}

}
If you run your application you will be able to draw on the screen with your fingers. Every device has an upper limit how many pointers are supported, test out how many simultaneous pointers your device supports. This application should look similar to the following screenshot.
Multitouch exercise

5. Tutorial: ScaleGestureDetector

Create the Android project called de.vogella.android.touch.scaledetector with an Activity calledScaleDetectorTestActivity.
Create the following class.
package de.vogella.android.touch.scaledetector;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;

public class ImageViewWithZoom extends View {
private Drawable image;
private float scaleFactor = 1.0f;
private ScaleGestureDetector scaleGestureDetector;

public ImageViewWithZoom(Context context) {
super(context);
image = context.getResources().getDrawable(R.drawable.icon);
setFocusable(true);
image.setBounds(0, 0, image.getIntrinsicWidth(),
image.getIntrinsicHeight());
scaleGestureDetector = new ScaleGestureDetector(context,
new ScaleListener());
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Set the image bounderies
canvas.save();
canvas.scale(scaleFactor, scaleFactor);
image.draw(canvas);
canvas.restore();
}

@Override
public boolean onTouchEvent(MotionEvent event) {
scaleGestureDetector.onTouchEvent(event);
invalidate();
return true;
}

private class ScaleListener extends
ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();

// don't let the object get too small or too large.
scaleFactor = Math.max(0.1f, Math.min(scaleFactor, 5.0f));

invalidate();
return true;
}
}
}
Add this View to your activity.
package de.vogella.android.touch.scaledetector;

import android.app.Activity;
import android.os.Bundle;

public class ScaleDetectorTestActivity extends Activity {

/** Called when the activity is first created. */

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new ImageViewWithZoom(this));
}
}
If you run your application you should be able to shrink and enlarge the image via a multi-touch gesture (pitch zoom).

1 comment:

  1. The blog on handling single and multi-touch on Android is a well-crafted tutorial that demystifies touch input for developers. https://www.mobilezmarket.com/

    ReplyDelete