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.
Related
i have problem with onTouch. I have GridLayout where i have 3x3 buttons like a image below.
B1,B2 -- is the Button view. My problem is, when i put 3 fingers on the screen at the same time and move all of them down then my phone detect it like threeSwipeGesture and make screenshot. How to avoid phone to detect gesture?
TIP: When i put 3 fingers one by one (tap 1 - wait second - tap 2 - wait second - tap 3) then i can move my fingers and gesture is not detected.
#Override
public boolean onTouch(View view, MotionEvent event) {
int action = event.getActionMasked();
int index = event.getActionIndex();
int x = -1;
int y = -1;
switch (action) {
case MotionEvent.ACTION_MOVE:
return false;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_OUTSIDE:
case MotionEvent.ACTION_CANCEL:
gridLayout.setBackgroundColor(Color.parseColor("#df4930"));
set.clear();
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
x = (int) event.getX(index);
y = (int) event.getY(index);
for (int i = 0; i < gridLayout.getChildCount(); i++) {
View button = gridLayout.getChildAt(i);
Rect outRect = new Rect(button.getLeft(), button.getTop(), button.getRight(), button.getBottom());
if (outRect.contains(x, y)) {
set.add(button.getTag());
}
}
if (set.size() == 4) {
Log.d(DEBUG_TAG, set.toString());
gridLayout.setBackgroundColor(Color.parseColor("#ffc120"));
}
}
return true;
}
Here is error log in the console when gesture is detected.
D/ViewRootImpl: cancle motionEvent because of threeGesture detecting
I found out in one of my earlier questions that setting up specific x,y coordinates as Touch Listeners was too specific and that just one position would be too small to pick up a touch input.
#ligi mentioned Regions in a comment under my question. I looked into it and realised that it's what I need, I just can't find out how to implement one.
Here is the code for a TouchListener for a specific x,y coordinate:
public boolean onTouchEvent(MotionEvent ev) {
if (!mDragging) {
return false;
}
int x = (int) ev.getX();
int y = (int) ev.getY();
if (x == 100 && y == 200) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "ACTION DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "ACTION MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "ACTION UP");
break;
}
}
Does anyone know how to set up a Region instead? Say if a user finger moves into the region area, do something. I can't find any basic implementations anywhere else.
Here's an image if the desc. wasn't clear:
You could do something like this
public boolean onTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "ACTION DOWN OR MOVE");
// check if the event is in region 1
if(x >= "left_x_region1" && x <= "right_x_region1"
&& y >= "top_x_region1" && y <= "bottom_x_region1")
// do something for region1
// check if the event is in region 2
else if(x >= "left_x_region2" && x <= "right_x_region2"
&& y >= "top_x_region2" && y <= "bottom_x_region2")
// do something for region1
// continue for other cases
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "ACTION UP");
break;
}
}
Or you could simplify it even further by making a list of what those actual values are, or even better create Rects from those values
List<Rect> regions = new ArrayList<Rect>();
regions.add(new Rect("left_x_region1", "top_y_region1", "right_x_region1", "bottom_y_region1");
regions.add(new Rect("left_x_region2", "top_y_region2", "right_x_region2", "bottom_y_region2");
// continue for each region
and then check your event in those regions inside your ACTION_DOWN || ACTION_MOVE section of the switch statement
for(int i = 0; i < regions.size(); i++){
if(regions.get(i).contains(x, y)){
// do stuff for region i
return;
}
}
What this does is checks to see if the motion event occurred within the bounds of the region.
I would imagine you'd use a rect in Android. You'd specify the rectangle(s) and see if the rect(s) contains your value. In the example below I made a list of rectangles so there are multiple hotspots. You could ideally just use one if you want just one specific rectangular region. The .contains internal method allows you to specify regions which your x,y coordinates would potentially entail.
List<Rect> retangles;
...
for(Rect rect : rectangles){
if(rect.contains(x,y)){
System.out.println("Touched Rectangle, do what you need to do.");
}
}
or for just one
Rect rect = new Rect(0, 0, 200, 100);
if(rect.contains(x,y))
System.out.println("Touched Rectangle, do what you need to do.");
Consider the following scheme below (for sake of better understanding of my problem).
As you can see, I am considering a list view surrounded by padding. Now, If a user presses a listview item, as the action I have provided it light blue background color. Now, My application is dealing with onTouch Events itself to determine actions like
Click
Left to Right Swipe
Right to Left Swipe
Here is my code.
public boolean onTouch(View v, MotionEvent event) {
if(v == null)
{
mSwipeDetected = Action.None;
return false;
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
downX = event.getRawX();
downY = event.getRawY();
mSwipeDetected = Action.Start;
// Find the child view that was touched (perform a hit test)
Rect rect = new Rect();
int childCount = listView.getChildCount();
int[] listViewCoords = new int[2];
listView.getLocationOnScreen(listViewCoords);
int x = (int) event.getRawX() - listViewCoords[0];
int y = (int) event.getRawY() - listViewCoords[1];
View child;
for (int i = 0; i < childCount; i++) {
child = listView.getChildAt(i);
child.getHitRect(rect);
if (rect.contains(x, y)) {
mDownView = child;
break;
}
}
return false; // allow other events like Click to be processed
}
case MotionEvent.ACTION_MOVE: {
upX = event.getRawX();
upY = event.getRawY();
float deltaX=0,deltaY=0;
deltaX = downX - upX;
deltaY = downY - upY;
if(deltaY < VERTICAL_MIN_DISTANCE)
{
setTranslationX(mDownView, -(deltaX));
setAlpha(mDownView, Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / listView.getWidth())));
return false;
}
else
{
forceBringBack(v);
}
return false;
}
case MotionEvent.ACTION_UP:
{
stopX = event.getX();
float stopValueY = event.getRawY() - downY;
float stopValue = stopX - downX;
if(!mDownView.isPressed())
{
forceBringBack(mDownView);
return false;
}
boolean dismiss = false;
boolean dismissRight = false;
if(Math.abs(stopValue)<10)
{
mSwipeDetected = Action.Start;
}
else
{
mSwipeDetected = Action.None;
}
String log = "";
Log.d(log, "Here is Y" + Math.abs(stopValueY));
Log.d(log, "First Comparison of Stop Value > with/4" + (Math.abs(stopValue) > (listView.getWidth() /4)));
Log.d(log, "Second Comparison " + (Math.abs(stopValueY)<VERTICAL_MIN_DISTANCE));
Log.d(log, "Action Detected is " + mSwipeDetected + " with Stop Value " + stopValue);
if((Math.abs(stopValue) > (listView.getWidth() /4))&&(Math.abs(stopValueY)<VERTICAL_MIN_DISTANCE))
{
dismiss = true;
dismissRight = stopValue > 0;
if(stopValue>0)
{
mSwipeDetected = Action.LR;
}
else
mSwipeDetected = Action.RL;
}
Log.d(log, "Action Detected is " + mSwipeDetected + " with Stop Value after dissmiss" + stopValue);
if(dismiss)
{
if(dismissRight)
mSwipeDetected = Action.LR;
else
mSwipeDetected = Action.RL;
animate(mDownView)
.translationX(dismissRight ? listView.getWidth() : - listView.getWidth())
.alpha(0)
.setDuration(mAnimationTime)
.setListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation)
{
}
});
}
else
{
animate(mDownView)
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setListener(null);
}
break;
}
}
return false;
}
As you can see, I determine the performed action in MotionEvent.ACTION_UP and set the value of Enum Action accordingly. This logic works like a charm if the user does not crosses the list view boundary.
Now, if the user, while sliding (or specifically), moving his finger along the list item moves from blue to orange, the MotionEvent.ACTION_UP would not be given to listview, which causes my code not to make a decision and due to translationX() method and setAlpha(), since no Action is ever determined in this case, that particular list item gets blank.
The problem does not stops here, since, I am not inflating view each time, same translatedX() row gets inflated each time leading to multiple occurance of a blank/white list item.
Is there anything possible to do so that even if I didn't encounter MotionEvent.ACTION_UP, I could still make some decison ?
You should return true; in case MotionEvent.ACTION_DOWN:, so the MotionEvent.ACTION_UP will be handled.
As explained on View.OnTouchListener:
Returns:
True if the listener has consumed the event, false otherwise.
MotionEvent.ACTION_UP Won't get called until the MotionEvent.ACTION_DOWN occurred, a logical explanation for this is that it's impossible for an ACTION_UP to occur if an ACTION_DOWN never occurred before it.
This logic enables the developer to block further events after ACTION_DOWN.
Also note that under certain circumstances (eg. screen rotations) the gesture may be cancelled, in which case a MotionEvent.ACTION_UP will NOT be sent. Instead a MotionEvent.ACTION_CANCEL is sent instead. Therefore a normal action switch statement should look something like this:
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// check if we want to handle touch events, return true
// else don't handle further touch events, return false
break;
// ... handle other cases
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// finish handling touch events
// note that these methods won't be called if 'false' was returned
// from any previous events related to the gesture
break;
}
I don't think adding return true;to case MotionEvent.ACTION_DOWN: would eventually solve the problem. It just complicated the situation where return false could have done the job like a charm.
What to notice is: MotionEvent.ACTION_DOWN: /*something*/ return true; will block any other Listener callbacks available for the view, even onClickListenerm, while correctly return false in MotionEvent.ACTION_UP: can help the MotionEvent be propagated to the right destination.
Reference to his original code sourse: https://github.com/romannurik/android-swipetodismiss
As Danpe explained in his concise answer - I had to add the ACTION_DOWN code in order to have ACTION_UP recognized.
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_UP:
XyPos xyPos = new XyPos();
xyPos.x = last_x;
xyPos.y = last_y;
handleViewElementPositionUpdate(xyPos);
break;
I had the entire onTouch(..) method returning true anyway, so I'm not sure why that wasn't enough ... but nice to have this quick solution .. (thanks!)
i am a student developing an air hockey android games.
i am having a problem with understanding multi touch.
i just learns about ACTION_DOWN, ACTION_POINTER_DOWN etc.
but by problem is at ACTION_MOVE.
i create 2 sprite for 2 player.1st sprite will move where my 1st finger go, but my 2nd sprite doesn't move where my 2nd finger move.
my question is, how i want to identified which finger is moving in ACTION_MOVE? i have tried to use getPointerId(index), but i am not understand how to use it because the index is changing if the 1st finger leave the screen
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
break;
}
case MotionEvent.ACTION_UP: {
break;
}
case MotionEvent.ACTION_POINTER_UP: {
break;
}
case MotionEvent.ACTION_MOVE: {
if((int)event.getPointerId(index) == 0){ //i know this IF statement is wrong, what should i do?
player1.setX((int)event.getX()); //player1 & player2 is a sprite object
player1.setY((int)event.getY());
}
if((int)event.getPointerId(index) == 1){
player1.setX((int)event.getX());
player1.setY((int)event.getY());
}
}
}
You cannot know which area will be touched first. The first finger/ touch gets pointer id = 0 the second = 1 and so on. What you can do is this:
public boolean onTouchEvent(MotionEvent event) {
// If this does not work search for a way to get the screen width
WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
int eventaction = event.getActionMasked();
int num = event.getPointerCount();
// For every touch
for (int a = 0; a < num; a++) {
int X = (int) event.getX(event.getPointerId(a));
int Y = (int) event.getY(event.getPointerId(a));
int allowed_touch_range = display.getWidth() / 2; // Your screen width divided by 2
switch (eventaction) {
case MotionEvent.ACTION_MOVE:
// Left half of the screen
if (X < allowed_touch_range) {
/* Player1 Move */
}
// Rigth half
if (X > allowed_touch_range) {
/* Player2 Move */
}
break;
}
}
return true;
}
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.