Android Studio应用中多点触控事件失效问题排查
在以下代码的第12行附近,有一段标记为从未执行的代码。ACTION_POINTER_DOWN事件中其他功能正常:当摇杆被按下时,else if分支会调用joystick.setIsPressed(true),按逻辑后续触摸事件触发时,joystick.getIsPressed()应该返回true,从而进入第一个if分支执行标记的代码,但实际从未执行。
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: if (joystick.getIsPressed()) { // Joystick was pressed before this event if (event.getX() > screenX / 2 && event.getY() > screenY / 2) { cat.isJumping = true; } // Code here never gets executed } else if(joystick.isPressed((double) event.getX(), (double) event.getY())) { // Joystick is pressed in this event and ID stored joystickPointerId = event.getPointerId(event.getActionIndex()); joystick.setIsPressed(true); } else { // Joystick was not previously, and is not pressed in this event if (event.getX() > screenX / 2 && event.getY() > screenY / 2) { cat.isJumping = true; } } if(event.getX() >= mainMenuButton.getValue(1) && event.getX() <= mainMenuButton.getValue(3) && event.getY() >= mainMenuButton.getValue(2) && event.getY() <= mainMenuButton.getValue(4)) { isPlaying = false; activity.startActivity(new Intent(activity, MainActivity.class)); activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); activity.finish(); } break; case MotionEvent.ACTION_MOVE: if(joystick.getIsPressed()) { joystick.setActuator((double) event.getX(), (double) event.getY()); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: if (joystickPointerId == event.getPointerId(event.getActionIndex())) { // Joystick was let go of joystick.setIsPressed(false); joystick.resetActuator(); } cat.isJumping = false; break; } return true; }
package com.example.combatcats; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import androidx.core.graphics.ColorUtils; public class Joystick { private int outerCircleCenterPositionX; private int outerCircleCenterPositionY; private int innerCircleCenterPositionX; private int innerCircleCenterPositionY; private int outerCircleRadius; private int innerCircleRadius; private Paint outerCirclePaint; private Paint innerCirclePaint; private double joystickCenterToTouchDistance; private boolean isPressed; private double actuatorX; private double actuatorY; public Joystick(int centerpositionX, int centerpositionY, int outerCircleRadius, int innerCircleRadius) { outerCircleCenterPositionX = centerpositionX; outerCircleCenterPositionY = centerpositionY; innerCircleCenterPositionX = centerpositionX; innerCircleCenterPositionY = centerpositionY; this.outerCircleRadius = outerCircleRadius; this.innerCircleRadius = innerCircleRadius; outerCirclePaint = new Paint(); outerCirclePaint.setColor(ColorUtils.setAlphaComponent(Color.GRAY,125)); outerCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE); innerCirclePaint = new Paint(); innerCirclePaint.setColor(ColorUtils.setAlphaComponent(Color.BLUE,125)); innerCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE); } public void draw(Canvas canvas) { canvas.drawCircle( outerCircleCenterPositionX, outerCircleCenterPositionY, outerCircleRadius, outerCirclePaint ); canvas.drawCircle( innerCircleCenterPositionX, innerCircleCenterPositionY, innerCircleRadius, innerCirclePaint ); } public void update() { updateInnerCirclePosition(); } private void updateInnerCirclePosition() { innerCircleCenterPositionX = (int) (outerCircleCenterPositionX + actuatorX*outerCircleRadius); innerCircleCenterPositionY = (int) (outerCircleCenterPositionY + actuatorY*outerCircleRadius); } public boolean isPressed(double touchPositionX, double touchPositionY) { joystickCenterToTouchDistance = Math.sqrt( Math.pow(outerCircleCenterPositionX - touchPositionX, 2) + Math.pow(outerCircleCenterPositionY - touchPositionY, 2) ); return joystickCenterToTouchDistance < outerCircleRadius; } public void setIsPressed(boolean isPressed) { this.isPressed = isPressed; } public boolean getIsPressed() { return isPressed; } public void setActuator(double touchPositionX, double touchPositionY) { double deltaX = touchPositionX - outerCircleCenterPositionX; double deltaY = touchPositionY - outerCircleCenterPositionY; double deltaDistance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); if(deltaDistance < outerCircleRadius) { actuatorX = deltaX/outerCircleRadius; actuatorY = deltaY/outerCircleRadius; } else { actuatorX = deltaX/deltaDistance; actuatorY = deltaY/deltaDistance; } } public void resetActuator() { actuatorX = 0.0; actuatorY = 0.0; } public double getActuatorX() { return actuatorX; } public double getActuatorY() { return actuatorY; } }
核心问题
多指针触摸场景下,event.getX()和event.getY()默认返回**第一个指针(指针ID为0)**的坐标,而非当前触发ACTION_POINTER_DOWN事件的那个指针的坐标。
当你用第一个手指按住摇杆(触发ACTION_DOWN,设置joystick.isPressed=true),再用第二个手指触摸屏幕触发ACTION_POINTER_DOWN时,代码里的event.getX()取的是第一个手指的位置(摇杆区域),但此时joystick.getIsPressed()已经是true,理论上应该进入第一个if分支,但实际上第二个手指的触摸位置才是你需要判断是否触发跳跃的坐标,而你用了错误的坐标值,导致逻辑不符合预期,同时因为第一个手指在摇杆区域,event.getX() > screenX/2的条件大概率不成立,所以你误以为代码没执行。
修复方案
在处理ACTION_DOWN和ACTION_POINTER_DOWN时,获取当前触发事件的指针索引,用该索引获取对应触摸点的坐标:
// 获取当前触发事件的指针索引 int pointerIndex = event.getActionIndex(); // 获取该指针的坐标 float x = event.getX(pointerIndex); float y = event.getY(pointerIndex);
修改后的触摸事件关键代码如下:
case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: int pointerIndex = event.getActionIndex(); float x = event.getX(pointerIndex); float y = event.getY(pointerIndex); if (joystick.getIsPressed()) { // Joystick was pressed before this event if (x > screenX / 2 && y > screenY / 2) { cat.isJumping = true; } // 现在这段代码会正确执行 } else if(joystick.isPressed((double) x, (double) y)) { joystickPointerId = event.getPointerId(pointerIndex); joystick.setIsPressed(true); } else { if (x > screenX / 2 && y > screenY / 2) { cat.isJumping = true; } } if(x >= mainMenuButton.getValue(1) && x <= mainMenuButton.getValue(3) && y >= mainMenuButton.getValue(2) && y <= mainMenuButton.getValue(4)) { // 主菜单按钮逻辑... } break;
同时,ACTION_MOVE事件中也需要修正坐标获取逻辑,确保操作的是当前按住摇杆的指针:
case MotionEvent.ACTION_MOVE: if(joystick.getIsPressed()) { // 找到摇杆对应的指针索引 int joystickPointerIndex = event.findPointerIndex(joystickPointerId); if(joystickPointerIndex != -1) { float moveX = event.getX(joystickPointerIndex); float moveY = event.getY(joystickPointerIndex); joystick.setActuator((double) moveX, (double) moveY); } } break;
这样就能保证多指针场景下,每个触摸事件都使用正确的坐标进行判断,标记的代码也会按预期执行。
内容的提问来源于stack exchange,提问作者Haluna

