Multitouch Virtual Joystick Controller Class, working in APDE

Here an example sketch.


// All the needed imports
import android.view.MotionEvent;
import android.os.Vibrator;
import android.content.Context;

// Callback interfaces
interface JoystickListener {
  void onJoystickPress(int id);
  void onJoystickMove(int id, float x, float y);
  void onJoystickRelease(int id);
}

interface ButtonListener {
  void onButtonPress(int index, String name);
  void onButtonRelease(int index, String name);
}

// Global joystick outputs
HashMap<Integer, Float[]> joystickOutputs = new HashMap<Integer, Float[]>();

// Main sketch
TouchController controller;
Sprite player; // Just for testing

void setup() {
  fullScreen();
  orientation(LANDSCAPE);
  controller = new TouchController("layout.json"); // if the file doesn't exist, a default layout will be loaded.
  player = new Sprite(width/2, height/2, 5.0);

  // Set joystick listener
  controller.setJoystickListener(new JoystickListener() {
    @Override
      void onJoystickPress(int id) {
      println("Joystick " + id + " pressed (listener)");
    }
    @Override
      void onJoystickMove(int id, float x, float y) {
      if (id == 1) {
        player.update(x, y);
      }
    }
    @Override
      void onJoystickRelease(int id) {
      println("Joystick " + id + " released (listener)");
    }
  }
  );

  // Set button listener
  controller.setButtonListener(new ButtonListener() {
    @Override
      void onButtonPress(int index, String name) {
      println("Button " + index + ": " + name + " pressed (listener)");
      if (name.equals("fire") || name.equals("toggle")) {
        fill(0, 255, 0); // Example: change player color
      }
    }
    @Override
      void onButtonRelease(int index, String name) {
      println("Button " + index + ": " + name + " released (listener)");
      if (name.equals("fire") || name.equals("toggle")) {
        fill(255, 0, 0); // Reset color
      }
    }
  }
  );
}

void draw() {
  background(255);
  controller.update();
  player.draw();
  controller.draw();

  // Display joystick values for debugging
  textAlign(CENTER);
  fill(0);
  int yOffset = 50;
  for (Integer id : joystickOutputs.keySet()) {
    Float[] outputs = joystickOutputs.get(id);
    text("Joystick " + id + ": X=" + nf(outputs[0], 1, 2) + ", Y=" + nf(outputs[1], 1, 2), width/2, yOffset);
    yOffset += 50;
  }
}

void touchStarted(TouchEvent e) {
  controller.touchStarted((MotionEvent) e.getNative());
}

void touchMoved(TouchEvent e) {
  controller.touchMoved((MotionEvent) e.getNative());
}

void touchEnded(TouchEvent e) {
  controller.touchEnded((MotionEvent) e.getNative());
}


// Action functions (fallback)

void action(int buttonIndex, String name) {
  println("Button " + buttonIndex + ": " + name + " pressed");
}

void actionUp(int buttonIndex, String name) {
  println("Button " + buttonIndex + ": " + name + " released");
}

void actionJoystick(int joystickIndex) {
  println("Joystick " + joystickIndex + " pressed");
}

void actionJoystickUp(int joystickIndex) {
  println("Joystick " + joystickIndex + " released");
}


// Controller class
class TouchController {
  // Timer type constants
  public static final String TIMER_TYPE_NONE = "none";
  public static final String TIMER_TYPE_BLOCK = "block";
  public static final String TIMER_TYPE_LATCHING = "latching";
  public static final String TIMER_TYPE_TOGGLE = "toggle";

  // Vibrator
  private Vibrator vibrator;
  private JoystickListener joystickListener;
  private ButtonListener buttonListener;

  // Joystick class
  private class Joystick {
    int id;
    float x, y;
    float radius;
    float dotRadius;
    float deadzone;
    float dotX, dotY;
    boolean active;
    int touchId;
    boolean returnToCenter;
    boolean slideOffRelease;
    PImage baseImage;
    PImage knobImage;
    PImage knobPressedImage;
    boolean useImageSize;
    boolean visible;
    float opacity;
    float touchRadius;
    long vibrateDuration;
    boolean vibrateOnMove;
    float vibrateOnMoveDist;
    float sensitivity;
    float lastVibrateX, lastVibrateY;

    Joystick(int id, float x, float y, float radius) {
      this.id = id;
      this.x = x <= 1.0 ? x * width : x;
      this.y = y <= 1.0 ? y * height : y;
      this.radius = radius;
      this.dotRadius = radius * 0.3;
      this.deadzone = 0.2;
      this.dotX = this.x;
      this.dotY = this.y;
      this.active = false;
      this.touchId = -1;
      this.returnToCenter = true;
      this.slideOffRelease = false;
      this.baseImage = null;
      this.knobImage = null;
      this.knobPressedImage = null;
      this.useImageSize = false;
      this.visible = true;
      this.opacity = 1.0;
      this.touchRadius = radius;
      this.vibrateDuration = 0;
      this.vibrateOnMove = false;
      this.vibrateOnMoveDist = 0.2;
      this.sensitivity = 1.0;
      this.lastVibrateX = this.x;
      this.lastVibrateY = this.y;
      joystickOutputs.put(id, new Float[]{0.0, 0.0});
    }
  }

  // Button class
  private class Button {
    float x, y;
    float radius;
    boolean active;
    int touchId;
    int index;
    String name;
    boolean slideOffRelease;
    boolean slideOn;
    String timerType;
    long timerDuration;
    long activationTime;
    long blockedUntil;
    PImage inactiveImage;
    PImage activeImage;
    boolean useImageSize;
    boolean visible;
    float opacity;
    float touchRadius;
    long vibrateDuration;

    Button(String name, float x, float y, float radius) {
      this(name, x, y, radius, false, true, TIMER_TYPE_NONE, 0);
    }

    Button(String name, float x, float y, float radius, boolean slideOffRelease, boolean slideOn, 
      String timerType, long timerDuration) {
      this.name = name;
      this.x = x <= 1.0 ? x * width : x;
      this.y = y <= 1.0 ? y * height : y;
      this.radius = radius;
      this.active = false;
      this.touchId = -1;
      this.index = nextButtonIndex++;
      this.slideOffRelease = slideOffRelease;
      this.slideOn = slideOn;
      this.timerType = timerType;
      this.timerDuration = timerDuration;
      this.activationTime = 0;
      this.blockedUntil = 0;
      this.inactiveImage = null;
      this.activeImage = null;
      this.useImageSize = false;
      this.visible = true;
      this.opacity = 1.0;
      this.touchRadius = radius;
      this.vibrateDuration = 0;
    }
  }

  // Storage
  private HashMap<Integer, Joystick> joysticks;
  private HashMap<String, Button> buttons;
  private int nextButtonIndex;
  private ArrayList<Integer> touchIds;

  // Empty constructor
  TouchController() {
    joysticks = new HashMap<Integer, Joystick>();
    buttons = new HashMap<String, Button>();
    nextButtonIndex = 0;
    touchIds = new ArrayList<Integer>();
    vibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
    joystickListener = null;
    buttonListener = null;
  }

  // JSON constructor
  TouchController(String filename) {
    this();
    loadLayout(filename);
  }

  // Set listeners
  void setJoystickListener(JoystickListener listener) {
    this.joystickListener = listener;
  }

  void setButtonListener(ButtonListener listener) {
    this.buttonListener = listener;
  }

  // Vibrate method
  void vibrate(long duration) {
    if (vibrator != null && duration > 0) {
      vibrator.vibrate(duration);
    }
  }

  // Reload layout
  void reloadLayout(String filename) {
    loadLayout(filename);
  }

  // Load layout from JSON file
  void loadLayout(String filename) {
    try {
      JSONObject json = loadJSONObject(filename);
      joysticks.clear();
      joystickOutputs.clear();
      buttons.clear();
      nextButtonIndex = 0;

      // Parse joysticks
      JSONArray joysticksJson = json.getJSONArray("joysticks");
      for (int i = 0; i < joysticksJson.size(); i++) {
        JSONObject joy = joysticksJson.getJSONObject(i);
        int id = joy.getInt("id");
        float x = joy.getFloat("x");
        float y = joy.getFloat("y");
        float radius = joy.getFloat("radius");
        addJoystick(id, x, y, radius);
        Joystick j = joysticks.get(id);
        j.dotRadius = joy.isNull("dotRadius") ? j.dotRadius : max(0, joy.getFloat("dotRadius"));
        j.deadzone = joy.isNull("deadzone") ? j.deadzone : max(0, min(1, joy.getFloat("deadzone")));
        j.slideOffRelease = joy.isNull("slideOffRelease") ? j.slideOffRelease : joy.getBoolean("slideOffRelease");
        j.returnToCenter = joy.isNull("returnToCenter") ? j.returnToCenter : joy.getBoolean("returnToCenter");
        j.useImageSize = joy.isNull("useImageSize") ? j.useImageSize : joy.getBoolean("useImageSize");
        j.visible = joy.isNull("visible") ? j.visible : joy.getBoolean("visible");
        j.opacity = joy.isNull("opacity") ? j.opacity : max(0, min(1, joy.getFloat("opacity")));
        j.touchRadius = joy.isNull("touchRadius") ? j.touchRadius : max(j.radius, joy.getFloat("touchRadius"));
        j.vibrateDuration = joy.isNull("vibrateDuration") ? 0 : (long) max(0, joy.getFloat("vibrateDuration"));
        j.vibrateOnMove = joy.isNull("vibrateOnMove") ? j.vibrateOnMove : joy.getBoolean("vibrateOnMove");
        j.vibrateOnMoveDist = joy.isNull("vibrateOnMoveDist") ? j.vibrateOnMoveDist : max(0, joy.getFloat("vibrateOnMoveDist"));
        j.sensitivity = joy.isNull("sensitivity") ? 1.0 : max(0, min(2, joy.getFloat("sensitivity")));
        String baseImage = joy.isNull("baseImage") ? null : joy.getString("baseImage");
        String knobImage = joy.isNull("knobImage") ? null : joy.getString("knobImage");
        String knobPressedImage = joy.isNull("knobPressedImage") ? null : joy.getString("knobPressedImage");
        if (baseImage != null) setJoystickBaseImage(id, baseImage);
        if (knobImage != null) setJoystickKnobImage(id, knobImage, knobPressedImage);
      }

      // Parse buttons
      JSONArray buttonsJson = json.getJSONArray("buttons");
      for (int i = 0; i < buttonsJson.size(); i++) {
        JSONObject btn = buttonsJson.getJSONObject(i);
        String name = btn.getString("name");
        float x = btn.getFloat("x");
        float y = btn.getFloat("y");
        float radius = btn.getFloat("radius");
        boolean slideOffRelease = btn.isNull("slideOffRelease") ? false : btn.getBoolean("slideOffRelease");
        boolean slideOn = btn.isNull("slideOn") ? true : btn.getBoolean("slideOn");
        String timerType = btn.isNull("timerType") ? TIMER_TYPE_NONE : btn.getString("timerType");
        long timerDuration = btn.isNull("timerDuration") ? 0 : (long) max(0, btn.getFloat("timerDuration"));
        boolean useImageSize = btn.isNull("useImageSize") ? false : btn.getBoolean("useImageSize");
        boolean visible = btn.isNull("visible") ? true : btn.getBoolean("visible");
        float opacity = btn.isNull("opacity") ? 1.0 : max(0, min(1, btn.getFloat("opacity")));
        float touchRadius = btn.isNull("touchRadius") ? radius : max(radius, btn.getFloat("touchRadius"));
        long vibrateDuration = btn.isNull("vibrateDuration") ? 0 : (long) max(0, btn.getFloat("vibrateDuration"));

        if (timerType.equals("none") || timerType.equals("block") || timerType.equals("latching") || timerType.equals("toggle")) {
          addButton(name, x, y, radius, slideOffRelease, slideOn, timerType, timerDuration);
          Button b = buttons.get(name);
          b.useImageSize = useImageSize;
          b.visible = visible;
          b.opacity = opacity;
          b.touchRadius = touchRadius;
          b.vibrateDuration = vibrateDuration;
          String inactiveImage = btn.isNull("inactiveImage") ? null : btn.getString("inactiveImage");
          String activeImage = btn.isNull("activeImage") ? null : btn.getString("activeImage");
          if (inactiveImage != null && activeImage != null) {
            setButtonImage(name, inactiveImage, activeImage);
          }
        }
      }
    } 
    catch (Exception e) {
      // Fallback default layout: one joystick, two buttons
      addJoystick(1, 0.2, 0.8, 100);
      Joystick j = joysticks.get(1);
      j.dotRadius = 30;
      j.deadzone = 0.2;
      j.slideOffRelease = false;
      j.returnToCenter = true;
      addButton("fire", 0.8, 0.6, 50, true, true, "none", 0);
      addButton("jump", 0.8, 0.4, 50, false, true, "block", 500);
    }
  }

  // Add joystick
  void addJoystick(int id, float x, float y, float radius) {
    if (!joysticks.containsKey(id)) {
      joysticks.put(id, new Joystick(id, x, y, radius));
    }
  }

  // Remove joystick
  void removeJoystick(int id) {
    if (joysticks.containsKey(id)) {
      joysticks.remove(id);
      joystickOutputs.remove(id);
    }
  }

  // Clear all joysticks
  void clearJoysticks() {
    joysticks.clear();
    joystickOutputs.clear();
  }

  // Set joystick properties
  void setJoystickBaseImage(int id, String filename) {
    if (joysticks.containsKey(id)) {
      PImage img = loadImage(filename);
      joysticks.get(id).baseImage = img != null ? img : null;
    }
  }

  void setJoystickKnobImage(int id, String filename, String pressedFilename) {
    if (joysticks.containsKey(id)) {
      Joystick j = joysticks.get(id);
      PImage img = loadImage(filename);
      j.knobImage = img != null ? img : null;
      PImage pressedImg = pressedFilename != null ? loadImage(pressedFilename) : null;
      j.knobPressedImage = pressedImg != null ? pressedImg : null;
    }
  }

  void setJoystickDotRadius(int id, float dotRadius) {
    if (joysticks.containsKey(id)) {
      joysticks.get(id).dotRadius = max(0, dotRadius);
    }
  }

  void setJoystickDeadzone(int id, float deadzone) {
    if (joysticks.containsKey(id)) {
      joysticks.get(id).deadzone = max(0, min(1, deadzone));
    }
  }

  void setJoystickSlideOffRelease(int id, boolean enabled) {
    if (joysticks.containsKey(id)) {
      joysticks.get(id).slideOffRelease = enabled;
    }
  }

  void setJoystickReturnToCenter(int id, boolean enabled) {
    if (joysticks.containsKey(id)) {
      joysticks.get(id).returnToCenter = enabled;
    }
  }

  void setJoystickUseImageSize(int id, boolean enabled) {
    if (joysticks.containsKey(id)) {
      joysticks.get(id).useImageSize = enabled;
    }
  }

  void setJoystickVisible(int id, boolean visible) {
    if (joysticks.containsKey(id)) {
      joysticks.get(id).visible = visible;
    }
  }

  void setJoystickOpacity(int id, float opacity) {
    if (joysticks.containsKey(id)) {
      joysticks.get(id).opacity = max(0, min(1, opacity));
    }
  }

  void setJoystickTouchRadius(int id, float touchRadius) {
    if (joysticks.containsKey(id)) {
      Joystick j = joysticks.get(id);
      j.touchRadius = max(j.radius, touchRadius);
    }
  }

  void setJoystickVibrateDuration(int id, long duration) {
    if (joysticks.containsKey(id)) {
      joysticks.get(id).vibrateDuration = (long) max(0, duration);
    }
  }

  void setJoystickVibrateOnMove(int id, boolean enabled) {
    if (joysticks.containsKey(id)) {
      joysticks.get(id).vibrateOnMove = enabled;
    }
  }

  void setJoystickVibrateOnMoveDist(int id, float dist) {
    if (joysticks.containsKey(id)) {
      joysticks.get(id).vibrateOnMoveDist = max(0, dist);
    }
  }

  void setJoystickSensitivity(int id, float sensitivity) {
    if (joysticks.containsKey(id)) {
      joysticks.get(id).sensitivity = max(0, min(2, sensitivity));
    }
  }

  // Set button properties
  void setButtonImage(String name, String inactiveFilename, String activeFilename) {
    if (buttons.containsKey(name)) {
      Button btn = buttons.get(name);
      btn.inactiveImage = loadImage(inactiveFilename);
      btn.activeImage = loadImage(activeFilename);
    }
  }

  void setButtonUseImageSize(String name, boolean enabled) {
    if (buttons.containsKey(name)) {
      buttons.get(name).useImageSize = enabled;
    }
  }

  void setButtonSlideOffRelease(String name, boolean enabled) {
    if (buttons.containsKey(name)) {
      buttons.get(name).slideOffRelease = enabled;
    }
  }

  void setButtonSlideOn(String name, boolean enabled) {
    if (buttons.containsKey(name)) {
      buttons.get(name).slideOn = enabled;
    }
  }

  void setButtonTimerType(String name, String timerType) {
    if (buttons.containsKey(name) && 
      (timerType.equals("none") || 
      timerType.equals("block") || 
      timerType.equals("latching") || 
      timerType.equals("toggle"))) {
      Button btn = buttons.get(name);
      btn.timerType = timerType;
      if (!timerType.equals(TIMER_TYPE_BLOCK)) {
        btn.blockedUntil = 0;
      }
      if (timerType.equals(TIMER_TYPE_NONE) || timerType.equals(TIMER_TYPE_TOGGLE)) {
        btn.timerDuration = 0;
      }
    }
  }

  void setButtonTimerDuration(String name, long timerDuration) {
    if (buttons.containsKey(name)) {
      Button btn = buttons.get(name);
      btn.timerDuration = (long) max(0, timerDuration);
      if (btn.active && btn.timerType.equals(TIMER_TYPE_BLOCK) && btn.activationTime > 0) {
        btn.blockedUntil = btn.activationTime + btn.timerDuration;
      }
    }
  }

  void setButtonVisible(String name, boolean visible) {
    if (buttons.containsKey(name)) {
      buttons.get(name).visible = visible;
    }
  }

  void setButtonOpacity(String name, float opacity) {
    if (buttons.containsKey(name)) {
      buttons.get(name).opacity = max(0, min(1, opacity));
    }
  }

  void setButtonTouchRadius(String name, float touchRadius) {
    if (buttons.containsKey(name)) {
      Button btn = buttons.get(name);
      btn.touchRadius = max(btn.radius, touchRadius);
    }
  }

  void setButtonVibrateDuration(String name, long duration) {
    if (buttons.containsKey(name)) {
      buttons.get(name).vibrateDuration = (long) max(0, duration);
    }
  }

  // Add a button (minimal) for quick builds
  void addButton(String name, float x, float y, float radius) {
    addButton(name, x, y, radius, false, true, TIMER_TYPE_NONE, 0);
  }

  // Add a button (full, for JSON)
  void addButton(String name, float x, float y, float radius, boolean slideOffRelease, boolean slideOn, 
    String timerType, long timerDuration) {
    if (!buttons.containsKey(name) && 
      (timerType.equals("none") || 
      timerType.equals("block") || 
      timerType.equals("latching") || 
      timerType.equals("toggle"))) {
      buttons.put(name, new Button(name, x, y, radius, slideOffRelease, slideOn, timerType, timerDuration));
    }
  }

  // Remove a button
  void removeButton(String name) {
    if (buttons.containsKey(name)) {
      buttons.remove(name);
    }
  }

  // Clear all buttons
  void clearButtons() {
    buttons.clear();
    nextButtonIndex = 0;
  }

  void update() {
    // Update joysticks
    for (Joystick j : joysticks.values()) {
      if (!j.active && j.returnToCenter) {
        j.dotX = j.x;
        j.dotY = j.y;
        joystickOutputs.get(j.id)[0] = 0.0;
        joystickOutputs.get(j.id)[1] = 0.0;
      }
    }

    // Update button timers
    long currentTime = millis();
    for (Button btn : buttons.values()) {
      if (btn.timerType.equals(TIMER_TYPE_BLOCK) && btn.blockedUntil > 0 && currentTime >= btn.blockedUntil) {
        btn.blockedUntil = 0;
      }
      if (btn.active && (btn.timerType.equals(TIMER_TYPE_BLOCK) || btn.timerType.equals(TIMER_TYPE_LATCHING))) {
        if (btn.activationTime > 0 && currentTime >= btn.activationTime + btn.timerDuration) {
          btn.active = false;
          btn.touchId = -1;
          btn.activationTime = 0;
          if (buttonListener != null) {
            buttonListener.onButtonRelease(btn.index, btn.name);
          } else {
            actionUp(btn.index, btn.name);
          }
        }
      }
      if (!btn.active && !btn.timerType.equals(TIMER_TYPE_LATCHING) && !btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
        btn.touchId = -1;
        btn.activationTime = 0;
      }
    }
  }

  void draw() {
    // Draw joysticks
    for (Joystick j : joysticks.values()) {
      if (!j.visible) continue;
      pushStyle();
      tint(255, j.opacity * 255);
      // Draw base
      if (j.baseImage != null && !j.useImageSize) {
        image(j.baseImage, j.x - j.radius, j.y - j.radius, j.radius * 2, j.radius * 2);
      } else if (j.baseImage != null) {
        image(j.baseImage, j.x - j.baseImage.width / 2, j.y - j.baseImage.height / 2);
      } else {
        fill(100, 100, 100, j.opacity * 150);
        ellipse(j.x, j.y, j.radius * 2, j.radius * 2);
      }
      // Draw deadzone.
      fill(150, 150, 150, j.opacity * 100);
      ellipse(j.x, j.y, j.radius * j.deadzone * 2, j.radius * j.deadzone * 2);
      // Draw knob
      if (j.knobImage != null && !j.useImageSize) {
        PImage knobImg = (j.active && j.knobPressedImage != null) ? j.knobPressedImage : j.knobImage;
        image(knobImg, j.dotX - j.dotRadius, j.dotY - j.dotRadius, j.dotRadius * 2, j.dotRadius * 2);
      } else if (j.knobImage != null) {
        PImage knobImg = (j.active && j.knobPressedImage != null) ? j.knobPressedImage : j.knobImage;
        image(knobImg, j.dotX - knobImg.width / 2, j.dotY - knobImg.height / 2);
      } else {
        fill(200, 200, 200, j.opacity * 200);
        ellipse(j.dotX, j.dotY, j.dotRadius * 2, j.dotRadius * 2);
      }
      popStyle();
    }

    // Draw buttons
    for (Button btn : buttons.values()) {
      if (!btn.visible) continue;
      pushStyle();
      tint(255, btn.opacity * 255);
      if (btn.inactiveImage != null && btn.activeImage != null && !btn.useImageSize) {
        PImage img = btn.active ? btn.activeImage : btn.inactiveImage;
        image(img, btn.x - btn.radius, btn.y - btn.radius, btn.radius * 2, btn.radius * 2);
      } else if (btn.inactiveImage != null && btn.activeImage != null) {
        PImage img = btn.active ? btn.activeImage : btn.inactiveImage;
        image(img, btn.x - img.width / 2, btn.y - img.height / 2);
      } else {
        fill(btn.active ? color(255, 100, 100, btn.opacity * 255) : color(100, 100, 255, btn.opacity * 150));
        ellipse(btn.x, btn.y, btn.radius * 2, btn.radius * 2);
      }
      popStyle();
    }
  }

  void touchStarted(MotionEvent me) {
    ArrayList<Integer> newIds = getMotionEventTouchIds(me);
    if (me.getActionMasked() != MotionEvent.ACTION_DOWN) {
      newIds.removeAll(touchIds);
    }

    for (int id : newIds) {
      int ptrIdx = me.findPointerIndex(id);
      if (ptrIdx < 0) continue;
      float px = me.getX(ptrIdx);
      float py = me.getY(ptrIdx);

      // Check joysticks
      for (Joystick j : joysticks.values()) {
        if (!j.visible || j.active || dist(px, py, j.x, j.y) >= j.touchRadius) continue;
        j.active = true;
        j.touchId = id;
        updateJoystick(j, px, py);
        if (j.vibrateDuration > 0) vibrate(j.vibrateDuration);
        if (joystickListener != null) {
          joystickListener.onJoystickPress(j.id);
        } else {
          actionJoystick(j.id);
        }
        break;
      }

      // Check buttons
      for (Button btn : buttons.values()) {
        if (!btn.visible || dist(px, py, btn.x, btn.y) >= btn.touchRadius) continue;
        if (btn.timerType.equals(TIMER_TYPE_BLOCK) && btn.blockedUntil > millis()) continue;
        if (btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
          // Toggle mode: flip active state
          btn.active = !btn.active;
          btn.touchId = id;
          if (btn.active) {
            btn.activationTime = millis();
            if (btn.vibrateDuration > 0) vibrate(btn.vibrateDuration);
            if (buttonListener != null) {
              buttonListener.onButtonPress(btn.index, btn.name);
            } else {
              action(btn.index, btn.name);
            }
          } else {
            btn.touchId = -1;
            btn.activationTime = 0;
            if (buttonListener != null) {
              buttonListener.onButtonRelease(btn.index, btn.name);
            } else {
              actionUp(btn.index, btn.name);
            }
          }
        } else if (!btn.active) {
          // Non-toggle modes: activate if not already active
          btn.active = true;
          btn.touchId = id;
          btn.activationTime = millis();
          if (btn.timerType.equals(TIMER_TYPE_BLOCK)) {
            btn.blockedUntil = btn.activationTime + btn.timerDuration;
          }
          if (btn.vibrateDuration > 0) vibrate(btn.vibrateDuration);
          if (buttonListener != null) {
            buttonListener.onButtonPress(btn.index, btn.name);
          } else {
            action(btn.index, btn.name);
          }
        }
        break;
      }
    }

    touchIds = getMotionEventTouchIds(me);
  }

  void touchMoved(MotionEvent me) {
    for (int i = 0; i < me.getPointerCount(); i++) {
      int id = me.getPointerId(i);
      if (!touchIds.contains(id)) continue;

      int ptrIdx = me.findPointerIndex(id);
      if (ptrIdx < 0) continue;
      float px = me.getX(ptrIdx);
      float py = me.getY(ptrIdx);

      // Update joysticks
      for (Joystick j : joysticks.values()) {
        if (!j.active || j.touchId != id) continue;
        if (j.slideOffRelease && dist(px, py, j.x, j.y) > j.touchRadius) {
          j.active = false;
          j.touchId = -1;
          if (j.returnToCenter) {
            j.dotX = j.x;
            j.dotY = j.y;
            joystickOutputs.get(j.id)[0] = 0.0;
            joystickOutputs.get(j.id)[1] = 0.0;
          }
          if (joystickListener != null) {
            joystickListener.onJoystickRelease(j.id);
          } else {
            actionJoystickUp(j.id);
          }
        } else {
          float moveDist = dist(px, py, j.lastVibrateX, j.lastVibrateY);
          if (j.vibrateOnMove && moveDist > j.radius * j.vibrateOnMoveDist) {
            vibrate(j.vibrateDuration);
            j.lastVibrateX = px;
            j.lastVibrateY = py;
          }
          updateJoystick(j, px, py);
          if (joystickListener != null) {
            joystickListener.onJoystickMove(j.id, joystickOutputs.get(j.id)[0], joystickOutputs.get(j.id)[1]);
          }
        }
      }

      // Update buttons
      for (Button btn : buttons.values()) {
        boolean withinRadius = dist(px, py, btn.x, btn.y) < btn.touchRadius;
        if (btn.touchId == id) {
          if (btn.slideOffRelease && !withinRadius && !btn.timerType.equals(TIMER_TYPE_LATCHING) && !btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
            btn.active = false;
            btn.touchId = -1;
            btn.activationTime = 0;
            if (buttonListener != null) {
              buttonListener.onButtonRelease(btn.index, btn.name);
            } else {
              actionUp(btn.index, btn.name);
            }
          } else if (btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
            btn.touchId = -1; // Allow new touch to toggle
          }
        } else if (btn.slideOn && !btn.active && withinRadius && !btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
          if (btn.timerType.equals(TIMER_TYPE_BLOCK) && btn.blockedUntil > millis()) continue;
          btn.active = true;
          btn.touchId = id;
          btn.activationTime = millis();
          if (btn.timerType.equals(TIMER_TYPE_BLOCK)) {
            btn.blockedUntil = btn.activationTime + btn.timerDuration;
          }
          if (btn.vibrateDuration > 0) vibrate(btn.vibrateDuration);
          if (buttonListener != null) {
            buttonListener.onButtonPress(btn.index, btn.name);
          } else {
            action(btn.index, btn.name);
          }
        }
      }
    }
  }

  void touchEnded(MotionEvent me) {
    ArrayList<Integer> oldIds = new ArrayList<Integer>();
    touchIds = getMotionEventTouchIds(me);
    int action = me.getActionMasked();

    if (action == MotionEvent.ACTION_UP) {
      oldIds = new ArrayList<Integer>(touchIds);
    } else if (action == MotionEvent.ACTION_POINTER_UP) {
      int idx = me.getActionIndex();
      int id = me.getPointerId(idx);
      oldIds.add(id);
    } else if (action == MotionEvent.ACTION_MOVE) {
      ArrayList<Integer> currentIds = getTouchesTouchIds();
      oldIds = new ArrayList<Integer>(currentIds);
      oldIds.removeAll(touchIds);
    }

    // Release joysticks
    for (Joystick j : joysticks.values()) {
      if (oldIds.contains(j.touchId)) {
        j.active = false;
        j.touchId = -1;
        if (j.returnToCenter) {
          j.dotX = j.x;
          j.dotY = j.y;
          joystickOutputs.get(j.id)[0] = 0.0;
          joystickOutputs.get(j.id)[1] = 0.0;
        }
        if (joystickListener != null) {
          joystickListener.onJoystickRelease(j.id);
        } else {
          actionJoystickUp(j.id);
        }
      }
    }

    // Release buttons
    for (Button btn : buttons.values()) {
      if (oldIds.contains(btn.touchId) && !btn.timerType.equals(TIMER_TYPE_LATCHING) && !btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
        btn.active = false;
        btn.touchId = -1;
        btn.activationTime = 0;
        if (buttonListener != null) {
          buttonListener.onButtonRelease(btn.index, btn.name);
        } else {
          actionUp(btn.index, btn.name);
        }
      } else if (oldIds.contains(btn.touchId)) {
        btn.touchId = -1;
      }
    }

    touchIds.removeAll(oldIds);
  }

  private void updateJoystick(Joystick j, float x, float y) {
    float dx = x - j.x;
    float dy = y - j.y;
    float dist = sqrt(dx * dx + dy * dy);

    // Constrain to circle
    if (dist > j.radius) {
      float angle = atan2(dy, dx);
      dx = cos(angle) * j.radius;
      dy = sin(angle) * j.radius;
      dist = j.radius;
    }

    j.dotX = j.x + dx;
    j.dotY = j.y + dy;

    // Apply deadzone and sensitivity
    float deadzoneRadius = j.radius * j.deadzone;
    if (dist < deadzoneRadius) {
      joystickOutputs.get(j.id)[0] = 0.0;
      joystickOutputs.get(j.id)[1] = 0.0;
    } else {
      float range = j.radius - deadzoneRadius;
      float outputX = (dx - (dx / dist) * deadzoneRadius) / range;
      float outputY = -(dy - (dy / dist) * deadzoneRadius) / range;
      joystickOutputs.get(j.id)[0] = constrain(outputX * j.sensitivity, -1.0, 1.0);
      joystickOutputs.get(j.id)[1] = constrain(outputY * j.sensitivity, -1.0, 1.0);
    }
  }

  private ArrayList<Integer> getMotionEventTouchIds(MotionEvent me) {
    ArrayList<Integer> ids = new ArrayList<Integer>();
    for (int i = 0; i < me.getPointerCount(); i++) {
      ids.add(me.getPointerId(i));
    }
    return ids;
  }

  private ArrayList<Integer> getTouchesTouchIds() {
    ArrayList<Integer> ids = new ArrayList<Integer>();
    for (int i = 0; i < touches.length; i++) {
      ids.add(touches[i].id);
    }
    return ids;
  }
}

// Simple Sprite class for testing
class Sprite {
  float x, y;
  float speed;

  Sprite(float x, float y, float speed) {
    this.x = x;
    this.y = y;
    this.speed = speed;
  }

  void update(float dx, float dy) {
    x += dx * speed;
    y += dy * speed;
    x = constrain(x, 0, width);
    y = constrain(y, 0, height);
  }

  void draw() {
    fill(255, 0, 0);
    ellipse(x, y, 50, 50);
  }
}

1 Like