So, to put it simply, a handler I was using for multitouch would crash and throw an "IllegalArgumentException: pointerIndex out of range".
I tried just catching the error, but then realized that the (x, y) would default to (min.x, max.y), which was (0.0, 480.0) in my case.
After quite a bit of messing around, I found the error only cropped up while spamming touches, but could be specifically and very regularly reproduced simply by alternately tapping in two different places quickly enough(but still slow enough to happen during a regular multitouch game).
Here's the onTouch method:
public boolean onTouch(View v, MotionEvent event) {
synchronized (this) {
int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
int pointerId = event.getPointerId(pointerIndex);
TouchEvent touchEvent;
// pointerId fix
if (pointerId >= event.getPointerCount() && pointerId > 0)
pointerId--;
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
touchEvent = touchEventPool.newObject();
touchEvent.type = TouchEvent.TOUCH_DOWN;
touchEvent.pointer = pointerId;
try {
touchEvent.x = touchX[pointerId] = (int) (event
.getX(pointerId) * scaleX);
touchEvent.y = touchY[pointerId] = (int) (event
.getY(pointerId) * scaleY);
} catch (IllegalArgumentException e) {
Log.d("multiTouchHandler", e.toString() + ":: PID = "
+ pointerId);
}
isTouched[pointerId] = true;
touchEventsBuffer.add(touchEvent);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
touchEvent = touchEventPool.newObject();
touchEvent.type = TouchEvent.TOUCH_UP;
touchEvent.pointer = pointerId;
try {
touchEvent.x = touchX[pointerId] = (int) (event
.getX(pointerIndex) * scaleX);
touchEvent.y = touchY[pointerId] = (int) (event
.getY(pointerIndex) * scaleY);
} catch (IllegalArgumentException e) {
Log.d("multiTouchHandler", e.toString() + ":: PID = "
+ pointerId);
}
isTouched[pointerId] = false;
touchEventsBuffer.add(touchEvent);
return false;
case MotionEvent.ACTION_MOVE:
int pointerCount = event.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
touchEvent = touchEventPool.newObject();
touchEvent.type = TouchEvent.TOUCH_DRAGGED;
touchEvent.pointer = pointerId;
try {
touchEvent.x = touchX[pointerId] = (int) (event
.getX(pointerIndex) * scaleX);
touchEvent.y = touchY[pointerId] = (int) (event
.getY(pointerIndex) * scaleY);
} catch (IllegalArgumentException e) {
Log.d("multiTouchHandler", e.toString() + ":: PID = "
+ pointerId);
}
isTouched[pointerId] = false;
touchEventsBuffer.add(touchEvent);
}
break;
case MotionEvent.ACTION_CANCEL:
}
return true;
}
}
What it seemed was happening from the Log.d output was that occasionally the pointerId would get assigned to the next available pointer(ex. 1), but the actually pointer data would get stored in the previous pointer location(0).
By checking if the pointerId was out of range of the current pointerCount, and if so going back one, I was able to at the very least fix this issue for two fingers with two lines of code.
// pointerId fix
if (pointerId >= event.getPointerCount() && pointerId > 0)
pointerId--;
I'm not sure if this really fixes the issue, but it seems to work great for two-finger multitouch controls like those used for touch screen fps games.
My own tests showed that it accurately was able to determine touch from both points without exceptions, and store the correct (x, y) values
Is there any better solution, or is the multitouch API use of pointer IDs just really that bad?
If anyone wants to know, this was on a 4.0.3 device.
It seems strange to me that you access event.getX() and event.getY() with
pointerId as the argument (at ACTION_POINTER_DOWN).
I think it should be pointerIndex instead, as you do in all other cases further down.
Related
I need to move, rotate and resize edittext based on user's touch gestures.
Here is what I am doing.
1. Listen for ACTION_DOWN event to detect single finger touch and then use that for movement.
Listen for ACTION_POINTER_DOWN, and get the index count to detect two finger touch and then use ACTION_MOVE for rotation.
I need to make resize work such that only the width gets decreased on resize by touch.
Problem I am facing
Movement is working perfectly fine. But when I apply rotation using the ACTION_POINTER_DOWN, rotation goes all sorts of haywire. Any suggestions on how to make the view movement and rotation work together?
Here is the code for touchListener I have applied
tvMain.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
moving = true;
break;
case MotionEvent.ACTION_POINTER_DOWN:
Log.d("ActionPointerIndex", "" + event.getActionIndex());
int index = event.getActionIndex();
moving = false;
//FOR ROTATION DETAILS
xc = tvMain.getWidth() / 2;
yc = tvMain.getHeight() / 2;
x_ = event.getX();
y_ = event.getY();
Log.d("currentAnglePD", ": " + x_ + "," + y_);
if (index == 1) {
rotate = true;
mCurrAngle = Math.toDegrees(Math.atan2(x_ - xc, yc - y_));
moving = false;
}
break;
case MotionEvent.ACTION_MOVE:
if (moving) {
leftBound = rootView.getX();
topBound = rootView.getY();
rightBound = rootView.getX() + 1280;
bottomBound = rootView.getY() + 618;
x = event.getRawX() - tvMain.getWidth() / 2;
y = event.getRawY() - tvMain.getHeight();
dx = Float.valueOf(rootView.getWidth() - tvMain.getWidth());
dy = Float.valueOf(rootView.getHeight() - tvMain.getHeight());
bottomBound_ = tvMain.getY() + tvMain.getHeight();
rightBound_ = tvMain.getX() + tvMain.getWidth();
if (x >= leftBound && y >= topBound && x <= dx && y <= dy) {
tvMain.setX(x);
tvMain.setY(y);
}
}
if (rotate) {
Log.d("currentAngleMove", ": " + x_ + "," + y_);
mPrevAngle = mCurrAngle;
mCurrAngle = Math.toDegrees(Math.atan2(x_ - event.getX(), yc - event.getY()));
animate(mPrevAngle, mCurrAngle, 0);
System.out.println(mCurrAngle);
}
break;
case MotionEvent.ACTION_POINTER_UP:
rotate = false;
break;
case MotionEvent.ACTION_UP:
rotate = false;
moving = false;
//tvMain.clearAnimation();
break;
}
return true;
}
});
Movement is working fine, but once I move the view, rotation goes haywire, and it does not rotate along its center. Once I stop rotating, the view stops listening for any more touch gestures.
In my multitouch program I'm encountering a runtime error where the Multitouch logic for a pointer sliding on the screen (MotionEvent.ACTION_MOVE) is only entered by the initial pointer. This means the code within my switch statement for case ACTION_MOVE is only accessible by the first finger to touch the screen, aka pointer index = 0. The following is the relevant parts of my code:
public boolean onTouchEvent(MotionEvent event) {
// get pointer index from the event object
int pointerIndex = event.getActionIndex();
// get pointer ID
int pointerId = event.getPointerId(pointerIndex);
// get masked (not specific to a pointer) action
int maskedAction = event.getActionMasked();
switch (maskedAction) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
// We have a new pointer. Lets add it to the list of pointers
PointF f = new PointF();
f.x = event.getX(pointerIndex);
f.y = event.getY(pointerIndex);
mActivePointers.put(pointerId, f);
Log.d("Multitouch", "x coord = " + Float.toString(f.x));
Log.d("Multitouch", "y coord = " + Float.toString(f.y));
break;
}
case MotionEvent.ACTION_MOVE: { // a pointer was moved
Log.e("Multitouch", "Pointer"+ Integer.toString(pointerId) +" is moving");
for (int size = event.getPointerCount(), i = 0; i < size; i++) {
PointF f = mActivePointers.get(event.getPointerId(i));
if (f != null) {
f.x = event.getX(i);
f.y = event.getY(i);
}
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
mActivePointers.remove(pointerId);
break;
}
}
invalidate();
return true;
}
Needless to say I want the code in Action_Move to be triggered by any pointer, not just the initial pointer. What is preventing pointers beyond index zero from triggering a move event?
As stated in the Documentation, MotionEvent.getActionIndex() only returns the Index for ACTION_POINTER_UP and ACTION_POINTER_DOWN (http://developer.android.com/reference/android/view/MotionEvent.html#getActionIndex%28%29). You however use the pointer ID generated from the pointer Index returned by said function in your log output. Hence, the log is incorrect, as the function doesn't return what you expect.
Im trying to do a pinch to zoom on a imageview. The problem is that the when i zoom it does'nt scale where i pinched it scales up to the left corner. Im not sure why i does this and i seems that there is a lot of people having the same problem but i havent found a soloution to it yet.
public override bool OnTouchEvent(MotionEvent ev)
{
_scaleDetector.OnTouchEvent(ev);
MotionEventActions action = ev.Action & MotionEventActions.Mask;
int pointerIndex;
switch (action)
{
case MotionEventActions.Down:
_lastTouchX = ev.GetX();
_lastTouchY = ev.GetY();
_activePointerId = ev.GetPointerId(0);
break;
case MotionEventActions.Move:
pointerIndex = ev.FindPointerIndex(_activePointerId);
float x = ev.GetX(pointerIndex);
float y = ev.GetY(pointerIndex);
if (!_scaleDetector.IsInProgress)
{
// Only move the ScaleGestureDetector isn't already processing a gesture.
float deltaX = x - _lastTouchX;
float deltaY = y - _lastTouchY;
_posX += deltaX;
_posY += deltaY;
Invalidate();
}
_lastTouchX = x;
_lastTouchY = y;
break;
case MotionEventActions.Up:
case MotionEventActions.Cancel:
// We no longer need to keep track of the active pointer.
_activePointerId = InvalidPointerId;
break;
case MotionEventActions.PointerUp:
// check to make sure that the pointer that went up is for the gesture we're tracking.
pointerIndex = (int) (ev.Action & MotionEventActions.PointerIndexMask) >> (int) MotionEventActions.PointerIndexShift;
int pointerId = ev.GetPointerId(pointerIndex);
if (pointerId == _activePointerId)
{
// This was our active pointer going up. Choose a new
// action pointer and adjust accordingly
int newPointerIndex = pointerIndex == 0 ? 1 : 0;
_lastTouchX = ev.GetX(newPointerIndex);
_lastTouchY = ev.GetY(newPointerIndex);
_activePointerId = ev.GetPointerId(newPointerIndex);
}
break;
}
return true;
}
protected override void OnDraw(Canvas canvas)
{
base.OnDraw(canvas);
canvas.Save();
canvas.Translate(_posX, _posY);
canvas.Scale(_scaleFactor, _scaleFactor, _lastTouchX, _lastTouchY);
_icon.Draw(canvas);
canvas.Restore();
}
I think it might have to do with this code in the beginning of the class where the bounds of the image is set to 0. But if i delete that code the image wont render.
public GestureRecognizer(Context context, ImageView imgview)
: base(context, null, 0)
{
//_icon = context.Resources.GetDrawable(Resource.Drawable.ic_launcher);
_icon = imgview.Drawable;
_icon.SetBounds(0, 0, _icon.IntrinsicWidth, _icon.IntrinsicHeight);
_scaleDetector = new ScaleGestureDetector(context, new MyScaleListener(this));
}
Have a look at MultiTouchController library. It makes it much easier to write multitouch applications for Android and pinch to zoom, rotate and drag is nicely implemented. Here is the link.
I try to fix an issue which appears in my code when I added multitouch functions in my app.
The problem seems to come from ACTION_POINTER_DOWN :
private float oldDist = 0;
backCard.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent me) {
switch(me.getAction()){
case MotionEvent.ACTION_DOWN:
firstX = (int) me.getX();
case MotionEvent.ACTION_POINTER_DOWN:
if(me.getPointerCount() >= 2){
oldDist = getSpacing(me);
System.out.println(oldDist);
}
break;
case MotionEvent.ACTION_MOVE:
float newDist = getSpacing(me);
if(newDist - oldDist > 200 && oldDist != 0){
System.out.println("Enabled");
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
break;
}
return true;
}
private float getSpacing(MotionEvent me){
float difx = me.getX(0) - me.getX(1);
float dify = me.getY(0) - me.getY(1);
float spacing = (float) Math.sqrt(difx*difx + dify*dify);
return spacing;
}
});
When I use it without the getPointerCount() condition in ACTION_POINTER_DOWN, I have an out of range error. But if I use the condition, the log doesn't show anything I print in the code. (Of course I use 2 fingers ! :) ), so the condition is never true, even if several fingers touch the screen at the same time.
How can I fix that ? Thank you.
My device is a GS3.
Use me.getActionMasked() instead of me.getAction()
Greets,
Does anyone how I go about detecting when a user presses on a bitmap which is inside a canvas?
Thanks
You should work with something like so:
public boolean onTouchEvent(MotionEvent event){
int action = event.getAction();
int x = event.getX() // or getRawX();
int y = event.getY();
switch(action){
case MotionEvent.ACTION_DOWN:
if (x >= xOfYourBitmap && x < (xOfYourBitmap + yourBitmap.getWidth())
&& y >= yOfYourBitmap && y < (yOfYourBitmap + yourBitmap.getHeight())) {
//tada, if this is true, you've started your click inside your bitmap
}
break;
}
}
That's a start, but you need to handle case MotionEvent.ACTION_MOVE and case MotionEvent.ACTION_UP to make sure you properly deal with the user input. The onTouchEvent method gets called every time the user: puts a finger down, moves a finger already on the screen or lifts a finger. Each time the event carries the X and Y coordinates of where the finger is. For example if you want to check for someone tapping inside your bitmap, you should do something like set a boolean that the touch started inside the bitmap on ACTION_DOWN, and then check on ACTION_UP that you're still inside the bitmap.
Steve,
Google has a great article and tutorial for handling UI Events # http://developer.android.com/guide/topics/ui/ui-events.html. This is what got me started and it was great for me :-)
This will detect a touch and check if it is not transparent. You need this if your bitmaps are not rectangles. myBitmap is just a simple container class I use.
private boolean clickOnBitmap(MyBitmap myBitmap, MotionEvent event) {
float xEnd = myBitmap.originX() + myBitmap.width();
float yEnd = myBitmap.originY() + myBitmap.height();;
if ((event.getX() >= myBitmap.originX() && event.getX() <= xEnd)
&& (event.getY() >= myBitmap.originY() && event.getY() <= yEnd) ) {
int pixX = (int) (event.getX() - myBitmap.originX());
int pixY = (int) (event.getY() - myBitmap.originY());
if (!(myBitmap.getBitmap().getPixel(pixX, pixY) == 0)) {
return true;
} else {
return false;
}
}
return false;
}
This is the on touch code for completeness
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (clickOnBitmap(dog, event)) {
Toast.makeText(getContext(), "dog", Toast.LENGTH_SHORT).show();
} else if (clickOnBitmap(mouse,event)) {
Toast.makeText(getContext(), "mouse", Toast.LENGTH_SHORT).show();
}
return true;
case MotionEvent.ACTION_OUTSIDE:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
return true;
}
return false;
}