BasicMultitouch / src / com.example.android.basicmultitouch /

TouchDisplayView.java

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
}