PointerLocationView.java revision 33bbfd2232ea9eaae9a9d87a05a95a430f09bd83
1/* 2 * Copyright (C) 2010 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 17package com.android.internal.widget; 18 19import android.content.Context; 20import android.graphics.Canvas; 21import android.graphics.Paint; 22import android.graphics.RectF; 23import android.graphics.Paint.FontMetricsInt; 24import android.util.Log; 25import android.view.InputDevice; 26import android.view.MotionEvent; 27import android.view.VelocityTracker; 28import android.view.View; 29import android.view.ViewConfiguration; 30import android.view.MotionEvent.PointerCoords; 31 32import java.util.ArrayList; 33 34public class PointerLocationView extends View { 35 private static final String TAG = "Pointer"; 36 37 public static class PointerState { 38 // Trace of previous points. 39 private float[] mTraceX = new float[32]; 40 private float[] mTraceY = new float[32]; 41 private int mTraceCount; 42 43 // True if the pointer is down. 44 private boolean mCurDown; 45 46 // Most recent coordinates. 47 private PointerCoords mCoords = new PointerCoords(); 48 49 // Most recent velocity. 50 private float mXVelocity; 51 private float mYVelocity; 52 53 public void clearTrace() { 54 mTraceCount = 0; 55 } 56 57 public void addTrace(float x, float y) { 58 int traceCapacity = mTraceX.length; 59 if (mTraceCount == traceCapacity) { 60 traceCapacity *= 2; 61 float[] newTraceX = new float[traceCapacity]; 62 System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount); 63 mTraceX = newTraceX; 64 65 float[] newTraceY = new float[traceCapacity]; 66 System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount); 67 mTraceY = newTraceY; 68 } 69 70 mTraceX[mTraceCount] = x; 71 mTraceY[mTraceCount] = y; 72 mTraceCount += 1; 73 } 74 } 75 76 private final ViewConfiguration mVC; 77 private final Paint mTextPaint; 78 private final Paint mTextBackgroundPaint; 79 private final Paint mTextLevelPaint; 80 private final Paint mPaint; 81 private final Paint mTargetPaint; 82 private final Paint mPathPaint; 83 private final FontMetricsInt mTextMetrics = new FontMetricsInt(); 84 private int mHeaderBottom; 85 private boolean mCurDown; 86 private int mCurNumPointers; 87 private int mMaxNumPointers; 88 private int mActivePointerId; 89 private final ArrayList<PointerState> mPointers = new ArrayList<PointerState>(); 90 private final PointerCoords mHoverCoords = new PointerCoords(); 91 92 private final VelocityTracker mVelocity; 93 94 private final FasterStringBuilder mText = new FasterStringBuilder(); 95 96 private boolean mPrintCoords = true; 97 98 public PointerLocationView(Context c) { 99 super(c); 100 setFocusable(true); 101 mVC = ViewConfiguration.get(c); 102 mTextPaint = new Paint(); 103 mTextPaint.setAntiAlias(true); 104 mTextPaint.setTextSize(10 105 * getResources().getDisplayMetrics().density); 106 mTextPaint.setARGB(255, 0, 0, 0); 107 mTextBackgroundPaint = new Paint(); 108 mTextBackgroundPaint.setAntiAlias(false); 109 mTextBackgroundPaint.setARGB(128, 255, 255, 255); 110 mTextLevelPaint = new Paint(); 111 mTextLevelPaint.setAntiAlias(false); 112 mTextLevelPaint.setARGB(192, 255, 0, 0); 113 mPaint = new Paint(); 114 mPaint.setAntiAlias(true); 115 mPaint.setARGB(255, 255, 255, 255); 116 mPaint.setStyle(Paint.Style.STROKE); 117 mPaint.setStrokeWidth(2); 118 mTargetPaint = new Paint(); 119 mTargetPaint.setAntiAlias(false); 120 mTargetPaint.setARGB(255, 0, 0, 192); 121 mPathPaint = new Paint(); 122 mPathPaint.setAntiAlias(false); 123 mPathPaint.setARGB(255, 0, 96, 255); 124 mPaint.setStyle(Paint.Style.STROKE); 125 mPaint.setStrokeWidth(1); 126 127 PointerState ps = new PointerState(); 128 mPointers.add(ps); 129 mActivePointerId = 0; 130 131 mVelocity = VelocityTracker.obtain(); 132 133 logInputDeviceCapabilities(); 134 } 135 136 private void logInputDeviceCapabilities() { 137 int[] deviceIds = InputDevice.getDeviceIds(); 138 for (int i = 0; i < deviceIds.length; i++) { 139 InputDevice device = InputDevice.getDevice(deviceIds[i]); 140 if (device != null) { 141 Log.i(TAG, device.toString()); 142 } 143 } 144 } 145 146 public void setPrintCoords(boolean state) { 147 mPrintCoords = state; 148 } 149 150 @Override 151 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 152 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 153 mTextPaint.getFontMetricsInt(mTextMetrics); 154 mHeaderBottom = -mTextMetrics.ascent+mTextMetrics.descent+2; 155 if (false) { 156 Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent 157 + " descent=" + mTextMetrics.descent 158 + " leading=" + mTextMetrics.leading 159 + " top=" + mTextMetrics.top 160 + " bottom=" + mTextMetrics.bottom); 161 } 162 } 163 164 // Draw an oval. When angle is 0 radians, orients the major axis vertically, 165 // angles less than or greater than 0 radians rotate the major axis left or right. 166 private RectF mReusableOvalRect = new RectF(); 167 private void drawOval(Canvas canvas, float x, float y, float major, float minor, 168 float angle, Paint paint) { 169 canvas.save(Canvas.MATRIX_SAVE_FLAG); 170 canvas.rotate((float) (angle * 180 / Math.PI), x, y); 171 mReusableOvalRect.left = x - minor / 2; 172 mReusableOvalRect.right = x + minor / 2; 173 mReusableOvalRect.top = y - major / 2; 174 mReusableOvalRect.bottom = y + major / 2; 175 canvas.drawOval(mReusableOvalRect, paint); 176 canvas.restore(); 177 } 178 179 @Override 180 protected void onDraw(Canvas canvas) { 181 synchronized (mPointers) { 182 final int w = getWidth(); 183 final int itemW = w/7; 184 final int base = -mTextMetrics.ascent+1; 185 final int bottom = mHeaderBottom; 186 187 final int NP = mPointers.size(); 188 189 // Labels 190 if (mActivePointerId >= 0) { 191 final PointerState ps = mPointers.get(mActivePointerId); 192 193 canvas.drawRect(0, 0, itemW-1, bottom,mTextBackgroundPaint); 194 canvas.drawText(mText.clear() 195 .append("P: ").append(mCurNumPointers) 196 .append(" / ").append(mMaxNumPointers) 197 .toString(), 1, base, mTextPaint); 198 199 final int N = ps.mTraceCount; 200 if ((mCurDown && ps.mCurDown) || N == 0) { 201 canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, mTextBackgroundPaint); 202 canvas.drawText(mText.clear() 203 .append("X: ").append(ps.mCoords.x, 1) 204 .toString(), 1 + itemW, base, mTextPaint); 205 canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, mTextBackgroundPaint); 206 canvas.drawText(mText.clear() 207 .append("Y: ").append(ps.mCoords.y, 1) 208 .toString(), 1 + itemW * 2, base, mTextPaint); 209 } else { 210 float dx = ps.mTraceX[N - 1] - ps.mTraceX[0]; 211 float dy = ps.mTraceY[N - 1] - ps.mTraceY[0]; 212 canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, 213 Math.abs(dx) < mVC.getScaledTouchSlop() 214 ? mTextBackgroundPaint : mTextLevelPaint); 215 canvas.drawText(mText.clear() 216 .append("dX: ").append(dx, 1) 217 .toString(), 1 + itemW, base, mTextPaint); 218 canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, 219 Math.abs(dy) < mVC.getScaledTouchSlop() 220 ? mTextBackgroundPaint : mTextLevelPaint); 221 canvas.drawText(mText.clear() 222 .append("dY: ").append(dy, 1) 223 .toString(), 1 + itemW * 2, base, mTextPaint); 224 } 225 226 canvas.drawRect(itemW * 3, 0, (itemW * 4) - 1, bottom, mTextBackgroundPaint); 227 canvas.drawText(mText.clear() 228 .append("Xv: ").append(ps.mXVelocity, 3) 229 .toString(), 1 + itemW * 3, base, mTextPaint); 230 231 canvas.drawRect(itemW * 4, 0, (itemW * 5) - 1, bottom, mTextBackgroundPaint); 232 canvas.drawText(mText.clear() 233 .append("Yv: ").append(ps.mYVelocity, 3) 234 .toString(), 1 + itemW * 4, base, mTextPaint); 235 236 canvas.drawRect(itemW * 5, 0, (itemW * 6) - 1, bottom, mTextBackgroundPaint); 237 canvas.drawRect(itemW * 5, 0, (itemW * 5) + (ps.mCoords.pressure * itemW) - 1, 238 bottom, mTextLevelPaint); 239 canvas.drawText(mText.clear() 240 .append("Prs: ").append(ps.mCoords.pressure, 2) 241 .toString(), 1 + itemW * 5, base, mTextPaint); 242 243 canvas.drawRect(itemW * 6, 0, w, bottom, mTextBackgroundPaint); 244 canvas.drawRect(itemW * 6, 0, (itemW * 6) + (ps.mCoords.size * itemW) - 1, 245 bottom, mTextLevelPaint); 246 canvas.drawText(mText.clear() 247 .append("Size: ").append(ps.mCoords.size, 2) 248 .toString(), 1 + itemW * 6, base, mTextPaint); 249 } 250 251 // Pointer trace. 252 for (int p = 0; p < NP; p++) { 253 final PointerState ps = mPointers.get(p); 254 255 // Draw path. 256 final int N = ps.mTraceCount; 257 float lastX = 0, lastY = 0; 258 boolean haveLast = false; 259 boolean drawn = false; 260 mPaint.setARGB(255, 128, 255, 255); 261 for (int i=0; i < N; i++) { 262 float x = ps.mTraceX[i]; 263 float y = ps.mTraceY[i]; 264 if (Float.isNaN(x)) { 265 haveLast = false; 266 continue; 267 } 268 if (haveLast) { 269 canvas.drawLine(lastX, lastY, x, y, mPathPaint); 270 canvas.drawPoint(lastX, lastY, mPaint); 271 drawn = true; 272 } 273 lastX = x; 274 lastY = y; 275 haveLast = true; 276 } 277 278 // Draw velocity vector. 279 if (drawn) { 280 mPaint.setARGB(255, 255, 64, 128); 281 float xVel = ps.mXVelocity * (1000 / 60); 282 float yVel = ps.mYVelocity * (1000 / 60); 283 canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint); 284 } 285 286 if (mCurDown && ps.mCurDown) { 287 // Draw crosshairs. 288 canvas.drawLine(0, ps.mCoords.y, getWidth(), ps.mCoords.y, mTargetPaint); 289 canvas.drawLine(ps.mCoords.x, 0, ps.mCoords.x, getHeight(), mTargetPaint); 290 291 // Draw current point. 292 int pressureLevel = (int)(ps.mCoords.pressure * 255); 293 mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel); 294 canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint); 295 296 // Draw current touch ellipse. 297 mPaint.setARGB(255, pressureLevel, 255 - pressureLevel, 128); 298 drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor, 299 ps.mCoords.touchMinor, ps.mCoords.orientation, mPaint); 300 301 // Draw current tool ellipse. 302 mPaint.setARGB(255, pressureLevel, 128, 255 - pressureLevel); 303 drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor, 304 ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint); 305 306 // Draw the orientation arrow. 307 mPaint.setARGB(255, pressureLevel, 255, 0); 308 float orientationVectorX = (float) (Math.sin(-ps.mCoords.orientation) 309 * ps.mCoords.toolMajor * 0.7); 310 float orientationVectorY = (float) (Math.cos(-ps.mCoords.orientation) 311 * ps.mCoords.toolMajor * 0.7); 312 canvas.drawLine( 313 ps.mCoords.x - orientationVectorX, ps.mCoords.y - orientationVectorY, 314 ps.mCoords.x + orientationVectorX, ps.mCoords.y + orientationVectorY, 315 mPaint); 316 } 317 } 318 } 319 } 320 321 private void logPointerCoords(int action, int index, MotionEvent.PointerCoords coords, int id) { 322 final String prefix; 323 switch (action & MotionEvent.ACTION_MASK) { 324 case MotionEvent.ACTION_DOWN: 325 prefix = "DOWN"; 326 break; 327 case MotionEvent.ACTION_UP: 328 prefix = "UP"; 329 break; 330 case MotionEvent.ACTION_MOVE: 331 prefix = "MOVE"; 332 break; 333 case MotionEvent.ACTION_CANCEL: 334 prefix = "CANCEL"; 335 break; 336 case MotionEvent.ACTION_OUTSIDE: 337 prefix = "OUTSIDE"; 338 break; 339 case MotionEvent.ACTION_POINTER_DOWN: 340 if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK) 341 >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) { 342 prefix = "DOWN"; 343 } else { 344 prefix = "MOVE"; 345 } 346 break; 347 case MotionEvent.ACTION_POINTER_UP: 348 if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK) 349 >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) { 350 prefix = "UP"; 351 } else { 352 prefix = "MOVE"; 353 } 354 break; 355 case MotionEvent.ACTION_HOVER_MOVE: 356 prefix = "HOVER MOVE"; 357 break; 358 case MotionEvent.ACTION_SCROLL: 359 prefix = "SCROLL"; 360 break; 361 default: 362 prefix = Integer.toString(action); 363 break; 364 } 365 366 Log.i(TAG, mText.clear() 367 .append("Pointer ").append(id + 1) 368 .append(": ") 369 .append(prefix) 370 .append(" (").append(coords.x, 3).append(", ").append(coords.y, 3) 371 .append(") Pressure=").append(coords.pressure, 3) 372 .append(" Size=").append(coords.size, 3) 373 .append(" TouchMajor=").append(coords.touchMajor, 3) 374 .append(" TouchMinor=").append(coords.touchMinor, 3) 375 .append(" ToolMajor=").append(coords.toolMajor, 3) 376 .append(" ToolMinor=").append(coords.toolMinor, 3) 377 .append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1) 378 .append("deg") 379 .append(" VScroll=").append(coords.getAxisValue(MotionEvent.AXIS_VSCROLL), 1) 380 .append(" HScroll=").append(coords.getAxisValue(MotionEvent.AXIS_HSCROLL), 1) 381 .toString()); 382 } 383 384 public void addPointerEvent(MotionEvent event) { 385 synchronized (mPointers) { 386 int action = event.getAction(); 387 388 //Log.i(TAG, "Motion: action=0x" + Integer.toHexString(action) 389 // + " pointers=" + event.getPointerCount()); 390 391 int NP = mPointers.size(); 392 393 //mRect.set(0, 0, getWidth(), mHeaderBottom+1); 394 //invalidate(mRect); 395 //if (mCurDown) { 396 // mRect.set(mCurX-mCurWidth-3, mCurY-mCurWidth-3, 397 // mCurX+mCurWidth+3, mCurY+mCurWidth+3); 398 //} else { 399 // mRect.setEmpty(); 400 //} 401 if (action == MotionEvent.ACTION_DOWN 402 || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { 403 final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) 404 >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for down 405 if (action == MotionEvent.ACTION_DOWN) { 406 for (int p=0; p<NP; p++) { 407 final PointerState ps = mPointers.get(p); 408 ps.clearTrace(); 409 ps.mCurDown = false; 410 } 411 mCurDown = true; 412 mCurNumPointers = 0; 413 mMaxNumPointers = 0; 414 mVelocity.clear(); 415 } 416 417 mCurNumPointers += 1; 418 if (mMaxNumPointers < mCurNumPointers) { 419 mMaxNumPointers = mCurNumPointers; 420 } 421 422 final int id = event.getPointerId(index); 423 while (NP <= id) { 424 PointerState ps = new PointerState(); 425 mPointers.add(ps); 426 NP++; 427 } 428 429 if (mActivePointerId < 0 || 430 !mPointers.get(mActivePointerId).mCurDown) { 431 mActivePointerId = id; 432 } 433 434 final PointerState ps = mPointers.get(id); 435 ps.mCurDown = true; 436 } 437 438 final int NI = event.getPointerCount(); 439 440 mVelocity.addMovement(event); 441 mVelocity.computeCurrentVelocity(1); 442 443 final int N = event.getHistorySize(); 444 for (int historyPos = 0; historyPos < N; historyPos++) { 445 for (int i = 0; i < NI; i++) { 446 final int id = event.getPointerId(i); 447 final PointerState ps = mCurDown ? mPointers.get(id) : null; 448 final PointerCoords coords = ps != null ? ps.mCoords : mHoverCoords; 449 event.getHistoricalPointerCoords(i, historyPos, coords); 450 if (mPrintCoords) { 451 logPointerCoords(action, i, coords, id); 452 } 453 if (ps != null) { 454 ps.addTrace(coords.x, coords.y); 455 } 456 } 457 } 458 for (int i = 0; i < NI; i++) { 459 final int id = event.getPointerId(i); 460 final PointerState ps = mCurDown ? mPointers.get(id) : null; 461 final PointerCoords coords = ps != null ? ps.mCoords : mHoverCoords; 462 event.getPointerCoords(i, coords); 463 if (mPrintCoords) { 464 logPointerCoords(action, i, coords, id); 465 } 466 if (ps != null) { 467 ps.addTrace(coords.x, coords.y); 468 ps.mXVelocity = mVelocity.getXVelocity(id); 469 ps.mYVelocity = mVelocity.getYVelocity(id); 470 } 471 } 472 473 if (action == MotionEvent.ACTION_UP 474 || action == MotionEvent.ACTION_CANCEL 475 || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) { 476 final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) 477 >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for UP 478 479 final int id = event.getPointerId(index); 480 final PointerState ps = mPointers.get(id); 481 ps.mCurDown = false; 482 483 if (action == MotionEvent.ACTION_UP 484 || action == MotionEvent.ACTION_CANCEL) { 485 mCurDown = false; 486 mCurNumPointers = 0; 487 } else { 488 mCurNumPointers -= 1; 489 if (mActivePointerId == id) { 490 mActivePointerId = event.getPointerId(index == 0 ? 1 : 0); 491 } 492 ps.addTrace(Float.NaN, Float.NaN); 493 } 494 } 495 496 //if (mCurDown) { 497 // mRect.union(mCurX-mCurWidth-3, mCurY-mCurWidth-3, 498 // mCurX+mCurWidth+3, mCurY+mCurWidth+3); 499 //} 500 //invalidate(mRect); 501 postInvalidate(); 502 } 503 } 504 505 @Override 506 public boolean onTouchEvent(MotionEvent event) { 507 addPointerEvent(event); 508 return true; 509 } 510 511 @Override 512 public boolean onGenericMotionEvent(MotionEvent event) { 513 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { 514 addPointerEvent(event); 515 return true; 516 } 517 return super.onGenericMotionEvent(event); 518 } 519 520 @Override 521 public boolean onTrackballEvent(MotionEvent event) { 522 Log.i(TAG, "Trackball: " + event); 523 return super.onTrackballEvent(event); 524 } 525 526 // HACK 527 // A quick and dirty string builder implementation optimized for GC. 528 // Using String.format causes the application grind to a halt when 529 // more than a couple of pointers are down due to the number of 530 // temporary objects allocated while formatting strings for drawing or logging. 531 private static final class FasterStringBuilder { 532 private char[] mChars; 533 private int mLength; 534 535 public FasterStringBuilder() { 536 mChars = new char[64]; 537 } 538 539 public FasterStringBuilder clear() { 540 mLength = 0; 541 return this; 542 } 543 544 public FasterStringBuilder append(String value) { 545 final int valueLength = value.length(); 546 final int index = reserve(valueLength); 547 value.getChars(0, valueLength, mChars, index); 548 mLength += valueLength; 549 return this; 550 } 551 552 public FasterStringBuilder append(int value) { 553 return append(value, 0); 554 } 555 556 public FasterStringBuilder append(int value, int zeroPadWidth) { 557 final boolean negative = value < 0; 558 if (negative) { 559 value = - value; 560 if (value < 0) { 561 append("-2147483648"); 562 return this; 563 } 564 } 565 566 int index = reserve(11); 567 final char[] chars = mChars; 568 569 if (value == 0) { 570 chars[index++] = '0'; 571 mLength += 1; 572 return this; 573 } 574 575 if (negative) { 576 chars[index++] = '-'; 577 } 578 579 int divisor = 1000000000; 580 int numberWidth = 10; 581 while (value < divisor) { 582 divisor /= 10; 583 numberWidth -= 1; 584 if (numberWidth < zeroPadWidth) { 585 chars[index++] = '0'; 586 } 587 } 588 589 do { 590 int digit = value / divisor; 591 value -= digit * divisor; 592 divisor /= 10; 593 chars[index++] = (char) (digit + '0'); 594 } while (divisor != 0); 595 596 mLength = index; 597 return this; 598 } 599 600 public FasterStringBuilder append(float value, int precision) { 601 int scale = 1; 602 for (int i = 0; i < precision; i++) { 603 scale *= 10; 604 } 605 value = (float) (Math.rint(value * scale) / scale); 606 607 append((int) value); 608 609 if (precision != 0) { 610 append("."); 611 value = Math.abs(value); 612 value -= Math.floor(value); 613 append((int) (value * scale), precision); 614 } 615 616 return this; 617 } 618 619 @Override 620 public String toString() { 621 return new String(mChars, 0, mLength); 622 } 623 624 private int reserve(int length) { 625 final int oldLength = mLength; 626 final int newLength = mLength + length; 627 final char[] oldChars = mChars; 628 final int oldCapacity = oldChars.length; 629 if (newLength > oldCapacity) { 630 final int newCapacity = oldCapacity * 2; 631 final char[] newChars = new char[newCapacity]; 632 System.arraycopy(oldChars, 0, newChars, 0, oldLength); 633 mChars = newChars; 634 } 635 return oldLength; 636 } 637 } 638} 639