package yuku.multigesture; import android.graphics.PointF; import android.util.FloatMath; import android.view.MotionEvent; import java.util.ArrayList; import java.util.Iterator; // Android Multi-Touch event demo // David Bouchard // http://www.deadpixel.ca public class MultiGestureDetector { // util methods impl by yuku public static float dist(float x1, float y1, float x2, float y2) { float dx = x1 - x2; float dy = y1 - y2; return FloatMath.sqrt(dx*dx + dy*dy); } // The main detector object final TouchProcessor touch; // listener private OnMultiGestureListener listener; // ------------------------------------------------------------------------------------- public MultiGestureDetector(float screenDensity) { touch = new TouchProcessor(screenDensity); } public OnMultiGestureListener getListener() { return listener; } public void setListener(OnMultiGestureListener listener) { this.listener = listener; } // ------------------------------------------------------------------------------------- public void draw() { // I do the analysis and event processing inside draw, since I found that on Android // trying to draw from outside the main thread can cause pretty serious screen flickering // devices // TODO touch.analyse(); touch.sendEvents(); } // ------------------------------------------------------------------------------------- // MULTI TOUCH EVENTS! void onTap(TapEvent event) { if (event.isSingleTap()) { if (listener != null) listener.onTap(event); } if (event.isDoubleTap()) { if (listener != null) listener.onDoubleTap(event); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void onFlick(FlickEvent event) { if (listener != null) listener.onFlick(event); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void onDrag(DragEvent event) { if (listener != null) listener.onDrag(event); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void onRotate(RotateEvent event) { if (listener != null) listener.onRotate(event); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void onPinch(PinchEvent event) { if (listener != null) listener.onPinch(event); } // ------------------------------------------------------------------------------------- // This is the stock Android touch event // modified by yuku public boolean onTouchEvent(MotionEvent event) { // extract the action code & the pointer ID int action = event.getAction(); int code = action & MotionEvent.ACTION_MASK; int index = action >> MotionEvent.ACTION_POINTER_ID_SHIFT; float x = event.getX(index); float y = event.getY(index); int id = event.getPointerId(index); // pass the events to the TouchProcessor if (code == MotionEvent.ACTION_DOWN || code == MotionEvent.ACTION_POINTER_DOWN) { touch.pointDown(x, y, id); } else if (code == MotionEvent.ACTION_UP || code == MotionEvent.ACTION_POINTER_UP) { touch.pointUp(event.getPointerId(index)); } else if (code == MotionEvent.ACTION_MOVE) { int numPointers = event.getPointerCount(); for (int i = 0; i < numPointers; i++) { id = event.getPointerId(i); x = event.getX(i); y = event.getY(i); touch.pointMoved(x, y, id); } } // immediately analyze and sendEvents touch.analyse(); touch.sendEvents(); return true; } // Event classes // ///////////////////////////////////////////////////////////////////////////////// public class TouchEvent { // empty base class to make event handling easier } // ///////////////////////////////////////////////////////////////////////////////// public class DragEvent extends TouchEvent { public float x; // position public float y; public float dx; // movement public float dy; public int numberOfPoints; DragEvent(float x, float y, float dx, float dy, int n) { this.x = x; this.y = y; this.dx = dx; this.dy = dy; numberOfPoints = n; } } // ///////////////////////////////////////////////////////////////////////////////// public class PinchEvent extends TouchEvent { public float centerX; public float centerY; public float amount; // in pixels public float scale; public int numberOfPoints; PinchEvent(float centerX, float centerY, float amount, float scale, int n) { this.centerX = centerX; this.centerY = centerY; this.amount = amount; this.scale = scale; this.numberOfPoints = n; } } // ///////////////////////////////////////////////////////////////////////////////// public class RotateEvent extends TouchEvent { public float centerX; public float centerY; public float angle; // delta, in radians public int numberOfPoints; RotateEvent(float centerX, float centerY, float angle, int n) { this.centerX = centerX; this.centerY = centerY; this.angle = angle; } } // ///////////////////////////////////////////////////////////////////////////////// public class TapEvent extends TouchEvent { public static final int SINGLE = 0; public static final int DOUBLE = 1; public float x; public float y; public int type; TapEvent(float x, float y, int type) { this.x = x; this.y = y; this.type = type; } public boolean isSingleTap() { return (type == SINGLE) ? true : false; } public boolean isDoubleTap() { return (type == DOUBLE) ? true : false; } } // ///////////////////////////////////////////////////////////////////////////////// public class FlickEvent extends TouchEvent { public float x; public float y; public PointF velocity; FlickEvent(float x, float y, PointF velocity) { this.x = x; this.y = y; this.velocity = velocity; } } class TouchPoint { public float x; public float y; public float px; public float py; public int id; // used for gesture detection float angle; float oldAngle; float pinch; float oldPinch; // ------------------------------------------------------------------------------------- TouchPoint(float x, float y, int id) { this.x = x; this.y = y; this.px = x; this.py = y; this.id = id; } // ------------------------------------------------------------------------------------- public void update(float x, float y) { px = this.x; py = this.y; this.x = x; this.y = y; } // ------------------------------------------------------------------------------------- public void initGestureData(float cx, float cy) { pinch = oldPinch = dist(x, y, cx, cy); angle = oldAngle = (float) Math.atan2((y - cy), (x - cx)); } // ------------------------------------------------------------------------------------- // delta x -- int to get rid of some noise public int dx() { return (int) (x - px); } // ------------------------------------------------------------------------------------- // delta y -- int to get rid of some noise public int dy() { return (int) (y - py); } // ------------------------------------------------------------------------------------- public void setAngle(float angle) { oldAngle = this.angle; this.angle = angle; } // ------------------------------------------------------------------------------------- public void setPinch(float pinch) { oldPinch = this.pinch; this.pinch = pinch; } } // TODO: make distance thershold based on pixel density information! class TouchProcessor { // heuristic constants int DOUBLE_TAP_DIST_THRESHOLD = 30; int FLICK_VELOCITY_THRESHOLD = 20; float MAX_MULTI_DRAG_DISTANCE = 100; // from the centroid // A list of currently active touch points ArrayList<TouchPoint> touchPoints; // Used for tap/doubletaps TouchPoint firstTap; TouchPoint secondTap; long tap; int tapCount = 0; // Events to be broadcast to the sketch ArrayList<TouchEvent> events; // centroid information float cx, cy; float old_cx, old_cy; boolean pointsChanged = false; // ------------------------------------------------------------------------------------- public TouchProcessor(float screenDensity) { touchPoints = new ArrayList<TouchPoint>(); events = new ArrayList<TouchEvent>(); DOUBLE_TAP_DIST_THRESHOLD *= screenDensity; FLICK_VELOCITY_THRESHOLD *= screenDensity; MAX_MULTI_DRAG_DISTANCE *= screenDensity; } // ------------------------------------------------------------------------------------- // Point Update functions public synchronized void pointDown(float x, float y, int id) { TouchPoint p = new TouchPoint(x, y, id); touchPoints.add(p); updateCentroid(); if (touchPoints.size() >= 2) { p.initGestureData(cx, cy); if (touchPoints.size() == 2) { // if this is the second point, we now have a valid centroid to update the first point TouchPoint frst = (TouchPoint) touchPoints.get(0); frst.initGestureData(cx, cy); } } // tap detection if (tapCount == 0) { firstTap = p; } if (tapCount == 1) { secondTap = p; } tap = System.currentTimeMillis(); pointsChanged = true; } // ------------------------------------------------------------------------------------- public synchronized void pointUp(int id) { TouchPoint p = getPoint(id); touchPoints.remove(p); // tap detection // TODO: handle a long press event here? if (p == firstTap || p == secondTap) { // this could be either a Tap or a Flick gesture, based on movement float d = dist(p.x, p.y, p.px, p.py); if (d > FLICK_VELOCITY_THRESHOLD) { FlickEvent event = new FlickEvent(p.px, p.py, new PointF(p.x - p.px, p.y - p.py)); events.add(event); } else { // long interval = System.currentTimeMillis() - tap; tapCount++; } } pointsChanged = true; } // ------------------------------------------------------------------------------------- public synchronized void pointMoved(float x, float y, int id) { TouchPoint p = getPoint(id); p.update(x, y); // since the events will be in sync with draw(), we just wait until analyse() to // look for gestures pointsChanged = true; } // ------------------------------------------------------------------------------------- // Calculate the centroid of all active points public void updateCentroid() { old_cx = cx; old_cy = cy; cx = 0; cy = 0; for (int i = 0; i < touchPoints.size(); i++) { TouchPoint p = (TouchPoint) touchPoints.get(i); cx += p.x; cy += p.y; } cx /= touchPoints.size(); cy /= touchPoints.size(); } // ------------------------------------------------------------------------------------- public synchronized void analyse() { handleTaps(); // simple event priority rule: do not try to rotate or pinch while dragging // this gets rid of a lot of jittery events if (pointsChanged) { updateCentroid(); if (handleDrag()) { // we have handled drag, reset tap/doubletap firstTap = secondTap = null; tapCount = 0; } else { boolean ret1 = handleRotation(); boolean ret2 = handlePinch(); if (ret1 || ret2) { // we have handled rotation or pinch, reset tap/doubletap firstTap = secondTap = null; tapCount = 0; } } pointsChanged = false; } } // ------------------------------------------------------------------------------------- // send events to the sketch public void sendEvents() { for (int i = 0; i < events.size(); i++) { TouchEvent e = (TouchEvent) events.get(i); if (e instanceof TapEvent) onTap((TapEvent) e); else if (e instanceof FlickEvent) onFlick((FlickEvent) e); else if (e instanceof DragEvent) onDrag((DragEvent) e); else if (e instanceof PinchEvent) onPinch((PinchEvent) e); else if (e instanceof RotateEvent) onRotate((RotateEvent) e); } events.clear(); } // ------------------------------------------------------------------------------------- public void handleTaps() { if (tapCount == 2) { // check if the tap point has moved float d = dist(firstTap.x, firstTap.y, secondTap.x, secondTap.y); if (d > DOUBLE_TAP_DIST_THRESHOLD) { // if the two taps are apart, count them as two single taps // TapEvent event1 = new TapEvent(firstTap.x, firstTap.y, TapEvent.SINGLE); // onTap(event1); TapEvent event2 = new TapEvent(secondTap.x, secondTap.y, TapEvent.SINGLE); onTap(event2); } else { events.add(new TapEvent(firstTap.x, firstTap.y, TapEvent.DOUBLE)); } tapCount = 0; } else if (tapCount == 1) { // long interval = System.currentTimeMillis() - tap; // if (interval > TAP_TIMEOUT) { events.add(new TapEvent(firstTap.x, firstTap.y, TapEvent.SINGLE)); // tapCount = 0; // } } } // ------------------------------------------------------------------------------------- // rotation is the average angle change between each point and the centroid public boolean handleRotation() { if (touchPoints.size() < 2) return false; // look for rotation events float rotation = 0; for (int i = 0; i < touchPoints.size(); i++) { TouchPoint p = (TouchPoint) touchPoints.get(i); float angle = (float) Math.atan2(p.y - cy, p.x - cx); p.setAngle(angle); float delta = p.angle - p.oldAngle; if (delta > Math.PI) delta -= (float) Math.PI * 2.f; if (delta < -Math.PI) delta += (float) Math.PI * 2.f; rotation += delta; } rotation /= touchPoints.size(); if (rotation != 0) { events.add(new RotateEvent(cx, cy, rotation, touchPoints.size())); return true; } return false; } // ------------------------------------------------------------------------------------- // pinch is simply the average distance change from each points to the centroid public boolean handlePinch() { int ntouches = touchPoints.size(); if (ntouches < 2) return false; // look for pinch events float pinch = 0; float scalediff = 0.f; for (int i = 0; i < ntouches; i++) { TouchPoint p = touchPoints.get(i); float distance = dist(p.x, p.y, cx, cy); p.setPinch(distance); float delta = p.pinch - p.oldPinch; pinch += delta; scalediff += delta / distance; } pinch /= ntouches; scalediff /= ntouches; if (pinch != 0) { events.add(new PinchEvent(cx, cy, pinch, 1.f + scalediff, ntouches)); return true; } return false; } // ------------------------------------------------------------------------------------- public boolean handleDrag() { // look for multi-finger drag events // multi-drag is defined as all the fingers moving close-ish together in the same direction boolean x_drag = true; boolean y_drag = true; boolean clustered = false; int first_x_dir = 0; int first_y_dir = 0; for (int i = 0; i < touchPoints.size(); i++) { TouchPoint p = (TouchPoint) touchPoints.get(i); int x_dir = 0; int y_dir = 0; if (p.dx() > 0) x_dir = 1; if (p.dx() < 0) x_dir = -1; if (p.dy() > 0) y_dir = 1; if (p.dy() < 0) y_dir = -1; if (i == 0) { first_x_dir = x_dir; first_y_dir = y_dir; } else { if (first_x_dir != x_dir) x_drag = false; if (first_y_dir != y_dir) y_drag = false; } // if the point is stationary if (x_dir == 0) x_drag = false; if (y_dir == 0) y_drag = false; if (touchPoints.size() == 1) clustered = true; else { float distance = dist(p.x, p.y, cx, cy); if (distance < MAX_MULTI_DRAG_DISTANCE) { clustered = true; } } } if ((x_drag || y_drag) && clustered) { if (touchPoints.size() == 1) { TouchPoint p = (TouchPoint) touchPoints.get(0); // use the centroid to calculate the position and delta of this drag event events.add(new DragEvent(p.x, p.y, p.dx(), p.dy(), 1)); } else { // use the centroid to calculate the position and delta of this drag event events.add(new DragEvent(cx, cy, cx - old_cx, cy - old_cy, touchPoints.size())); } return true; } return false; } // ------------------------------------------------------------------------------------- @SuppressWarnings("unchecked") public synchronized ArrayList<TouchPoint> getPoints() { return (ArrayList<TouchPoint>) touchPoints.clone(); } // ------------------------------------------------------------------------------------- public synchronized TouchPoint getPoint(int pid) { Iterator<TouchPoint> i = touchPoints.iterator(); while (i.hasNext()) { TouchPoint tp = (TouchPoint) i.next(); if (tp.id == pid) return tp; } return null; } } }