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