/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.basicmultitouch;
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;
import com.example.android.basicmultitouch.Pools.SimplePool;
/**
* View that shows touch events and their history. This view demonstrates the
* use of {@link #onTouchEvent(android.view.MotionEvent)} and {@link android.view.MotionEvent}s to keep
* track of touch pointers across events.
*/
public class TouchDisplayView extends View {
// Hold data for active touch pointer IDs
private SparseArray<TouchHistory> mTouches;
// Is there an active touch?
private boolean mHasTouch = false;
/**
* Holds data related to a touch pointer, including its current position,
* pressure and historical positions. Objects are allocated through an
* object pool using {@link #obtain()} and {@link #recycle()} to reuse
* existing objects.
*/
static final class TouchHistory {
// number of historical points to store
public static final int HISTORY_COUNT = 20;
public float x;
public float y;
public float pressure = 0f;
public String label = null;
// current position in history array
public int historyIndex = 0;
public int historyCount = 0;
// arrray of pointer position history
public PointF[] history = new PointF[HISTORY_COUNT];
private static final int MAX_POOL_SIZE = 10;
private static final SimplePool<TouchHistory> sPool =
new SimplePool<TouchHistory>(MAX_POOL_SIZE);
public static TouchHistory obtain(float x, float y, float pressure) {
TouchHistory data = sPool.acquire();
if (data == null) {
data = new TouchHistory();
}
data.setTouch(x, y, pressure);
return data;
}
public TouchHistory() {
// initialise history array
for (int i = 0; i < HISTORY_COUNT; i++) {
history[i] = new PointF();
}
}
public void setTouch(float x, float y, float pressure) {
this.x = x;
this.y = y;
this.pressure = pressure;
}
public void recycle() {
this.historyIndex = 0;
this.historyCount = 0;
sPool.release(this);
}
/**
* Add a point to its history. Overwrites oldest point if the maximum
* number of historical points is already stored.
*
* @param point
*/
public void addHistory(float x, float y) {
PointF p = history[historyIndex];
p.x = x;
p.y = y;
historyIndex = (historyIndex + 1) % history.length;
if (historyCount < HISTORY_COUNT) {
historyCount++;
}
}
}
public TouchDisplayView(Context context, AttributeSet attrs) {
super(context, attrs);
// SparseArray for touch events, indexed by touch id
mTouches = new SparseArray<TouchHistory>(10);
initialisePaint();
}
// BEGIN_INCLUDE(onTouchEvent)
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
/*
* Switch on the action. The action is extracted from the event by
* applying the MotionEvent.ACTION_MASK. Alternatively a call to
* event.getActionMasked() would yield in the action as well.
*/
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
// first pressed gesture has started
/*
* Only one touch event is stored in the MotionEvent. Extract
* the pointer identifier of this touch from the first index
* within the MotionEvent object.
*/
int id = event.getPointerId(0);
TouchHistory data = TouchHistory.obtain(event.getX(0), event.getY(0),
event.getPressure(0));
data.label = "id: " + 0;
/*
* Store the data under its pointer identifier. The pointer
* number stays consistent for the duration of a gesture,
* accounting for other pointers going up or down.
*/
mTouches.put(id, data);
mHasTouch = true;
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
/*
* A non-primary pointer has gone down, after an event for the
* primary pointer (ACTION_DOWN) has already been received.
*/
/*
* The MotionEvent object contains multiple pointers. Need to
* extract the index at which the data for this particular event
* is stored.
*/
int index = event.getActionIndex();
int id = event.getPointerId(index);
TouchHistory data = TouchHistory.obtain(event.getX(index), event.getY(index),
event.getPressure(index));
data.label = "id: " + id;
/*
* Store the data under its pointer identifier. The index of
* this pointer can change over multiple events, but this
* pointer is always identified by the same identifier for this
* active gesture.
*/
mTouches.put(id, data);
break;
}
case MotionEvent.ACTION_UP: {
/*
* Final pointer has gone up and has ended the last pressed
* gesture.
*/
/*
* Extract the pointer identifier for the only event stored in
* the MotionEvent object and remove it from the list of active
* touches.
*/
int id = event.getPointerId(0);
TouchHistory data = mTouches.get(id);
mTouches.remove(id);
data.recycle();
mHasTouch = false;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
/*
* A non-primary pointer has gone up and other pointers are
* still active.
*/
/*
* The MotionEvent object contains multiple pointers. Need to
* extract the index at which the data for this particular event
* is stored.
*/
int index = event.getActionIndex();
int id = event.getPointerId(index);
TouchHistory data = mTouches.get(id);
mTouches.remove(id);
data.recycle();
break;
}
case MotionEvent.ACTION_MOVE: {
/*
* A change event happened during a pressed gesture. (Between
* ACTION_DOWN and ACTION_UP or ACTION_POINTER_DOWN and
* ACTION_POINTER_UP)
*/
/*
* Loop through all active pointers contained within this event.
* Data for each pointer is stored in a MotionEvent at an index
* (starting from 0 up to the number of active pointers). This
* loop goes through each of these active pointers, extracts its
* data (position and pressure) and updates its stored data. A
* pointer is identified by its pointer number which stays
* constant across touch events as long as it remains active.
* This identifier is used to keep track of a pointer across
* events.
*/
for (int index = 0; index < event.getPointerCount(); index++) {
// get pointer id for data stored at this index
int id = event.getPointerId(index);
// get the data stored externally about this pointer.
TouchHistory data = mTouches.get(id);
// add previous position to history and add new values
data.addHistory(data.x, data.y);
data.setTouch(event.getX(index), event.getY(index),
event.getPressure(index));
}
break;
}
}
// trigger redraw on UI thread
this.postInvalidate();
return true;
}
// END_INCLUDE(onTouchEvent)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Canvas background color depends on whether there is an active touch
if (mHasTouch) {
canvas.drawColor(BACKGROUND_ACTIVE);
} else {
// draw inactive border
canvas.drawRect(mBorderWidth, mBorderWidth, getWidth() - mBorderWidth, getHeight()
- mBorderWidth, mBorderPaint);
}
// loop through all active touches and draw them
for (int i = 0; i < mTouches.size(); i++) {
// get the pointer id and associated data for this index
int id = mTouches.keyAt(i);
TouchHistory data = mTouches.valueAt(i);
// draw the data and its history to the canvas
drawCircle(canvas, id, data);
}
}
/*
* Below are only helper methods and variables required for drawing.
*/
// radius of active touch circle in dp
private static final float CIRCLE_RADIUS_DP = 75f;
// radius of historical circle in dp
private static final float CIRCLE_HISTORICAL_RADIUS_DP = 7f;
// calculated radiuses in px
private float mCircleRadius;
private float mCircleHistoricalRadius;
private Paint mCirclePaint = new Paint();
private Paint mTextPaint = new Paint();
private static final int BACKGROUND_ACTIVE = Color.WHITE;
// inactive border
private static final float INACTIVE_BORDER_DP = 15f;
private static final int INACTIVE_BORDER_COLOR = 0xFFffd060;
private Paint mBorderPaint = new Paint();
private float mBorderWidth;
public final int[] COLORS = {
0xFF33B5E5, 0xFFAA66CC, 0xFF99CC00, 0xFFFFBB33, 0xFFFF4444,
0xFF0099CC, 0xFF9933CC, 0xFF669900, 0xFFFF8800, 0xFFCC0000
};
/**
* Sets up the required {@link android.graphics.Paint} objects for the screen density of this
* device.
*/
private void initialisePaint() {
// Calculate radiuses in px from dp based on screen density
float density = getResources().getDisplayMetrics().density;
mCircleRadius = CIRCLE_RADIUS_DP * density;
mCircleHistoricalRadius = CIRCLE_HISTORICAL_RADIUS_DP * density;
// Setup text paint for circle label
mTextPaint.setTextSize(27f);
mTextPaint.setColor(Color.BLACK);
// Setup paint for inactive border
mBorderWidth = INACTIVE_BORDER_DP * density;
mBorderPaint.setStrokeWidth(mBorderWidth);
mBorderPaint.setColor(INACTIVE_BORDER_COLOR);
mBorderPaint.setStyle(Paint.Style.STROKE);
}
/**
* Draws the data encapsulated by a {@link TouchDisplayView.TouchHistory} object to a canvas.
* A large circle indicates the current position held by the
* {@link TouchDisplayView.TouchHistory} object, while a smaller circle is drawn for each
* entry in its history. The size of the large circle is scaled depending on
* its pressure, clamped to a maximum of <code>1.0</code>.
*
* @param canvas
* @param id
* @param data
*/
protected void drawCircle(Canvas canvas, int id, TouchHistory data) {
// select the color based on the id
int color = COLORS[id % COLORS.length];
mCirclePaint.setColor(color);
/*
* Draw the circle, size scaled to its pressure. Pressure is clamped to
* 1.0 max to ensure proper drawing. (Reported pressure values can
* exceed 1.0, depending on the calibration of the touch screen).
*/
float pressure = Math.min(data.pressure, 1f);
float radius = pressure * mCircleRadius;
canvas.drawCircle(data.x, (data.y) - (radius / 2f), radius,
mCirclePaint);
// draw all historical points with a lower alpha value
mCirclePaint.setAlpha(125);
for (int j = 0; j < data.history.length && j < data.historyCount; j++) {
PointF p = data.history[j];
canvas.drawCircle(p.x, p.y, mCircleHistoricalRadius, mCirclePaint);
}
// draw its label next to the main circle
canvas.drawText(data.label, data.x + radius, data.y
- radius, mTextPaint);
}
}