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:
- pointer index: index of an array where the touch information is stored
- pointer ID: unique and time constant identifier of a touch
- x coordinate of the center
- y coordinate of the center
- 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:
- 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 - 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>
- 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
- 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:
You can download the project here.
Leave a Reply