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