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 
obtain(float x, float y, float pressure)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 
TouchHistory()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 
setTouch(float x, float y, float pressure)90         public void setTouch(float x, float y, float pressure) {
91             this.x = x;
92             this.y = y;
93             this.pressure = pressure;
94         }
95 
recycle()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          */
addHistory(float x, float y)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 
TouchDisplayView(Context context, AttributeSet attrs)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 
131     // BEGIN_INCLUDE(onTouchEvent)
132     @Override
onTouchEvent(MotionEvent event)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 
283     // END_INCLUDE(onTouchEvent)
284 
285     @Override
onDraw(Canvas canvas)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      */
initialisePaint()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      */
drawCircle(Canvas canvas, int id, TouchHistory data)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 }
402