blob: 939f1182f1faaef23a882b6cb878a377643cb8f1 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Paint.FontMetricsInt;
import android.util.Log;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import java.util.ArrayList;
public class PointerLocationView extends View {
private static final String TAG = "Pointer";
public static class PointerState {
// Trace of previous points.
private float[] mTraceX = new float[32];
private float[] mTraceY = new float[32];
private int mTraceCount;
// True if the pointer is down.
private boolean mCurDown;
// Most recent coordinates.
private MotionEvent.PointerCoords mCoords = new MotionEvent.PointerCoords();
// Most recent velocity.
private float mXVelocity;
private float mYVelocity;
public void clearTrace() {
mTraceCount = 0;
}
public void addTrace(float x, float y) {
int traceCapacity = mTraceX.length;
if (mTraceCount == traceCapacity) {
traceCapacity *= 2;
float[] newTraceX = new float[traceCapacity];
System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount);
mTraceX = newTraceX;
float[] newTraceY = new float[traceCapacity];
System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount);
mTraceY = newTraceY;
}
mTraceX[mTraceCount] = x;
mTraceY[mTraceCount] = y;
mTraceCount += 1;
}
}
private final ViewConfiguration mVC;
private final Paint mTextPaint;
private final Paint mTextBackgroundPaint;
private final Paint mTextLevelPaint;
private final Paint mPaint;
private final Paint mTargetPaint;
private final Paint mPathPaint;
private final FontMetricsInt mTextMetrics = new FontMetricsInt();
private int mHeaderBottom;
private boolean mCurDown;
private int mCurNumPointers;
private int mMaxNumPointers;
private final ArrayList<PointerState> mPointers = new ArrayList<PointerState>();
private final VelocityTracker mVelocity;
private final FasterStringBuilder mText = new FasterStringBuilder();
private boolean mPrintCoords = true;
public PointerLocationView(Context c) {
super(c);
setFocusable(true);
mVC = ViewConfiguration.get(c);
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(10
* getResources().getDisplayMetrics().density);
mTextPaint.setARGB(255, 0, 0, 0);
mTextBackgroundPaint = new Paint();
mTextBackgroundPaint.setAntiAlias(false);
mTextBackgroundPaint.setARGB(128, 255, 255, 255);
mTextLevelPaint = new Paint();
mTextLevelPaint.setAntiAlias(false);
mTextLevelPaint.setARGB(192, 255, 0, 0);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setARGB(255, 255, 255, 255);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
mTargetPaint = new Paint();
mTargetPaint.setAntiAlias(false);
mTargetPaint.setARGB(255, 0, 0, 192);
mPathPaint = new Paint();
mPathPaint.setAntiAlias(false);
mPathPaint.setARGB(255, 0, 96, 255);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(1);
PointerState ps = new PointerState();
mPointers.add(ps);
mVelocity = VelocityTracker.obtain();
logInputDeviceCapabilities();
}
private void logInputDeviceCapabilities() {
int[] deviceIds = InputDevice.getDeviceIds();
for (int i = 0; i < deviceIds.length; i++) {
InputDevice device = InputDevice.getDevice(deviceIds[i]);
if (device != null) {
Log.i(TAG, device.toString());
}
}
}
public void setPrintCoords(boolean state) {
mPrintCoords = state;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mTextPaint.getFontMetricsInt(mTextMetrics);
mHeaderBottom = -mTextMetrics.ascent+mTextMetrics.descent+2;
if (false) {
Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent
+ " descent=" + mTextMetrics.descent
+ " leading=" + mTextMetrics.leading
+ " top=" + mTextMetrics.top
+ " bottom=" + mTextMetrics.bottom);
}
}
// Draw an oval. When angle is 0 radians, orients the major axis vertically,
// angles less than or greater than 0 radians rotate the major axis left or right.
private RectF mReusableOvalRect = new RectF();
private void drawOval(Canvas canvas, float x, float y, float major, float minor,
float angle, Paint paint) {
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.rotate((float) (angle * 180 / Math.PI), x, y);
mReusableOvalRect.left = x - minor / 2;
mReusableOvalRect.right = x + minor / 2;
mReusableOvalRect.top = y - major / 2;
mReusableOvalRect.bottom = y + major / 2;
canvas.drawOval(mReusableOvalRect, paint);
canvas.restore();
}
@Override
protected void onDraw(Canvas canvas) {
synchronized (mPointers) {
final int w = getWidth();
final int itemW = w/7;
final int base = -mTextMetrics.ascent+1;
final int bottom = mHeaderBottom;
final int NP = mPointers.size();
// Labels
if (NP > 0) {
final PointerState ps = mPointers.get(0);
canvas.drawRect(0, 0, itemW-1, bottom,mTextBackgroundPaint);
canvas.drawText(mText.clear()
.append("P: ").append(mCurNumPointers)
.append(" / ").append(mMaxNumPointers)
.toString(), 1, base, mTextPaint);
final int N = ps.mTraceCount;
if ((mCurDown && ps.mCurDown) || N == 0) {
canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, mTextBackgroundPaint);
canvas.drawText(mText.clear()
.append("X: ").append(ps.mCoords.x, 1)
.toString(), 1 + itemW, base, mTextPaint);
canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, mTextBackgroundPaint);
canvas.drawText(mText.clear()
.append("Y: ").append(ps.mCoords.y, 1)
.toString(), 1 + itemW * 2, base, mTextPaint);
} else {
float dx = ps.mTraceX[N - 1] - ps.mTraceX[0];
float dy = ps.mTraceY[N - 1] - ps.mTraceY[0];
canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom,
Math.abs(dx) < mVC.getScaledTouchSlop()
? mTextBackgroundPaint : mTextLevelPaint);
canvas.drawText(mText.clear()
.append("dX: ").append(dx, 1)
.toString(), 1 + itemW, base, mTextPaint);
canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom,
Math.abs(dy) < mVC.getScaledTouchSlop()
? mTextBackgroundPaint : mTextLevelPaint);
canvas.drawText(mText.clear()
.append("dY: ").append(dy, 1)
.toString(), 1 + itemW * 2, base, mTextPaint);
}
canvas.drawRect(itemW * 3, 0, (itemW * 4) - 1, bottom, mTextBackgroundPaint);
canvas.drawText(mText.clear()
.append("Xv: ").append(ps.mXVelocity, 3)
.toString(), 1 + itemW * 3, base, mTextPaint);
canvas.drawRect(itemW * 4, 0, (itemW * 5) - 1, bottom, mTextBackgroundPaint);
canvas.drawText(mText.clear()
.append("Yv: ").append(ps.mYVelocity, 3)
.toString(), 1 + itemW * 4, base, mTextPaint);
canvas.drawRect(itemW * 5, 0, (itemW * 6) - 1, bottom, mTextBackgroundPaint);
canvas.drawRect(itemW * 5, 0, (itemW * 5) + (ps.mCoords.pressure * itemW) - 1,
bottom, mTextLevelPaint);
canvas.drawText(mText.clear()
.append("Prs: ").append(ps.mCoords.pressure, 2)
.toString(), 1 + itemW * 5, base, mTextPaint);
canvas.drawRect(itemW * 6, 0, w, bottom, mTextBackgroundPaint);
canvas.drawRect(itemW * 6, 0, (itemW * 6) + (ps.mCoords.size * itemW) - 1,
bottom, mTextLevelPaint);
canvas.drawText(mText.clear()
.append("Size: ").append(ps.mCoords.size, 2)
.toString(), 1 + itemW * 6, base, mTextPaint);
}
// Pointer trace.
for (int p = 0; p < NP; p++) {
final PointerState ps = mPointers.get(p);
// Draw path.
final int N = ps.mTraceCount;
float lastX = 0, lastY = 0;
boolean haveLast = false;
boolean drawn = false;
mPaint.setARGB(255, 128, 255, 255);
for (int i=0; i < N; i++) {
float x = ps.mTraceX[i];
float y = ps.mTraceY[i];
if (Float.isNaN(x)) {
haveLast = false;
continue;
}
if (haveLast) {
canvas.drawLine(lastX, lastY, x, y, mPathPaint);
canvas.drawPoint(lastX, lastY, mPaint);
drawn = true;
}
lastX = x;
lastY = y;
haveLast = true;
}
// Draw velocity vector.
if (drawn) {
mPaint.setARGB(255, 255, 64, 128);
float xVel = ps.mXVelocity * (1000 / 60);
float yVel = ps.mYVelocity * (1000 / 60);
canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
}
if (mCurDown && ps.mCurDown) {
// Draw crosshairs.
canvas.drawLine(0, ps.mCoords.y, getWidth(), ps.mCoords.y, mTargetPaint);
canvas.drawLine(ps.mCoords.x, 0, ps.mCoords.x, getHeight(), mTargetPaint);
// Draw current point.
int pressureLevel = (int)(ps.mCoords.pressure * 255);
mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);
canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);
// Draw current touch ellipse.
mPaint.setARGB(255, pressureLevel, 255 - pressureLevel, 128);
drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor,
ps.mCoords.touchMinor, ps.mCoords.orientation, mPaint);
// Draw current tool ellipse.
mPaint.setARGB(255, pressureLevel, 128, 255 - pressureLevel);
drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor,
ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint);
}
}
}
}
private void logPointerCoords(MotionEvent.PointerCoords coords, int id) {
Log.i(TAG, mText.clear()
.append("Pointer ").append(id + 1)
.append(": (").append(coords.x, 3).append(", ").append(coords.y, 3)
.append(") Pressure=").append(coords.pressure, 3)
.append(" Size=").append(coords.size, 3)
.append(" TouchMajor=").append(coords.touchMajor, 3)
.append(" TouchMinor=").append(coords.touchMinor, 3)
.append(" ToolMajor=").append(coords.toolMajor, 3)
.append(" ToolMinor=").append(coords.toolMinor, 3)
.append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1)
.append("deg").toString());
}
public void addTouchEvent(MotionEvent event) {
synchronized (mPointers) {
int action = event.getAction();
//Log.i(TAG, "Motion: action=0x" + Integer.toHexString(action)
// + " pointers=" + event.getPointerCount());
int NP = mPointers.size();
//mRect.set(0, 0, getWidth(), mHeaderBottom+1);
//invalidate(mRect);
//if (mCurDown) {
// mRect.set(mCurX-mCurWidth-3, mCurY-mCurWidth-3,
// mCurX+mCurWidth+3, mCurY+mCurWidth+3);
//} else {
// mRect.setEmpty();
//}
if (action == MotionEvent.ACTION_DOWN
|| (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for down
if (action == MotionEvent.ACTION_DOWN) {
for (int p=0; p<NP; p++) {
final PointerState ps = mPointers.get(p);
ps.clearTrace();
ps.mCurDown = false;
}
mCurDown = true;
mMaxNumPointers = 0;
mVelocity.clear();
}
final int id = event.getPointerId(index);
while (NP <= id) {
PointerState ps = new PointerState();
mPointers.add(ps);
NP++;
}
final PointerState ps = mPointers.get(id);
ps.mCurDown = true;
if (mPrintCoords) {
Log.i(TAG, mText.clear().append("Pointer ")
.append(id + 1).append(": DOWN").toString());
}
}
final int NI = event.getPointerCount();
mCurDown = action != MotionEvent.ACTION_UP
&& action != MotionEvent.ACTION_CANCEL;
mCurNumPointers = mCurDown ? NI : 0;
if (mMaxNumPointers < mCurNumPointers) {
mMaxNumPointers = mCurNumPointers;
}
mVelocity.addMovement(event);
mVelocity.computeCurrentVelocity(1);
for (int i=0; i<NI; i++) {
final int id = event.getPointerId(i);
final PointerState ps = mPointers.get(id);
final int N = event.getHistorySize();
for (int j=0; j<N; j++) {
event.getHistoricalPointerCoords(i, j, ps.mCoords);
if (mPrintCoords) {
logPointerCoords(ps.mCoords, id);
}
ps.addTrace(event.getHistoricalX(i, j), event.getHistoricalY(i, j));
}
event.getPointerCoords(i, ps.mCoords);
if (mPrintCoords) {
logPointerCoords(ps.mCoords, id);
}
ps.addTrace(ps.mCoords.x, ps.mCoords.y);
ps.mXVelocity = mVelocity.getXVelocity(id);
ps.mYVelocity = mVelocity.getYVelocity(id);
}
if (action == MotionEvent.ACTION_UP
|| (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) {
final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for UP
final int id = event.getPointerId(index);
final PointerState ps = mPointers.get(id);
ps.mCurDown = false;
if (mPrintCoords) {
Log.i(TAG, mText.clear().append("Pointer ")
.append(id + 1).append(": UP").toString());
}
if (action == MotionEvent.ACTION_UP) {
mCurDown = false;
} else {
ps.addTrace(Float.NaN, Float.NaN);
}
}
//if (mCurDown) {
// mRect.union(mCurX-mCurWidth-3, mCurY-mCurWidth-3,
// mCurX+mCurWidth+3, mCurY+mCurWidth+3);
//}
//invalidate(mRect);
postInvalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
addTouchEvent(event);
return true;
}
@Override
public boolean onTrackballEvent(MotionEvent event) {
Log.i(TAG, "Trackball: " + event);
return super.onTrackballEvent(event);
}
// HACK
// A quick and dirty string builder implementation optimized for GC.
// Using the basic StringBuilder implementation causes the application grind to a halt when
// more than a couple of pointers are down due to the number of temporary objects allocated
// while formatting strings for drawing or logging.
private static final class FasterStringBuilder {
private char[] mChars;
private int mLength;
public FasterStringBuilder() {
mChars = new char[64];
}
public FasterStringBuilder clear() {
mLength = 0;
return this;
}
public FasterStringBuilder append(String value) {
final int valueLength = value.length();
final int index = reserve(valueLength);
value.getChars(0, valueLength, mChars, index);
mLength += valueLength;
return this;
}
public FasterStringBuilder append(int value) {
return append(value, 0);
}
public FasterStringBuilder append(int value, int zeroPadWidth) {
final boolean negative = value < 0;
if (negative) {
value = - value;
if (value < 0) {
append("-2147483648");
return this;
}
}
int index = reserve(11);
final char[] chars = mChars;
if (value == 0) {
chars[index++] = '0';
mLength += 1;
return this;
}
if (negative) {
chars[index++] = '-';
}
int divisor = 1000000000;
int numberWidth = 10;
while (value < divisor) {
divisor /= 10;
numberWidth -= 1;
if (numberWidth < zeroPadWidth) {
chars[index++] = '0';
}
}
do {
int digit = value / divisor;
value -= digit * divisor;
divisor /= 10;
chars[index++] = (char) (digit + '0');
} while (divisor != 0);
mLength = index;
return this;
}
public FasterStringBuilder append(float value, int precision) {
int scale = 1;
for (int i = 0; i < precision; i++) {
scale *= 10;
}
value = (float) (Math.rint(value * scale) / scale);
append((int) value);
if (precision != 0) {
append(".");
value = Math.abs(value);
value -= Math.floor(value);
append((int) (value * scale), precision);
}
return this;
}
@Override
public String toString() {
return new String(mChars, 0, mLength);
}
private int reserve(int length) {
final int oldLength = mLength;
final int newLength = mLength + length;
final char[] oldChars = mChars;
final int oldCapacity = oldChars.length;
if (newLength > oldCapacity) {
final int newCapacity = oldCapacity * 2;
final char[] newChars = new char[newCapacity];
System.arraycopy(oldChars, 0, newChars, 0, oldLength);
mChars = newChars;
}
return oldLength;
}
}
}