Skip to content

Instantly share code, notes, and snippets.

@RandomEtc
Created March 7, 2010 05:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RandomEtc/324166 to your computer and use it in GitHub Desktop.
Save RandomEtc/324166 to your computer and use it in GitHub Desktop.
multi-touch for Android Processing
// world offset and scale
float tx = 0, ty = 0, sc = 1, rot = 0;
MultiTouchController multitouchController;
void setup() {
size(480, 800);
multitouchController = new MultiTouchController(new TouchHelper(), this.getResources());
}
public void checkMotionEvent(MotionEvent event) {
super.checkMotionEvent(event);
multitouchController.onTouchEvent(event);
}
void draw() {
background(200);
pushMatrix();
translate(tx, ty);
scale(sc);
rotate(rot);
// draw some stuff
noFill();
stroke(0);
strokeWeight(2.0/sc);
for (int i = 512; i > 2; i /= 2) {
rect((width-i)/2,(height-i)/2,i,i);
}
popMatrix();
}
// from http://lukehutch.wordpress.com/2010/01/06/my-multi-touch-code-ported-to-eclair/
/**
* MultiTouchController.java
*
* (c) Luke Hutchison (luke.hutch@mit.edu)
*
* Modified for official level 5 API by Cyanogen (shade@chemlab.org)
*
* Modified for Android Processing-0178 by Tom Carden (tom@stamen.com)
* - removed use of generic T in favour of Object
* - moved static inner classes outside of MultiTouchController
*
* Released under the Apache License v2.
*/
import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
/**
* A class that simplifies the implementation of multitouch in applications. Subclass this and read the fields here as needed in
* subclasses.
*
* @author Luke Hutchison
*/
public class MultiTouchController {
/**
* * Time in ms required after a change in event status (e.g. putting down or lifting off the second finger) before events
* * actually do anything -- helps eliminate noisy jumps that happen on change of status
*/
private static final long EVENT_SETTLE_TIME_INTERVAL = 100;
// The biggest possible abs val of the change in x or y between multitouch events
// (larger dx/dy events are ignored) -- helps eliminate jumping on finger 2 up/down
private static final float MAX_MULTITOUCH_POS_JUMP_SIZE = 30.0f;
// The biggest possible abs val of the change in multitouchWidth or multitouchHeight between
// multitouch events (larger-jump events are ignored) -- helps eliminate jumping on finger 2 up/down
private static final float MAX_MULTITOUCH_DIM_JUMP_SIZE = 40.0f;
// The smallest possible distance between multitouch points (used to avoid div-by-zero errors and display glitches)
private static final float MIN_MULTITOUCH_SEPARATION = 30.0f;
// --
MultiTouchObjectCanvas objectCanvas;
private PointInfo currPt, prevPt;
// --
private Object draggedObject = null;
private long dragStartTime, dragSettleTime;
// Conversion from object coords to screen coords, and from drag width to object scale
private float objDraggedPointX, objDraggedPointY, objStartScale;
private PositionAndScale objPosAndScale = new PositionAndScale();
// --
/** Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses */
private boolean handleSingleTouchEvents;
// --
private static final int MODE_NOTHING = 0;
private static final int MODE_DRAG = 1;
private static final int MODE_STRETCH = 2;
private int dragMode = MODE_NOTHING;
// ------------------------------------------------------------------------------------
/** Constructor that sets handleSingleTouchEvents to true */
public MultiTouchController(MultiTouchObjectCanvas objectCanvas, Resources res) {
this(objectCanvas, res, true);
}
/** Full constructor */
public MultiTouchController(MultiTouchObjectCanvas objectCanvas, Resources res, boolean handleSingleTouchEvents) {
this.currPt = new PointInfo(res);
this.prevPt = new PointInfo(res);
this.handleSingleTouchEvents = handleSingleTouchEvents;
this.objectCanvas = objectCanvas;
}
// ------------------------------------------------------------------------------------
/**
* * Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses.
* * Default: true
*/
protected void setHandleSingleTouchEvents(boolean handleSingleTouchEvents) {
this.handleSingleTouchEvents = handleSingleTouchEvents;
}
/**
* * Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses.
* * Default: true
*/
protected boolean getHandleSingleTouchEvents() {
return handleSingleTouchEvents;
}
// ------------------------------------------------------------------------------------
/** Process incoming touch events */
public boolean onTouchEvent(MotionEvent event) {
if (dragMode == MODE_NOTHING && !handleSingleTouchEvents && event.getPointerCount() == 1)
// Not handling initial single touch events, just pass them on
return false;
// Handle history first, if any (we sometimes get history with ACTION_MOVE events)
int histLen = event.getHistorySize() / event.getPointerCount();
// Don't try to fetch second touchpoint if only one exists
int secondPointerIndex = event.findPointerIndex(1);
for (int i = 0; i < histLen; i++) {
if (secondPointerIndex >= 0)
decodeTouchEvent(event.getHistoricalX(i), event.getHistoricalY(i), event.getPressure(i), event.getPointerCount(),
event.getHistoricalX(secondPointerIndex, i), event.getHistoricalY(secondPointerIndex, i), event
.getHistoricalPressure(secondPointerIndex, i), MotionEvent.ACTION_MOVE, true, event
.getHistoricalEventTime(i));
else
// Can't read from invalid pointer index
decodeTouchEvent(event.getHistoricalX(i), event.getHistoricalY(i), event.getPressure(i), event.getPointerCount(),
0, 0, 0.0f, MotionEvent.ACTION_MOVE, true, event.getHistoricalEventTime(i));
}
// Handle actual event at end of history
if (secondPointerIndex >= 0)
decodeTouchEvent(event.getX(), event.getY(), event.getPressure(), event.getPointerCount(), event
.getX(secondPointerIndex), event.getY(secondPointerIndex), event.getPressure(secondPointerIndex), event
.getAction(), event.getAction() != MotionEvent.ACTION_UP && event.getAction() != MotionEvent.ACTION_CANCEL,
event.getEventTime());
else
// Can't read from invalid pointer index
decodeTouchEvent(event.getX(), event.getY(), event.getPressure(), event.getPointerCount(), 0, 0, 0.0f, event
.getAction(), event.getAction() != MotionEvent.ACTION_UP && event.getAction() != MotionEvent.ACTION_CANCEL,
event.getEventTime());
return true;
}
private void decodeTouchEvent(float x, float y, float pressure, int pointerCount, float x2, float y2, float pressure2,
int action, boolean down, long eventTime) {
prevPt.set(currPt);
currPt.set(x, y, pressure, pointerCount, x2, y2, pressure2, action, down, eventTime);
multiTouchController();
}
// ------------------------------------------------------------------------------------
/** Start dragging/stretching, or reset drag/stretch to current point if something goes out of range */
private void resetDrag() {
if (draggedObject == null)
return;
// Get dragged object position and scale
objectCanvas.getPositionAndScale(draggedObject, objPosAndScale);
// Figure out the object coords of the drag start point's screen coords.
// All stretching should be around this point in object-coord-space.
float scaleInv = (objPosAndScale.scale == 0.0f ? 1.0f : 1.0f / objPosAndScale.scale);
objDraggedPointX = (currPt.getX() - objPosAndScale.xOff) * scaleInv;
objDraggedPointY = (currPt.getY() - objPosAndScale.yOff) * scaleInv;
// Figure out ratio between object scale factor and multitouch diameter (they are linearly correlated)
float diam = currPt.getMultiTouchDiameter();
objStartScale = objPosAndScale.scale / (diam == 0.0f ? 1.0f : diam);
}
/** Drag/stretch the dragged object to the current touch position and diameter */
private void performDrag() {
// Don't do anything if we're not dragging anything
if (draggedObject == null)
return;
// Calc new position of dragged object
float scale = (objPosAndScale.scale == 0.0f ? 1.0f : objPosAndScale.scale);
float newObjPosX = currPt.getX() - objDraggedPointX * scale;
float newObjPosY = currPt.getY() - objDraggedPointY * scale;
// Get new drag diameter (avoiding divsion by zero), and calculate new drag scale
float diam;
if (!currPt.isMultiTouch) {
// Single-touch, no change in scale
diam = 1.0f;
}
else {
diam = currPt.getMultiTouchDiameter();
if (diam < MIN_MULTITOUCH_SEPARATION)
diam = MIN_MULTITOUCH_SEPARATION;
}
float newScale = diam * objStartScale;
// Get the new obj coords and scale, and set them (notifying the subclass of the change)
objPosAndScale.set(newObjPosX, newObjPosY, newScale);
boolean success = objectCanvas.setPositionAndScale(draggedObject, objPosAndScale, currPt);
if (!success)
; // If we could't set those params, do nothing currently
}
/** The main single-touch and multi-touch logic */
private void multiTouchController() {
switch (dragMode) {
case MODE_NOTHING:
// Not doing anything currently
if (currPt.isDown()) {
// Start a new single-point drag
draggedObject = objectCanvas.getDraggableObjectAtPoint(currPt);
if (draggedObject != null) {
// Started a new single-point drag
dragMode = MODE_DRAG;
objectCanvas.selectObject(draggedObject, currPt);
resetDrag();
// Don't need any settling time if just placing one finger, there is no noise
dragStartTime = dragSettleTime = currPt.getEventTime();
}
}
break;
case MODE_DRAG:
// Currently in a single-point drag
if (!currPt.isDown()) {
// First finger was released, stop dragging
dragMode = MODE_NOTHING;
objectCanvas.selectObject((draggedObject = null), currPt);
}
else if (currPt.isMultiTouch()) {
// Point 1 was already down and point 2 was just placed down
dragMode = MODE_STRETCH;
// Restart the drag with the new drag position (that is at the midpoint between the touchpoints)
resetDrag();
// Need to let events settle before moving things, to help with event noise on touchdown
dragStartTime = currPt.getEventTime();
dragSettleTime = dragStartTime + EVENT_SETTLE_TIME_INTERVAL;
}
else {
// Point 1 is still down and point 2 did not change state, just do single-point drag to new location
if (currPt.getEventTime() < dragSettleTime) {
// Ignore the first few events if we just stopped stretching, because if finger 2 was kept down while
// finger 1 is lifted, then point 1 gets mapped to finger 2. Restart the drag from the new position.
resetDrag();
}
else {
// Keep dragging, move to new point
performDrag();
}
}
break;
case MODE_STRETCH:
// Two-point stretch
if (!currPt.isMultiTouch() || !currPt.isDown()) {
// Dropped one or both points, stop stretching
if (!currPt.isDown()) {
// Dropped both points, go back to doing nothing
dragMode = MODE_NOTHING;
objectCanvas.selectObject((draggedObject = null), currPt);
}
else {
// Just dropped point 2, downgrade to a single-point drag
dragMode = MODE_DRAG;
// Restart the drag with the single-finger position
resetDrag();
// Ignore the first few events after the drop, in case we dropped finger 1 and left finger 2 down
dragStartTime = currPt.getEventTime();
dragSettleTime = dragStartTime + EVENT_SETTLE_TIME_INTERVAL;
}
}
else {
// Keep stretching
if (Math.abs(currPt.getX() - prevPt.getX()) > MAX_MULTITOUCH_POS_JUMP_SIZE
|| Math.abs(currPt.getY() - prevPt.getY()) > MAX_MULTITOUCH_POS_JUMP_SIZE
|| Math.abs(currPt.getMultiTouchWidth() - prevPt.getMultiTouchWidth()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE
|| Math.abs(currPt.getMultiTouchHeight() - prevPt.getMultiTouchHeight()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE) {
// Jumped too far, probably event noise, reset and ignore events for a bit
resetDrag();
dragStartTime = currPt.getEventTime();
dragSettleTime = dragStartTime + EVENT_SETTLE_TIME_INTERVAL;
}
else if (currPt.eventTime < dragSettleTime) {
// Events have not yet settled, reset
resetDrag();
}
else {
// Stretch to new position and size
performDrag();
}
}
break;
}
}
// ------------------------------------------------------------------------------------
}
public interface MultiTouchObjectCanvas {
/** See if there is a draggable object at the current point. Returns the object at the point, or null if nothing to drag. */
public Object getDraggableObjectAtPoint(PointInfo pt);
/**
** Get the screen coords of the dragged object's origin, and scale multiplier to convert screen coords to obj coords. Call
** the .set() method on the passed PositionAndScale object.
*/
public void getPositionAndScale(Object obj, PositionAndScale objPosAndScaleOut);
/**
** Set the position and scale of the dragged object, in object coords. Return true for success, or false if those
** parameters are out of range.
*/
public boolean setPositionAndScale(Object obj, PositionAndScale newObjPosAndScale, PointInfo touchPoint);
/**
** Select an object at the given point. Can be used to bring the object to top etc. Only called when first touchpoint goes
** down, not when multitouch is initiated. Also called with null when drag op stops.
*/
public void selectObject(Object obj, PointInfo pt);
}
/** A class that packages up all MotionEvent information with all derived multitouch information (if available) */
public class PointInfo {
private float x, y, dx, dy, size, diameter, diameterSq, angle, pressure, pressure2;
private boolean down, isMultiTouch, diameterSqIsCalculated, diameterIsCalculated, angleIsCalculated;
private int action;
private long eventTime;
// --
private int displayWidth, displayHeight;
// --
public PointInfo(Resources res) {
DisplayMetrics metrics = res.getDisplayMetrics();
this.displayWidth = metrics.widthPixels;
this.displayHeight = metrics.heightPixels;
}
// --
public PointInfo(PointInfo other) {
this.set(other);
}
/** Copy all fields */
public void set(PointInfo other) {
this.displayWidth = other.displayWidth;
this.displayHeight = other.displayHeight;
this.x = other.x;
this.y = other.y;
this.dx = other.dx;
this.dy = other.dy;
this.size = other.size;
this.diameter = other.diameter;
this.diameterSq = other.diameterSq;
this.angle = other.angle;
this.pressure = other.pressure;
this.pressure2 = other.pressure2;
this.down = other.down;
this.action = other.action;
this.isMultiTouch = other.isMultiTouch;
this.diameterIsCalculated = other.diameterIsCalculated;
this.diameterSqIsCalculated = other.diameterSqIsCalculated;
this.angleIsCalculated = other.angleIsCalculated;
this.eventTime = other.eventTime;
}
private void set(float x, float y, float pressure, int pointerCount, float x2, float y2, float pressure2, int action,
boolean down, long eventTime) {
// Log.i("Multitouch", "x: " + x + " y: " + y + " pointerCount: " + pointerCount +
// " x2: " + x2 + " y2: " + y2 + " action: " + action + " down: " + down);
this.eventTime = eventTime;
this.action = action;
this.x = x;
this.y = y;
this.pressure = pressure;
this.pressure2 = pressure2;
this.down = down;
this.isMultiTouch = pointerCount == 2;
if (isMultiTouch) {
float xMid = (x2 + x) * .5f;
float yMid = (y2 + y) * .5f;
dx = Math.abs(x2 - x);
dy = Math.abs(y2 - y);
this.x = xMid;
this.y = yMid;
}
else {
// Single-touch event
dx = dy = 0.0f;
}
// Need to re-calculate the expensive params if they're needed
diameterSqIsCalculated = diameterIsCalculated = angleIsCalculated = false;
}
// Fast integer sqrt, by Jim Ulery. Should be faster than Math.sqrt()
private int julery_isqrt(int val) {
int temp, g = 0, b = 0x8000, bshft = 15;
do {
if (val >= (temp = (((g << 1) + b) << bshft--))) {
g += b;
val -= temp;
}
}
while ((b >>= 1) > 0);
return g;
}
/** Calculate the squared diameter of the multitouch event, and cache it. Use this if you don't need to perform the sqrt. */
public float getMultiTouchDiameterSq() {
if (!diameterSqIsCalculated) {
diameterSq = (isMultiTouch ? dx * dx + dy * dy : 0.0f);
diameterSqIsCalculated = true;
}
return diameterSq;
}
/** Calculate the diameter of the multitouch event, and cache it. Uses fast int sqrt but gives accuracy to 1/16px. */
public float getMultiTouchDiameter() {
if (!diameterIsCalculated) {
// Get 1/16 pixel's worth of subpixel accuracy, works on screens up to 2048x2048
// before we get overflow (at which point you can reduce or eliminate subpix
// accuracy, or use longs in julery_isqrt())
float diamSq = getMultiTouchDiameterSq();
diameter = (diamSq == 0.0f ? 0.0f : (float) julery_isqrt((int) (256 * diamSq)) / 16.0f);
// Make sure diameter is never less than dx or dy, for trig purposes
if (diameter < dx)
diameter = dx;
if (diameter < dy)
diameter = dy;
diameterIsCalculated = true;
}
return diameter;
}
/**
* * Calculate the angle of a multitouch event, and cache it. Actually gives the smaller of the two angles between the x
* * axis and the line between the two touchpoints, so range is [0,Math.PI/2]. Uses Math.atan2().
*/
public float getMultiTouchAngle() {
if (!angleIsCalculated) {
angle = (float) Math.atan2(dy, dx);
angleIsCalculated = true;
}
return angle;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
public float getMultiTouchWidth() {
return dx;
}
public float getMultiTouchHeight() {
return dy;
}
public float getPressure() {
return pressure;
}
public float getPressure2() {
return pressure2;
}
public boolean isDown() {
return down;
}
public int getAction() {
return action;
}
public boolean isMultiTouch() {
return isMultiTouch;
}
public long getEventTime() {
return eventTime;
}
}
/**
** A class that is used to store scroll offsets and scale information for objects that are managed by the multitouch
** controller
*/
public class PositionAndScale {
private float xOff, yOff, scale;
public PositionAndScale() {
}
public void set(float xOff, float yOff, float scale) {
this.xOff = xOff;
this.yOff = yOff;
this.scale = scale;
}
public float getXOff() {
return xOff;
}
public float getYOff() {
return yOff;
}
public float getScale() {
return scale;
}
}
class TouchHelper implements MultiTouchObjectCanvas {
Object getDraggableObjectAtPoint(PointInfo pt) {
return this;
}
void getPositionAndScale(Object obj, PositionAndScale objPosAndScaleOut) {
objPosAndScaleOut.set(tx,ty,sc);
}
boolean setPositionAndScale(Object obj, PositionAndScale newObjPosAndScale, PointInfo touchPoint) {
tx = newObjPosAndScale.getXOff();
ty = newObjPosAndScale.getYOff();
sc = newObjPosAndScale.getScale();
return true;
}
void selectObject(Object obj, PointInfo pt) {
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment