Multi-touch API is bugged (at least on Motorola Droid). Here is a method to reproduce the error for analysis:
1) Create a new android project in Eclipse with the following fields: Project name: PointerLocation Build target: Android 2.0.1 Application Name: PointerLocation Package Name: com.example.pointerlocation Create Activity: PointerLocation 2) Copy the following code and paste this into the PointerLocation.java file that is automatically created /* * Copyright (C) 2007 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.example.pointerlocation; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.FontMetricsInt; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.WindowManager; import android.view.VelocityTracker; import android.view.View; import java.util.ArrayList; /** * Demonstrates wrapping a layout in a ScrollView. * */ public class PointerLocation extends Activity { @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(new MyView(this)); // Make the screen full bright for this activity. WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.screenBrightness = 1.0f; getWindow().setAttributes(lp); } public static class PointerState { private final ArrayList<Float> mXs = new ArrayList<Float>(); private final ArrayList<Float> mYs = new ArrayList<Float>(); private boolean mCurDown; private int mCurX; private int mCurY; private float mCurPressure; private float mCurSize; private int mCurWidth; private VelocityTracker mVelocity; } public class MyView extends View { 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>(); public MyView(Context c) { super(c); 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(); ps.mVelocity = VelocityTracker.obtain(); mPointers.add(ps); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mTextPaint.getFontMetricsInt(mTextMetrics); mHeaderBottom = -mTextMetrics.ascent+mTextMetrics.descent +2; Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent + " descent=" + mTextMetrics.descent + " leading=" + mTextMetrics.leading + " top=" + mTextMetrics.top + " bottom=" + mTextMetrics.bottom); } @Override protected void onDraw(Canvas canvas) { final int w = getWidth(); final int itemW = w/7; final int base = -mTextMetrics.ascent+1; final int bottom = mHeaderBottom; final int NP = mPointers.size(); if (NP > 0) { final PointerState ps = mPointers.get(0); canvas.drawRect(0, 0, itemW-1, bottom,mTextBackgroundPaint); canvas.drawText("P: " + mCurNumPointers + " / " + mMaxNumPointers, 1, base, mTextPaint); final int N = ps.mXs.size(); if ((mCurDown && ps.mCurDown) || N == 0) { canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, mTextBackgroundPaint); canvas.drawText("X: " + ps.mCurX, 1 + itemW, base, mTextPaint); canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, mTextBackgroundPaint); canvas.drawText("Y: " + ps.mCurY, 1 + itemW * 2, base, mTextPaint); } else { float dx = ps.mXs.get(N-1) - ps.mXs.get(0); float dy = ps.mYs.get(N-1) - ps.mYs.get(0); canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, Math.abs(dx) < mVC.getScaledTouchSlop() ? mTextBackgroundPaint : mTextLevelPaint); canvas.drawText("dX: " + String.format("%.1f", dx), 1 + itemW, base, mTextPaint); canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, Math.abs(dy) < mVC.getScaledTouchSlop() ? mTextBackgroundPaint : mTextLevelPaint); canvas.drawText("dY: " + String.format("%.1f", dy), 1 + itemW * 2, base, mTextPaint); } canvas.drawRect(itemW * 3, 0, (itemW * 4) - 1, bottom, mTextBackgroundPaint); int velocity = ps.mVelocity == null ? 0 : (int) (ps.mVelocity.getXVelocity() * 1000); canvas.drawText("Xv: " + velocity, 1 + itemW * 3, base, mTextPaint); canvas.drawRect(itemW * 4, 0, (itemW * 5) - 1, bottom, mTextBackgroundPaint); velocity = ps.mVelocity == null ? 0 : (int) (ps.mVelocity.getYVelocity() * 1000); canvas.drawText("Yv: " + velocity, 1 + itemW * 4, base, mTextPaint); canvas.drawRect(itemW * 5, 0, (itemW * 6) - 1, bottom, mTextBackgroundPaint); canvas.drawRect(itemW * 5, 0, (itemW * 5) + (ps.mCurPressure * itemW) - 1, bottom, mTextLevelPaint); canvas.drawText("Prs: " + String.format("%.2f", ps.mCurPressure), 1 + itemW * 5, base, mTextPaint); canvas.drawRect(itemW * 6, 0, w, bottom, mTextBackgroundPaint); canvas.drawRect(itemW * 6, 0, (itemW * 6) + (ps.mCurSize * itemW) - 1, bottom, mTextLevelPaint); canvas.drawText("Size: " + String.format("%.2f", ps.mCurSize), 1 + itemW * 6, base, mTextPaint); } for (int p=0; p<NP; p++) { final PointerState ps = mPointers.get(p); if (mCurDown && ps.mCurDown) { canvas.drawLine(0, (int)ps.mCurY, getWidth(), (int) ps.mCurY, mTargetPaint); canvas.drawLine((int)ps.mCurX, 0, (int)ps.mCurX, getHeight(), mTargetPaint); int pressureLevel = (int)(ps.mCurPressure*255); mPaint.setARGB(255, pressureLevel, 128, 255- pressureLevel); canvas.drawPoint(ps.mCurX, ps.mCurY, mPaint); canvas.drawCircle(ps.mCurX, ps.mCurY, ps.mCurWidth, mPaint); } } for (int p=0; p<NP; p++) { final PointerState ps = mPointers.get(p); final int N = ps.mXs.size(); 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.mXs.get(i); float y = ps.mYs.get(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; } if (drawn) { if (ps.mVelocity != null) { mPaint.setARGB(255, 255, 64, 128); float xVel = ps.mVelocity.getXVelocity() * (1000/60); float yVel = ps.mVelocity.getYVelocity() * (1000/60); canvas.drawLine(lastX, lastY, lastX+xVel, lastY +yVel, mPaint); } else { canvas.drawPoint(lastX, lastY, mPaint); } } } } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); //Log.i("Pointer", "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) { for (int p=0; p<NP; p++) { final PointerState ps = mPointers.get(p); ps.mXs.clear(); ps.mYs.clear(); ps.mVelocity = VelocityTracker.obtain(); ps.mCurDown = false; } mPointers.get(0).mCurDown = true; mMaxNumPointers = 0; Log.i("Pointer", "Pointer 1: DOWN"); } if ((action&MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { final int id = (action&MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT; while (NP <= id) { PointerState ps = new PointerState(); ps.mVelocity = VelocityTracker.obtain(); mPointers.add(ps); NP++; } final PointerState ps = mPointers.get(id); ps.mVelocity = VelocityTracker.obtain(); ps.mCurDown = true; Log.i("Pointer", "Pointer " + (id+1) + ": DOWN"); } final int NI = event.getPointerCount(); mCurDown = action != MotionEvent.ACTION_UP && action != MotionEvent.ACTION_CANCEL; mCurNumPointers = mCurDown ? NI : 0; if (mMaxNumPointers < mCurNumPointers) { mMaxNumPointers = mCurNumPointers; } for (int i=0; i<NI; i++) { final PointerState ps = mPointers.get (event.getPointerId(i)); ps.mVelocity.addMovement(event); ps.mVelocity.computeCurrentVelocity(1); final int N = event.getHistorySize(); for (int j=0; j<N; j++) { Log.i("Pointer", "Pointer " + (i+1) + ": (" + event.getHistoricalX(i, j) + ", " + event.getHistoricalY(i, j) + ")" + " Prs=" + event.getHistoricalPressure(i, j) + " Size=" + event.getHistoricalSize(i, j)); ps.mXs.add(event.getHistoricalX(i, j)); ps.mYs.add(event.getHistoricalY(i, j)); } Log.i("Pointer", "Pointer " + (i+1) + ": (" + event.getX(i) + ", " + event.getY(i) + ")" + " Prs=" + event.getPressure(i) + " Size=" + event.getSize(i)); ps.mXs.add(event.getX(i)); ps.mYs.add(event.getY(i)); ps.mCurX = (int)event.getX(i); ps.mCurY = (int)event.getY(i); //Log.i("Pointer", "Pointer #" + p + ": (" + ps.mCurX // + "," + ps.mCurY + ")"); ps.mCurPressure = event.getPressure(i); ps.mCurSize = event.getSize(i); ps.mCurWidth = (int)(ps.mCurSize*(getWidth()/3)); } if ((action&MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) { final int id = (action&MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT; final PointerState ps = mPointers.get(id); ps.mXs.add(Float.NaN); ps.mYs.add(Float.NaN); ps.mCurDown = false; Log.i("Pointer", "Pointer " + (id+1) + ": UP"); } if (action == MotionEvent.ACTION_UP) { for (int i=0; i<NI; i++) { final PointerState ps = mPointers.get (event.getPointerId(i)); if (ps.mCurDown) { ps.mCurDown = false; Log.i("Pointer", "Pointer " + (i+1) + ": UP"); } } } //if (mCurDown) { // mRect.union(mCurX-mCurWidth-3, mCurY-mCurWidth-3, // mCurX+mCurWidth+3, mCurY+mCurWidth+3); //} //invalidate(mRect); invalidate(); return true; } } } 3) Run this on a device that is capable of multiple touch inputs (unless you have a dual mouse driver, the emulator will not work, you HAVE to have a device. Mine is Motorola Droid) 4) To re-create the bug, perform the following steps: 1) Touch screen with finger 1 and start doodling 2) Without removing finger 1, touch screen with finger 2 and start doodling 3) Remove finger 1 from the screen (without removing finger 2) 4) Replace finger 1 on the screen and start doodling again (never remove finger 2) 5) Voila, you will see the bug. The drawn lines for finger 1 will suddeny connect to finger 2. This is because there is a bug when the first finger is placed back down again, the event only has points for the wrong finger! Even though finger 1 went back down at a new location, the code thinks for some reason that finger 2 is the one that went back down (but it never went up). Someone please try this to see if you can duplicate this bug so we can get Google to address it. I have been trying for weeks for resolution and have not had any luck. If anyone knows any fixes or work-arounds, please post. Also, if you have a non-motorola phone, please try this and post results. Thanks in advance! -Colin
-- You received this message because you are subscribed to the Google Groups "Android Developers" group. To post to this group, send email to [email protected] To unsubscribe from this group, send email to [email protected] For more options, visit this group at http://groups.google.com/group/android-developers?hl=en

