1
/*
2
* Copyright (C) 2013 The Android Open Source Project
3
*
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
7
*
8
* http://www.apache.org/licenses/LICENSE-2.0
9
*
10
* Unless required by applicable law or agreed to in writing, software
11
* distributed under the License is distributed on an "AS IS" BASIS,
12
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
* See the License for the specific language governing permissions and
14
* limitations under the License.
15
*/
16
17
package com.example.android.basicmultitouch;
18
19
import android.content.Context;
20
import android.graphics.Canvas;
21
import android.graphics.Color;
22
import android.graphics.Paint;
23
import android.graphics.PointF;
24
import android.util.AttributeSet;
25
import android.util.SparseArray;
26
import android.view.MotionEvent;
27
import android.view.View;
28
29
import com.example.android.basicmultitouch.Pools.SimplePool;
30
31
/**
32
* View that shows touch events and their history. This view demonstrates the
33
* use of {@link #onTouchEvent(android.view.MotionEvent)} and {@link android.view.MotionEvent}s to keep
34
* track of touch pointers across events.
35
*/
36
public class TouchDisplayView extends View {
37
38
// Hold data for active touch pointer IDs
39
private SparseArray<TouchHistory> mTouches;
40
41
// Is there an active touch?
42
private boolean mHasTouch = false;
43
44
/**
45
* Holds data related to a touch pointer, including its current position,
46
* pressure and historical positions. Objects are allocated through an
47
* object pool using {@link #obtain()} and {@link #recycle()} to reuse
48
* existing objects.
49
*/
50
static final class TouchHistory {
51
52
// number of historical points to store
53
public static final int HISTORY_COUNT = 20;
54
55
public float x;
56
public float y;
57
public float pressure = 0f;
58
public String label = null;
59
60
// current position in history array
61
public int historyIndex = 0;
62
public int historyCount = 0;
63
64
// arrray of pointer position history
65
public PointF[] history = new PointF[HISTORY_COUNT];
66
67
private static final int MAX_POOL_SIZE = 10;
68
private static final SimplePool<TouchHistory> sPool =
69
new SimplePool<TouchHistory>(MAX_POOL_SIZE);
70
71
public static TouchHistory obtain(float x, float y, float pressure) {
72
TouchHistory data = sPool.acquire();
73
if (data == null) {
74
data = new TouchHistory();
75
}
76
77
data.setTouch(x, y, pressure);
78
79
return data;
80
}
81
82
public TouchHistory() {
83
84
// initialise history array
85
for (int i = 0; i < HISTORY_COUNT; i++) {
86
history[i] = new PointF();
87
}
88
}
89
90
public void setTouch(float x, float y, float pressure) {
91
this.x = x;
92
this.y = y;
93
this.pressure = pressure;
94
}
95
96
public void recycle() {
97
this.historyIndex = 0;
98
this.historyCount = 0;
99
sPool.release(this);
100
}
101
102
/**
103
* Add a point to its history. Overwrites oldest point if the maximum
104
* number of historical points is already stored.
105
*
106
* @param point
107
*/
108
public void addHistory(float x, float y) {
109
PointF p = history[historyIndex];
110
p.x = x;
111
p.y = y;
112
113
historyIndex = (historyIndex + 1) % history.length;
114
115
if (historyCount < HISTORY_COUNT) {
116
historyCount++;
117
}
118
}
119
120
}
121
122
public TouchDisplayView(Context context, AttributeSet attrs) {
123
super(context, attrs);
124
125
// SparseArray for touch events, indexed by touch id
126
mTouches = new SparseArray<TouchHistory>(10);
127
128
initialisePaint();
129
}
130
132
@Override
133
public boolean onTouchEvent(MotionEvent event) {
134
135
final int action = event.getAction();
136
137
/*
138
* Switch on the action. The action is extracted from the event by
139
* applying the MotionEvent.ACTION_MASK. Alternatively a call to
140
* event.getActionMasked() would yield in the action as well.
141
*/
142
switch (action & MotionEvent.ACTION_MASK) {
143
144
case MotionEvent.ACTION_DOWN: {
145
// first pressed gesture has started
146
147
/*
148
* Only one touch event is stored in the MotionEvent. Extract
149
* the pointer identifier of this touch from the first index
150
* within the MotionEvent object.
151
*/
152
int id = event.getPointerId(0);
153
154
TouchHistory data = TouchHistory.obtain(event.getX(0), event.getY(0),
155
event.getPressure(0));
156
data.label = "id: " + 0;
157
158
/*
159
* Store the data under its pointer identifier. The pointer
160
* number stays consistent for the duration of a gesture,
161
* accounting for other pointers going up or down.
162
*/
163
mTouches.put(id, data);
164
165
mHasTouch = true;
166
167
break;
168
}
169
170
case MotionEvent.ACTION_POINTER_DOWN: {
171
/*
172
* A non-primary pointer has gone down, after an event for the
173
* primary pointer (ACTION_DOWN) has already been received.
174
*/
175
176
/*
177
* The MotionEvent object contains multiple pointers. Need to
178
* extract the index at which the data for this particular event
179
* is stored.
180
*/
181
int index = event.getActionIndex();
182
int id = event.getPointerId(index);
183
184
TouchHistory data = TouchHistory.obtain(event.getX(index), event.getY(index),
185
event.getPressure(index));
186
data.label = "id: " + id;
187
188
/*
189
* Store the data under its pointer identifier. The index of
190
* this pointer can change over multiple events, but this
191
* pointer is always identified by the same identifier for this
192
* active gesture.
193
*/
194
mTouches.put(id, data);
195
196
break;
197
}
198
199
case MotionEvent.ACTION_UP: {
200
/*
201
* Final pointer has gone up and has ended the last pressed
202
* gesture.
203
*/
204
205
/*
206
* Extract the pointer identifier for the only event stored in
207
* the MotionEvent object and remove it from the list of active
208
* touches.
209
*/
210
int id = event.getPointerId(0);
211
TouchHistory data = mTouches.get(id);
212
mTouches.remove(id);
213
data.recycle();
214
215
mHasTouch = false;
216
217
break;
218
}
219
220
case MotionEvent.ACTION_POINTER_UP: {
221
/*
222
* A non-primary pointer has gone up and other pointers are
223
* still active.
224
*/
225
226
/*
227
* The MotionEvent object contains multiple pointers. Need to
228
* extract the index at which the data for this particular event
229
* is stored.
230
*/
231
int index = event.getActionIndex();
232
int id = event.getPointerId(index);
233
234
TouchHistory data = mTouches.get(id);
235
mTouches.remove(id);
236
data.recycle();
237
238
break;
239
}
240
241
case MotionEvent.ACTION_MOVE: {
242
/*
243
* A change event happened during a pressed gesture. (Between
244
* ACTION_DOWN and ACTION_UP or ACTION_POINTER_DOWN and
245
* ACTION_POINTER_UP)
246
*/
247
248
/*
249
* Loop through all active pointers contained within this event.
250
* Data for each pointer is stored in a MotionEvent at an index
251
* (starting from 0 up to the number of active pointers). This
252
* loop goes through each of these active pointers, extracts its
253
* data (position and pressure) and updates its stored data. A
254
* pointer is identified by its pointer number which stays
255
* constant across touch events as long as it remains active.
256
* This identifier is used to keep track of a pointer across
257
* events.
258
*/
259
for (int index = 0; index < event.getPointerCount(); index++) {
260
// get pointer id for data stored at this index
261
int id = event.getPointerId(index);
262
263
// get the data stored externally about this pointer.
264
TouchHistory data = mTouches.get(id);
265
266
// add previous position to history and add new values
267
data.addHistory(data.x, data.y);
268
data.setTouch(event.getX(index), event.getY(index),
269
event.getPressure(index));
270
271
}
272
273
break;
274
}
275
}
276
277
// trigger redraw on UI thread
278
this.postInvalidate();
279
280
return true;
281
}
282
284
285
@Override
286
protected void onDraw(Canvas canvas) {
287
super.onDraw(canvas);
288
289
// Canvas background color depends on whether there is an active touch
290
if (mHasTouch) {
291
canvas.drawColor(BACKGROUND_ACTIVE);
292
} else {
293
// draw inactive border
294
canvas.drawRect(mBorderWidth, mBorderWidth, getWidth() - mBorderWidth, getHeight()
295
- mBorderWidth, mBorderPaint);
296
}
297
298
// loop through all active touches and draw them
299
for (int i = 0; i < mTouches.size(); i++) {
300
301
// get the pointer id and associated data for this index
302
int id = mTouches.keyAt(i);
303
TouchHistory data = mTouches.valueAt(i);
304
305
// draw the data and its history to the canvas
306
drawCircle(canvas, id, data);
307
}
308
}
309
310
/*
311
* Below are only helper methods and variables required for drawing.
312
*/
313
314
// radius of active touch circle in dp
315
private static final float CIRCLE_RADIUS_DP = 75f;
316
// radius of historical circle in dp
317
private static final float CIRCLE_HISTORICAL_RADIUS_DP = 7f;
318
319
// calculated radiuses in px
320
private float mCircleRadius;
321
private float mCircleHistoricalRadius;
322
323
private Paint mCirclePaint = new Paint();
324
private Paint mTextPaint = new Paint();
325
326
private static final int BACKGROUND_ACTIVE = Color.WHITE;
327
328
// inactive border
329
private static final float INACTIVE_BORDER_DP = 15f;
330
private static final int INACTIVE_BORDER_COLOR = 0xFFffd060;
331
private Paint mBorderPaint = new Paint();
332
private float mBorderWidth;
333
334
public final int[] COLORS = {
335
0xFF33B5E5, 0xFFAA66CC, 0xFF99CC00, 0xFFFFBB33, 0xFFFF4444,
336
0xFF0099CC, 0xFF9933CC, 0xFF669900, 0xFFFF8800, 0xFFCC0000
337
};
338
339
/**
340
* Sets up the required {@link android.graphics.Paint} objects for the screen density of this
341
* device.
342
*/
343
private void initialisePaint() {
344
345
// Calculate radiuses in px from dp based on screen density
346
float density = getResources().getDisplayMetrics().density;
347
mCircleRadius = CIRCLE_RADIUS_DP * density;
348
mCircleHistoricalRadius = CIRCLE_HISTORICAL_RADIUS_DP * density;
349
350
// Setup text paint for circle label
351
mTextPaint.setTextSize(27f);
352
mTextPaint.setColor(Color.BLACK);
353
354
// Setup paint for inactive border
355
mBorderWidth = INACTIVE_BORDER_DP * density;
356
mBorderPaint.setStrokeWidth(mBorderWidth);
357
mBorderPaint.setColor(INACTIVE_BORDER_COLOR);
358
mBorderPaint.setStyle(Paint.Style.STROKE);
359
360
}
361
362
/**
363
* Draws the data encapsulated by a {@link TouchDisplayView.TouchHistory} object to a canvas.
364
* A large circle indicates the current position held by the
365
* {@link TouchDisplayView.TouchHistory} object, while a smaller circle is drawn for each
366
* entry in its history. The size of the large circle is scaled depending on
367
* its pressure, clamped to a maximum of <code>1.0</code>.
368
*
369
* @param canvas
370
* @param id
371
* @param data
372
*/
373
protected void drawCircle(Canvas canvas, int id, TouchHistory data) {
374
// select the color based on the id
375
int color = COLORS[id % COLORS.length];
376
mCirclePaint.setColor(color);
377
378
/*
379
* Draw the circle, size scaled to its pressure. Pressure is clamped to
380
* 1.0 max to ensure proper drawing. (Reported pressure values can
381
* exceed 1.0, depending on the calibration of the touch screen).
382
*/
383
float pressure = Math.min(data.pressure, 1f);
384
float radius = pressure * mCircleRadius;
385
386
canvas.drawCircle(data.x, (data.y) - (radius / 2f), radius,
387
mCirclePaint);
388
389
// draw all historical points with a lower alpha value
390
mCirclePaint.setAlpha(125);
391
for (int j = 0; j < data.history.length && j < data.historyCount; j++) {
392
PointF p = data.history[j];
393
canvas.drawCircle(p.x, p.y, mCircleHistoricalRadius, mCirclePaint);
394
}
395
396
// draw its label next to the main circle
397
canvas.drawText(data.label, data.x + radius, data.y
398
- radius, mTextPaint);
399
}
400
401
}