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