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