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 }