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