Categories
Android

Handling the multi-touch in a view

In this post you find a full example about how to

  • extend a view to draw, in this case, a circle where you touch the screen
  • implement a listener on a view to handle the multi-touch


In this app the screen is divided into two parts:

  • a top area displaying the data related to the touch:
    1. pointer index: index of an array where the touch information is stored
    2. pointer ID: unique and time constant identifier of a touch
    3. x coordinate of the center
    4. y coordinate of the center
    5. action: action on the touch
  • a bottom area containing the view and a red circle for the first touch and a green circle for the second touch

The following steps show the most important files of this app:

  1. MainActivity.java, the main activity
    package eu.lucazanini.circleview;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.View.OnTouchListener;
    import android.widget.TextView;
    
    public class MainActivity extends Activity {
    
        private final static String MORE_THAN_TWO_TOUCHES = "I don't handle more than two touches";
        private final static String TAG = MainActivity.class.getName();
        private TouchView touchView;
        private TextView[] xValue, yValue, indexValue, pointerValue, actionValue;
    
        private void initViews() {
    	indexValue = new TextView[2];
    	pointerValue = new TextView[2];
    	xValue = new TextView[2];
    	yValue = new TextView[2];
    	actionValue = new TextView[2];
    
    	indexValue[0] = (TextView) findViewById(R.id.indexValue1);
    	indexValue[1] = (TextView) findViewById(R.id.indexValue2);
    
    	pointerValue[0] = (TextView) findViewById(R.id.pointerValue1);
    	pointerValue[1] = (TextView) findViewById(R.id.pointerValue2);
    
    	xValue[0] = (TextView) findViewById(R.id.xValue1);
    	xValue[1] = (TextView) findViewById(R.id.xValue2);
    
    	yValue[0] = (TextView) findViewById(R.id.yValue1);
    	yValue[1] = (TextView) findViewById(R.id.yValue2);
    
    	actionValue[0] = (TextView) findViewById(R.id.actionValue1);
    	actionValue[1] = (TextView) findViewById(R.id.actionValue2);
    
    	touchView = (TouchView) findViewById(R.id.circle);
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
    
    	setContentView(R.layout.activity_main);
    
    	initViews();
    
    	touchView.setOnTouchListener(new OnTouchListener() {
    
    	    @Override
    	    public boolean onTouch(View v, MotionEvent event) {
    
    		final int action = event.getActionMasked();
    		int pointerIndex, pointerId;
    		float x, y;
    
    		switch (action) {
    		case MotionEvent.ACTION_DOWN:
    
    		    pointerIndex = event.getActionIndex();
    		    pointerId = event.getPointerId(pointerIndex);
    		    x = event.getX(pointerIndex);
    		    y = event.getY(pointerIndex);
    		    setTextViews(pointerId, pointerIndex, x, y, "ACTION_DOWN");
    		    moveCircle(pointerId, x, y, action);
    
    		    break;
    		case MotionEvent.ACTION_MOVE:
    		    for (pointerIndex = 0; pointerIndex < event
    			    .getPointerCount(); pointerIndex++) {
    
    			pointerId = event.getPointerId(pointerIndex);
    			x = event.getX(pointerIndex);
    			y = event.getY(pointerIndex);
    			setTextViews(pointerId, pointerIndex, x, y,
    				"ACTION_MOVE");
    			moveCircle(pointerId, x, y, action);
    
    		    }
    		    break;
    		case MotionEvent.ACTION_CANCEL:
    
    		    for (pointerIndex = 0; pointerIndex < event
    			    .getPointerCount(); pointerIndex++) {
    
    			pointerId = event.getPointerId(pointerIndex);
    			setTextViews(pointerId, pointerIndex, "ACTION_CANCEL");
    			deleteCircle(pointerId);
    
    		    }
    		    break;
    		case MotionEvent.ACTION_UP:
    
    		    pointerIndex = event.getActionIndex();
    		    pointerId = event.getPointerId(pointerIndex);
    		    setTextViews(pointerId, pointerIndex, "ACTION_UP");
    		    deleteCircle(pointerId);
    
    		    break;
    		case MotionEvent.ACTION_POINTER_DOWN:
    
    		    pointerIndex = event.getActionIndex();
    		    pointerId = event.getPointerId(pointerIndex);
    		    x = event.getX(pointerIndex);
    		    y = event.getY(pointerIndex);
    		    setTextViews(pointerId, pointerIndex, x, y,
    			    "ACTION_POINTER_DOWN");
    		    moveCircle(pointerId, x, y, action);
    
    		    break;
    		case MotionEvent.ACTION_POINTER_UP:
    
    		    pointerIndex = event.getActionIndex();
    		    pointerId = event.getPointerId(pointerIndex);
    		    setTextViews(pointerId, pointerIndex, "ACTION_POINTER_UP");
    		    deleteCircle(pointerId);
    
    		    break;
    		}
    		touchView.invalidate();
    
    		return true;
    
    	    }
    
    	    private void deleteCircle(int pointerId) {
    
    		if (pointerId == 0) {
    		    touchView.setCircleOne(0, 0, -1);
    		} else if (pointerId == 1) {
    		    touchView.setCircleTwo(0, 0, -1);
    		} else {
    		    Log.d(TAG, MORE_THAN_TWO_TOUCHES);
    		}
    
    	    }
    
    	    private void moveCircle(int pointerId, float x, float y, int action) {
    
    		if (pointerId == 0) {
    		    touchView.setCircleOne(x, y, action);
    		} else if (pointerId == 1) {
    		    touchView.setCircleTwo(x, y, action);
    		}
    
    	    }
    
    	    private void resetCoordinates(int index) {
    		xValue[index].setText("");
    		yValue[index].setText("");
    	    }
    
    	    private void setCoordinates(int index, float x, float y) {
    		xValue[index].setText(String.valueOf(Math.round(x)));
    		yValue[index].setText(String.valueOf(Math.round(y)));
    	    }
    
    	    private void setTextViews(int pointerId, int pointerIndex, float x,
    		    float y, String actionDescription) {
    		if (pointerId < 2) {
    		    indexValue[pointerId].setText(Integer
    			    .toString(pointerIndex));
    		    pointerValue[pointerId].setText(Integer.toString(pointerId));
    		    actionValue[pointerId].setText(actionDescription);
    		    setCoordinates(pointerId, x, y);
    		} else {
    		    Log.d(TAG, MORE_THAN_TWO_TOUCHES);
    		}
    	    }
    
    	    private void setTextViews(int pointerId, int pointerIndex,
    		    String actionDescription) {
    		if (pointerId < 2) {
    		    indexValue[pointerId].setText("");
    		    pointerValue[pointerId].setText("");
    		    actionValue[pointerId].setText(actionDescription);
    		    resetCoordinates(pointerId);
    		} else {
    		    Log.d(TAG, MORE_THAN_TWO_TOUCHES);
    		}
    	    }
    	});
    
        }
    }
    

    from the line 51 onTouchListener I implement the class onTouchListener to handle events for the following actions:

    • ACTION_DOWN: first touch
    • ACTION_MOVE: moving touches
    • ACTION_CANCEL: break of the event (for example the display turns off)
    • ACTION_UP: release of the latest touch
    • ACTION_POINTER_DOWN: a touch after the first
    • ACTION_POINTER_UP: release of a touch with at least one touch yet

    note how I get the event data:

    • action = event.getActionMasked()
    • pointer index = event.getActionIndex() or with a loop from 0 to event.getPointerCount() for the actions (ACTION_MOVE e ACTION_CANCEL) that may relate to multiple touches at the same time
    • pointer id = event.getPointerId([pointer index])
    • x = event.getX([pointer index])
    • y = event.getY([pointer index])

    then I call private methods setTextViews(), moveCircle() and deleteCircle() to which the pointer index has never passed but the pointer id because the pointer id identifies the touch
    at line 187 the method invalidate() method calls the onDraw() method of the TouchView class to redraw the view

  2. res/layout/activity_main.xml, the layout
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="4dp" >
    
            <TextView
                android:id="@+id/indexLabel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:layout_alignParentTop="true"
                android:text="idx"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/pointerLabel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentTop="true"
                android:layout_marginLeft="10dp"
                android:layout_toRightOf="@+id/indexLabel"
                android:text="ID"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/xAxis"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentTop="true"
                android:layout_marginLeft="15dp"
                android:layout_toRightOf="@+id/pointerLabel"
                android:text="X"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/yAxis"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentTop="true"
                android:layout_marginLeft="30dp"
                android:layout_toRightOf="@+id/xAxis"
                android:text="Y"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/actionLabel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentTop="true"
                android:layout_marginLeft="30dp"
                android:layout_toRightOf="@+id/yAxis"
                android:text="Action"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/indexValue1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:layout_below="@+id/indexLabel"
                android:layout_marginTop="15dp"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/pointerValue1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/indexValue1"
                android:layout_alignBottom="@+id/indexValue1"
                android:layout_alignLeft="@+id/pointerLabel"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/xValue1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/pointerValue1"
                android:layout_alignBottom="@+id/pointerValue1"
                android:layout_alignLeft="@+id/xAxis"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/yValue1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/xValue1"
                android:layout_alignBottom="@+id/xValue1"
                android:layout_alignLeft="@+id/yAxis"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/actionValue1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/yValue1"
                android:layout_alignBottom="@+id/yValue1"
                android:layout_alignLeft="@+id/actionLabel"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/indexValue2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:layout_below="@+id/indexValue1"
                android:layout_marginTop="15dp"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/pointerValue2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/indexValue2"
                android:layout_alignBottom="@+id/indexValue2"
                android:layout_alignLeft="@+id/pointerLabel"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/xValue2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/pointerValue2"
                android:layout_alignBottom="@+id/pointerValue2"
                android:layout_alignLeft="@+id/xAxis"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/yValue2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/xValue2"
                android:layout_alignBottom="@+id/xValue2"
                android:layout_alignLeft="@+id/yAxis"
                android:textAppearance="?android:attr/textAppearanceSmall" />
    
            <TextView
                android:id="@+id/actionValue2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/yValue2"
                android:layout_alignBottom="@+id/yValue2"
                android:layout_alignLeft="@+id/actionLabel"
                android:textAppearance="?android:attr/textAppearanceSmall" />
        </RelativeLayout>
    
        <eu.lucazanini.circleview.TouchView
            android:id="@+id/circle"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </LinearLayout>
    
  3. TouchView.java
    package eu.lucazanini.circleview;
    
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.View;
    
    public class TouchView extends View {
    
        private final static String TAG = TouchView.class.getName();
        private Circle circleOne, circleTwo;
    
        public TouchView(Context context, AttributeSet attrs) {
    	super(context, attrs);
    
    	circleOne = new Circle();
    	circleTwo = new Circle();
        }
    
        public void setCircleOne(float x, float y, int action) {
    	circleOne.setX(x);
    	circleOne.setY(y);
    	circleOne.setAction(action);
        }
    
        public void setCircleTwo(float x, float y, int action) {
    	circleTwo.setX(x);
    	circleTwo.setY(y);
    	circleTwo.setAction(action);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
    	super.onDraw(canvas);
    
    	Paint paint = new Paint();
    
    	// draw white canvas
    	paint.setStyle(Paint.Style.FILL);
    	paint.setColor(Color.WHITE);
    	canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
    
    	// draw the circles
    	paint.setAntiAlias(true);
    	if (circleOne != null && circleOne.getAction() != -1) {
    	    paint.setColor(Color.RED);
    	    canvas.drawCircle(circleOne.getX(), circleOne.getY(), 100, paint);
    	}
    	if (circleTwo != null && circleTwo.getAction() != -1) {
    	    paint.setColor(Color.GREEN);
    	    canvas.drawCircle(circleTwo.getX(), circleTwo.getY(), 100, paint);
    	}
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    	int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    	int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    	int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    	int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
    	setMeasuredDimension(widthSize, heightSize);
        }
    
    }
    

    this class extends the class View and overrides two methods

    • onDraw(): it redraws the view, the white background and two circles
    • onMeasure(): it sizes the view, it is not much important in this example
  4. Circle.java
    package eu.lucazanini.circleview;
    
    public class Circle {
    
        private int action;
        private float x, y;
    
        public Circle() {
    	this.x = Float.NaN;
    	this.y = Float.NaN;
    	this.action = -1;
        }
    
        public Circle(int x, int y) {
    	this.x = x;
    	this.y = y;
    	this.action = -1;
        }
    
        public int getAction() {
    	return action;
        }
    
        public float getX() {
    	return x;
        }
    
        public float getY() {
    	return y;
        }
    
        public void setAction(int action) {
    	this.action = action;
        }
    
        public void setX(float x) {
    	this.x = x;
        }
    
        public void setY(float y) {
    	this.y = y;
        }
    
    }
    

Launch the app and touch the screen:
circleview

You can download the project here.

One reply on “Handling the multi-touch in a view”

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.