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