package org.metalev.multitouch.controller; /** * MultiTouchController.java * * Author: Luke Hutchison (luke.hutch@mit.edu) * Please drop me an email if you use this code so I can list your project here! * * Usage: * <code> * public class MyMTView extends View implements * MultiTouchObjectCanvas<PinchWidgetType> { * * private MultiTouchController<PinchWidgetType> * multiTouchController = new MultiTouchController<PinchWidgetType>(this); * * // Pass touch events to the MT controller * public boolean onTouchEvent(MotionEvent event) { * return multiTouchController.onTouchEvent(event); * } * * // ... then implement the MultiTouchObjectCanvas interface here, * // see details in the comments of that interface. * } * </code> * * Changelog: * 2010-06-09 v1.5.1 Some API changes to make it possible to * selectively update or not update scale / rotation. * Fixed anisotropic zoom. Cleaned up rotation code. * Added more comments. Better var names. (LH) * 2010-06-09 v1.4 Added ability to track pinch rotation (Mickael * Despesse, author of "Face Frenzy") and anisotropic pinch-zoom (LH) * 2010-06-09 v1.3.3 Bugfixes for Android-2.1; added optional debug info (LH) * 2010-06-09 v1.3 Ported to Android-2.2 (handle ACTION_POINTER_* actions); * fixed several bugs; refactoring; documentation (LH) * 2010-05-17 v1.2.1 Dual-licensed under Apache and GPL licenses * 2010-02-18 v1.2 Support for compilation under Android 1.5/1.6 using * introspection (mmin, author of handyCalc) * 2010-01-08 v1.1.1 Bugfixes to Cyanogen's patch that only showed up in more * complex uses of controller (LH) * 2010-01-06 v1.1 Modified for official level 5 MT API (Cyanogen) * 2009-01-25 v1.0 Original MT controller, released for hacked G1 kernel (LH) * * Planned features: * - Add inertia (flick-pinch-zoom or flick-scroll) * * Known usages: * - Mickael Despesse's "Face Frenzy" face distortion app, * to be published to the Market soon * - Yuan Chin's fork of ADW Launcher to support multitouch * - David Byrne's fractal viewing app Fractoid * - mmin's handyCalc calculator * - My own "MultiTouch Visualizer 2" in the Market * - Formerly: The browser in cyanogenmod (and before that, JesusFreke), * and other firmwares like dwang5. This usage has been * replaced with official pinch/zoom in Maps, Browser and Gallery[3D] as of API level 5. * * License: * Dual-licensed under the Apache License v2 and the GPL v2. */ import java.lang.reflect.Method; import android.util.Log; 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<T> { /** * 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 = 20; /** * The biggest possible abs val of the change in x or y between multitouch * events (larger dx/dy events are ignored) -- helps eliminate jumps in * pointer position 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 jumps in pointer position 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; /** The distance selectedObject must move before registering a drag taking place */ private static final float THRESHOLD = 3.0f; /** The max number of touch points that can be present on the screen at once */ public static final int MAX_TOUCH_POINTS = 20; /** Generate tons of log entries for debugging */ public static final boolean DEBUG = false; // --------------------------------------------------------------------------- MultiTouchObjectCanvas<T> objectCanvas; /** The current touch point */ private PointInfo mCurrPt; /** The previous touch point */ private PointInfo mPrevPt; /** Fields extracted from mCurrPt */ private float mCurrPtX, mCurrPtY, mCurrPtDiam, mCurrPtWidth, mCurrPtHeight, mCurrPtAng; /** * Extract fields from mCurrPt, respecting the update* fields of mCurrPt. * This just avoids code duplication. I hate that Java doesn't support * higher-order functions, tuples or multiple return values from functions. */ private void extractCurrPtInfo() { // Get new drag/pinch params. Only read multitouch fields that are needed, // to avoid unnecessary computation (diameter and angle are expensive operations). mCurrPtX = mCurrPt.getX(); mCurrPtY = mCurrPt.getY(); mCurrPtDiam = Math.max(MIN_MULTITOUCH_SEPARATION * .71f, !mCurrXform.updateScale ? 0.0f : mCurrPt.getMultiTouchDiameter()); mCurrPtWidth = Math.max(MIN_MULTITOUCH_SEPARATION, !mCurrXform.updateScaleXY ? 0.0f : mCurrPt.getMultiTouchWidth()); mCurrPtHeight = Math.max(MIN_MULTITOUCH_SEPARATION, !mCurrXform.updateScaleXY ? 0.0f : mCurrPt.getMultiTouchHeight()); mCurrPtAng = !mCurrXform.updateAngle ? 0.0f : mCurrPt.getMultiTouchAngle(); } // --------------------------------------------------------------------------- /** * Whether to handle single-touch events/drags before multi-touch is * initiated or not; if not, they are handled by subclasses */ private boolean handleSingleTouchEvents; /** The object being dragged/stretched */ private T selectedObject = null; /** Current position and scale of the dragged object */ private PositionAndScale mCurrXform = new PositionAndScale(); /** Drag/pinch start time and time to ignore spurious events until * (to smooth over event noise) */ private long mSettleStartTime, mSettleEndTime; /** Conversion from object coords to screen coords */ private float startPosX, startPosY; /** Conversion between scale and width, and object angle and start pinch angle */ private float startScaleOverPinchDiam, startAngleMinusPinchAngle; /** Conversion between X scale and width, and Y scale and height */ private float startScaleXOverPinchWidth, startScaleYOverPinchHeight; /** Whether the current object has moved beyond THRESHOLD */ private boolean mDragOccurred = false; // --------------------------------------------------------------------------- /** No touch points down. */ public static final int MODE_NOTHING = 0; /** One touch point down, dragging an object. */ public static final int MODE_DRAG = 1; /** Two or more touch points down, stretching/rotating an object using the * first two touch points. */ public static final int MODE_PINCH = 2; public static final int MODE_ST_GRAB = 3; /** Current drag mode */ private int mMode = MODE_NOTHING; // --------------------------------------------------------------------------- /** Constructor that sets handleSingleTouchEvents to true */ public MultiTouchController(MultiTouchObjectCanvas<T> objectCanvas) { this(objectCanvas, true); } /** Full constructor */ public MultiTouchController(MultiTouchObjectCanvas<T> objectCanvas, boolean handleSingleTouchEvents) { this.mCurrPt = new PointInfo(); this.mPrevPt = new PointInfo(); 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; } public boolean dragOccurred() { return mDragOccurred; } // --------------------------------------------------------------------------- public static final boolean multiTouchSupported; private static Method m_getPointerCount; private static Method m_getPointerId; private static Method m_getPressure; private static Method m_getHistoricalX; private static Method m_getHistoricalY; private static Method m_getHistoricalPressure; private static Method m_getX; private static Method m_getY; private static int ACTION_POINTER_UP = 6; private static int ACTION_POINTER_INDEX_SHIFT = 8; static { boolean succeeded = false; try { // Android 2.0.1 stuff: m_getPointerCount = MotionEvent.class.getMethod("getPointerCount"); m_getPointerId = MotionEvent.class.getMethod("getPointerId", Integer.TYPE); m_getPressure = MotionEvent.class.getMethod("getPressure", Integer.TYPE); m_getHistoricalX = MotionEvent.class.getMethod("getHistoricalX", Integer.TYPE, Integer.TYPE); m_getHistoricalY = MotionEvent.class.getMethod("getHistoricalY", Integer.TYPE, Integer.TYPE); m_getHistoricalPressure = MotionEvent.class. getMethod("getHistoricalPressure", Integer.TYPE, Integer.TYPE); m_getX = MotionEvent.class.getMethod("getX", Integer.TYPE); m_getY = MotionEvent.class.getMethod("getY", Integer.TYPE); succeeded = true; } catch (Exception e) { Log.e("MultiTouchController", "static initializer failed", e); } multiTouchSupported = succeeded; if (multiTouchSupported) { // Android 2.2+ stuff (the original Android 2.2 consts are declared above, // and these actions aren't used previous to Android 2.2): try { ACTION_POINTER_UP = MotionEvent.class .getField("ACTION_POINTER_UP").getInt(null); ACTION_POINTER_INDEX_SHIFT = MotionEvent.class .getField("ACTION_POINTER_INDEX_SHIFT").getInt(null); } catch (Exception e) { } } } // --------------------------------------------------------------------------- private static final float[] xVals = new float[MAX_TOUCH_POINTS]; private static final float[] yVals = new float[MAX_TOUCH_POINTS]; private static final float[] pressureVals = new float[MAX_TOUCH_POINTS]; private static final int[] pointerIds = new int[MAX_TOUCH_POINTS]; /** Process incoming touch events */ @SuppressWarnings("unused") public boolean onTouchEvent(MotionEvent event) { try { int pointerCount = multiTouchSupported ? (Integer) m_getPointerCount.invoke(event) : 1; if (DEBUG) Log.i("MultiTouch", "Got here 1 - " + multiTouchSupported + " " + mMode + " " + handleSingleTouchEvents + " " + pointerCount); if (mMode == MODE_NOTHING && !handleSingleTouchEvents && pointerCount == 1) // Not handling initial single touch events, just pass them on return false; if (DEBUG) Log.i("MultiTouch", "Got here 2"); // Handle history first (we sometimes get history with ACTION_MOVE events) int action = event.getAction(); int histLen = event.getHistorySize() / pointerCount; for (int histIdx = 0; histIdx <= histLen; histIdx++) { // Read from history entries until histIdx == histLen, // then read from current event boolean processingHist = histIdx < histLen; if (!multiTouchSupported || pointerCount == 1) { // Use single-pointer methods -- these are needed as a special // case (for some weird reason) even if // multitouch is supported but there's only one touch point down // currently -- event.getX(0) etc. throw an exception if there's // only one point down. if (DEBUG) Log.i("MultiTouch", "Got here 3"); xVals[0] = processingHist ? event.getHistoricalX(histIdx) : event.getX(); yVals[0] = processingHist ? event.getHistoricalY(histIdx) : event.getY(); pressureVals[0] = processingHist ? event.getHistoricalPressure(histIdx) : event.getPressure(); } else { // Read x, y and pressure of each pointer if (DEBUG) Log.i("MultiTouch", "Got here 4"); int numPointers = Math.min(pointerCount, MAX_TOUCH_POINTS); if (DEBUG && pointerCount > MAX_TOUCH_POINTS) Log.i("MultiTouch", "Got more pointers than MAX_TOUCH_POINTS"); for (int ptrIdx = 0; ptrIdx < numPointers; ptrIdx++) { int ptrId = (Integer) m_getPointerId.invoke(event, ptrIdx); pointerIds[ptrIdx] = ptrId; // N.B. if pointerCount == 1, then the following methods throw // an array index out of range exception, and the code above // is therefore required not just for Android 1.5/1.6 but // also for when there is only one touch point on the screen -- // pointlessly inconsistent :( xVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalX.invoke(event, ptrIdx, histIdx) : m_getX.invoke(event, ptrIdx)); yVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalY.invoke(event, ptrIdx, histIdx) : m_getY.invoke(event, ptrIdx)); pressureVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalPressure.invoke(event, ptrIdx, histIdx) : m_getPressure.invoke(event, ptrIdx)); } } // Decode event decodeTouchEvent(pointerCount, xVals, yVals, pressureVals, pointerIds, /* action = */processingHist ? MotionEvent.ACTION_MOVE : action, /* down = */processingHist ? true : action != MotionEvent.ACTION_UP && (action & ((1 << ACTION_POINTER_INDEX_SHIFT) - 1)) != ACTION_POINTER_UP && action != MotionEvent.ACTION_CANCEL, // processingHist ? event.getHistoricalEventTime(histIdx) : event.getEventTime()); } return true; } catch (Exception e) { // In case any of the introspection stuff fails (it shouldn't) Log.e("MultiTouchController", "onTouchEvent() failed", e); return false; } } private void decodeTouchEvent(int pointerCount, float[] x, float[] y, float[] pressure, int[] pointerIds, int action, boolean down, long eventTime) { if (DEBUG) Log.i("MultiTouch", "Got here 5 - " + pointerCount + " " + action + " " + down); // Swap curr/prev points PointInfo tmp = mPrevPt; mPrevPt = mCurrPt; mCurrPt = tmp; // Overwrite old prev point mCurrPt.set(pointerCount, x, y, pressure, pointerIds, action, down, eventTime); multiTouchController(); } // --------------------------------------------------------------------------- /** Start dragging/pinching, or reset drag/pinch to current point if something * goes out of range */ private void anchorAtThisPositionAndScale() { if (DEBUG) Log.i("MulitTouch", "anchorAtThisPositionAndScale()"); if (selectedObject == null) return; // Get selected object's current position and scale objectCanvas.getPositionAndScale(selectedObject, mCurrXform); // Figure out the object coords of the drag start point's screen coords. // All stretching should be around this point in object-coord-space. // Also figure out out ratio between object scale factor and multitouch // diameter at beginning of drag; same for angle and optional anisotropic // scale. float currScaleInv = 1.0f / (!mCurrXform.updateScale ? 1.0f : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale); extractCurrPtInfo(); startPosX = (mCurrPtX - mCurrXform.xOff) * currScaleInv; startPosY = (mCurrPtY - mCurrXform.yOff) * currScaleInv; startScaleOverPinchDiam = mCurrXform.scale / mCurrPtDiam; startScaleXOverPinchWidth = mCurrXform.scaleX / mCurrPtWidth; startScaleYOverPinchHeight = mCurrXform.scaleY / mCurrPtHeight; startAngleMinusPinchAngle = mCurrXform.angle - mCurrPtAng; } /** Drag/stretch/rotate the selected object using the current touch * position(s) relative to the anchor position(s). */ private void performDragOrPinch() { // Don't do anything if we're not dragging anything if (selectedObject == null) return; // Calc new position of dragged object float currScale = !mCurrXform.updateScale ? 1.0f : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale; extractCurrPtInfo(); float newPosX = mCurrPtX - startPosX * currScale; float newPosY = mCurrPtY - startPosY * currScale; // Calc new angle of object, if any //float newAngle = mCurrPtAng; //if (mMode == MODE_ST_GRAB) { // // grabbed by single touch area // newAngle = (float) Math.atan2(newPosY-startPosY, // newPosX-startPosX) + mCurrPtAng; //} else { // newAngle = startAngleMinusPinchAngle + mCurrPtAng; //} float deltaX = mCurrPt.getX() - mPrevPt.getX(); float deltaY = mCurrPt.getY() - mPrevPt.getY(); // Calc new scale of object, if any float newScale = mCurrXform.scale; if (mMode == MODE_ST_GRAB) { if (deltaX < 0.0f || deltaY < 0.0f) { newScale = mCurrXform.scale - 0.04f; } else { newScale = mCurrXform.scale + 0.04f; } if (newScale < 0.35f) return; } else { newScale = startScaleOverPinchDiam * mCurrPtDiam; } if (!mDragOccurred) { if (!pastThreshold(Math.abs(deltaX), Math.abs(deltaY), newScale)) { if (DEBUG) { Log.i("MultiTouch", "Change received by performDragOrPinch " + "was below the threshold"); } // Change was too small, don't go any further return; } } float newScaleX = startScaleXOverPinchWidth * mCurrPtWidth; float newScaleY = startScaleYOverPinchHeight * mCurrPtHeight; float newAngle = startAngleMinusPinchAngle + mCurrPtAng; // Set the new obj coords, scale, and angle as appropriate // (notifying the subclass of the change). mCurrXform.set(newPosX, newPosY, newScale, newScaleX, newScaleY, newAngle); boolean success = objectCanvas.setPositionAndScale(selectedObject, mCurrXform, mCurrPt); if (!success) ; // If we could't set those params, do nothing currently mDragOccurred = true; } /** * Returns true if selectedObject has moved passed the movement THRESHOLD, * otherwise false. * This serves to help avoid small jitters in the object when the user * places their finger on the object without intending to move it. */ private boolean pastThreshold(float deltaX, float deltaY, float newScale) { if (deltaX < THRESHOLD && deltaY < THRESHOLD) { if (newScale == mCurrXform.scale) { mDragOccurred = false; return false; } } mDragOccurred = true; return true; } /** * State-based controller for tracking switches between no-touch, * single-touch and multi-touch situations. Includes logic for cleaning up the * event stream, as events around touch up/down are noisy at least on * early Synaptics sensors. */ private void multiTouchController() { if (DEBUG) Log.i("MultiTouch", "Got here 6 - " + mMode + " " + mCurrPt.getNumTouchPoints() + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch()); switch (mMode) { case MODE_NOTHING: if (DEBUG) Log.i("MultiTouch", "MODE_NOTHING"); // Not doing anything currently if (mCurrPt.isDown()) { // Start a new single-point drag selectedObject = objectCanvas.getDraggableObjectAtPoint(mCurrPt); if (selectedObject != null) { if (objectCanvas.pointInObjectGrabArea(mCurrPt, selectedObject)) { // Started a new single-point scale/rotate mMode = MODE_ST_GRAB; objectCanvas.selectObject(selectedObject, mCurrPt); anchorAtThisPositionAndScale(); mSettleStartTime = mSettleEndTime = mCurrPt.getEventTime(); } else { // Started a new single-point drag mMode = MODE_DRAG; objectCanvas.selectObject(selectedObject, mCurrPt); anchorAtThisPositionAndScale(); // Don't need any settling time if just placing one finger, // there is no noise mSettleStartTime = mSettleEndTime = mCurrPt.getEventTime(); } } } break; case MODE_ST_GRAB: if (DEBUG) Log.i("MultiTouch", "MODE_ST_GRAB"); // Currently in a single-point drag if (!mCurrPt.isDown()) { // First finger was released, stop scale/rotate mMode = MODE_NOTHING; objectCanvas.selectObject((selectedObject = null), mCurrPt); mDragOccurred = false; } else { // Point 1 is still down, do scale/rotate performDragOrPinch(); } break; case MODE_DRAG: if (DEBUG) Log.i("MultiTouch", "MODE_DRAG"); // Currently in a single-point drag if (!mCurrPt.isDown()) { // First finger was released, stop dragging mMode = MODE_NOTHING; objectCanvas.selectObject((selectedObject = null), mCurrPt); mDragOccurred = false; } else if (mCurrPt.isMultiTouch()) { // Point 1 was already down and point 2 was just placed down mMode = MODE_PINCH; // Restart the drag with the new drag position (that is at the // midpoint between the touchpoints) anchorAtThisPositionAndScale(); // Need to let events settle before moving things, // to help with event noise on touchdown mSettleStartTime = mCurrPt.getEventTime(); mSettleEndTime = mSettleStartTime + 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 (mCurrPt.getEventTime() < mSettleEndTime) { // 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. anchorAtThisPositionAndScale(); } else { // Keep dragging, move to new point performDragOrPinch(); } } break; case MODE_PINCH: if (DEBUG) Log.i("MultiTouch", "MODE_PINCH"); // Two-point pinch-scale/rotate/translate if (!mCurrPt.isMultiTouch() || !mCurrPt.isDown()) { // Dropped one or both points, stop stretching if (!mCurrPt.isDown()) { // Dropped both points, go back to doing nothing mMode = MODE_NOTHING; objectCanvas.selectObject((selectedObject = null), mCurrPt); } else { // Just dropped point 2, downgrade to a single-point drag mMode = MODE_DRAG; // Restart the pinch with the single-finger position anchorAtThisPositionAndScale(); // Ignore the first few events after the drop, in case we // dropped finger 1 and left finger 2 down mSettleStartTime = mCurrPt.getEventTime(); mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; } } else { // Still pinching if (Math.abs(mCurrPt.getX() - mPrevPt.getX()) > MAX_MULTITOUCH_POS_JUMP_SIZE || Math.abs(mCurrPt.getY() - mPrevPt.getY()) > MAX_MULTITOUCH_POS_JUMP_SIZE || Math.abs(mCurrPt.getMultiTouchWidth() - mPrevPt.getMultiTouchWidth()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE || Math.abs(mCurrPt.getMultiTouchHeight() - mPrevPt.getMultiTouchHeight()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE) { // Jumped too far, probably event noise, reset and ignore events // for a bit anchorAtThisPositionAndScale(); mSettleStartTime = mCurrPt.getEventTime(); mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; } else if (mCurrPt.eventTime < mSettleEndTime) { // Events have not yet settled, reset anchorAtThisPositionAndScale(); } else { // Stretch to new position and size performDragOrPinch(); } } break; } if (DEBUG) Log.i("MultiTouch", "Got here 7 - " + mMode + " " + mCurrPt.getNumTouchPoints() + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch()); } public int getMode() { return mMode; } // --------------------------------------------------------------------------- /** A class that packages up all MotionEvent information with all derived * multitouch information (if available) */ public static class PointInfo { // Multitouch information private int numPoints; private float[] xs = new float[MAX_TOUCH_POINTS]; private float[] ys = new float[MAX_TOUCH_POINTS]; private float[] pressures = new float[MAX_TOUCH_POINTS]; private int[] pointerIds = new int[MAX_TOUCH_POINTS]; // Midpoint of pinch operations private float xMid, yMid, pressureMid; // Width/diameter/angle of pinch operations private float dx, dy, diameter, diameterSq, angle; // Whether or not there is at least one finger down (isDown) and/or at // least two fingers down (isMultiTouch) private boolean isDown, isMultiTouch; // Whether or not these fields have already been calculated, for caching purposes private boolean diameterSqIsCalculated, diameterIsCalculated, angleIsCalculated; // Event action code and event time private int action; private long eventTime; // --------------------------------------------------------------------------- /** Set all point info */ private void set(int numPoints, float[] x, float[] y, float[] pressure, int[] pointerIds, int action, boolean isDown, long eventTime) { if (DEBUG) Log.i("MultiTouch", "Got here 8 - " + +numPoints + " " + x[0] + " " + y[0] + " " + (numPoints > 1 ? x[1] : x[0]) + " " + (numPoints > 1 ? y[1] : y[0]) + " " + action + " " + isDown); this.eventTime = eventTime; this.action = action; this.numPoints = numPoints; for (int i = 0; i < numPoints; i++) { this.xs[i] = x[i]; this.ys[i] = y[i]; this.pressures[i] = pressure[i]; this.pointerIds[i] = pointerIds[i]; } this.isDown = isDown; this.isMultiTouch = numPoints >= 2; if (isMultiTouch) { xMid = (x[0] + x[1]) * .5f; yMid = (y[0] + y[1]) * .5f; pressureMid = (pressure[0] + pressure[1]) * .5f; dx = Math.abs(x[1] - x[0]); dy = Math.abs(y[1] - y[0]); } else { // Single-touch event xMid = x[0]; yMid = y[0]; pressureMid = pressure[0]; dx = dy = 0.0f; } // Need to re-calculate the expensive params if they're needed diameterSqIsCalculated = diameterIsCalculated = angleIsCalculated = false; } /** * Copy all fields from one PointInfo class to another. PointInfo objects * are volatile so you should use this if you want to keep track of the last * touch event in your own code. */ public void set(PointInfo other) { this.numPoints = other.numPoints; for (int i = 0; i < numPoints; i++) { this.xs[i] = other.xs[i]; this.ys[i] = other.ys[i]; this.pressures[i] = other.pressures[i]; this.pointerIds[i] = other.pointerIds[i]; } this.xMid = other.xMid; this.yMid = other.yMid; this.pressureMid = other.pressureMid; this.dx = other.dx; this.dy = other.dy; this.diameter = other.diameter; this.diameterSq = other.diameterSq; this.angle = other.angle; this.isDown = other.isDown; this.action = other.action; this.isMultiTouch = other.isMultiTouch; this.diameterIsCalculated = other.diameterIsCalculated; this.diameterSqIsCalculated = other.diameterSqIsCalculated; this.angleIsCalculated = other.angleIsCalculated; this.eventTime = other.eventTime; } // --------------------------------------------------------------------------- /** True if number of touch points >= 2. */ public boolean isMultiTouch() { return isMultiTouch; } /** Difference between x coords of touchpoint 0 and 1. */ public float getMultiTouchWidth() { return isMultiTouch ? dx : 0.0f; } /** Difference between y coords of touchpoint 0 and 1. */ public float getMultiTouchHeight() { return isMultiTouch ? dy : 0.0f; } /** Fast integer sqrt, by Jim Ulery. Much faster than Math.sqrt() * for integers. */ 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) { if (!isMultiTouch) { diameter = 0.0f; } else { // 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) { if (!isMultiTouch) angle = 0.0f; else angle = (float) Math.atan2(ys[1] - ys[0], xs[1] - xs[0]); angleIsCalculated = true; } return angle; } // --------------------------------------------------------------------------- /** Return the total number of touch points */ public int getNumTouchPoints() { return numPoints; } /** Return the X coord of the first touch point if there's only one, * or the midpoint between first and second touch points if two or more. */ public float getX() { return xMid; } /** Return the array of X coords -- only the first getNumTouchPoints() * of these is defined. */ public float[] getXs() { return xs; } /** Return the X coord of the first touch point if there's only one, * or the midpoint between first and second touch points if two or more. */ public float getY() { return yMid; } /** Return the array of Y coords -- only the first getNumTouchPoints() * of these is defined. */ public float[] getYs() { return ys; } /** * Return the array of pointer ids -- only the first getNumTouchPoints() * of these is defined. These don't have to be all the numbers from 0 to * getNumTouchPoints()-1 inclusive, numbers can be skipped if a finger is * lifted and the touch sensor is capable of detecting that that * particular touch point is no longer down. Note that a lot of sensors do * not have this capability: when finger 1 is lifted up finger 2 * becomes the new finger 1. However in theory these IDs can correct for * that. Convert back to indices using MotionEvent.findPointerIndex(). */ public int[] getPointerIds() { return pointerIds; } /** Return the pressure the first touch point if there's only one, * or the average pressure of first and second touch points if two or more. */ public float getPressure() { return pressureMid; } /** Return the array of pressures -- only the first getNumTouchPoints() * of these is defined. */ public float[] getPressures() { return pressures; } // --------------------------------------------------------------------------- public boolean isDown() { return isDown; } public int getAction() { return action; } 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 static class PositionAndScale { private float xOff, yOff, scale, scaleX, scaleY, angle; private boolean updateScale, updateScaleXY, updateAngle; /** * Set position and optionally scale, anisotropic scale, and/or angle. * Where if the corresponding "update" flag is set to false, the field's * value will not be changed during a pinch operation. If the value is * not being updated *and* the value is not used by the client * application, then the value can just be zero. However if the value is * not being updated but the value *is* being used by the client * application, the value should still be specified and the update flag * should be false (e.g. angle of the object being dragged should still * be specified even if the program is in "resize" mode rather than "rotate" * mode). */ public void set(float xOff, float yOff, boolean updateScale, float scale, boolean updateScaleXY, float scaleX, float scaleY, boolean updateAngle, float angle) { this.xOff = xOff; this.yOff = yOff; this.updateScale = updateScale; this.scale = scale == 0.0f ? 1.0f : scale; this.updateScaleXY = updateScaleXY; this.scaleX = scaleX == 0.0f ? 1.0f : scaleX; this.scaleY = scaleY == 0.0f ? 1.0f : scaleY; this.updateAngle = updateAngle; this.angle = angle; } /** Set position and optionally scale, anisotropic scale, and/or angle, * without changing the "update" flags. */ protected void set(float xOff, float yOff, float scale, float scaleX, float scaleY, float angle) { this.xOff = xOff; this.yOff = yOff; this.scale = scale == 0.0f ? 1.0f : scale; this.scaleX = scaleX == 0.0f ? 1.0f : scaleX; this.scaleY = scaleY == 0.0f ? 1.0f : scaleY; this.angle = angle; } public float getXOff() { return xOff; } public float getYOff() { return yOff; } public float getScale() { return !updateScale ? 1.0f : scale; } /** Included in case you want to support anisotropic scaling */ public float getScaleX() { return !updateScaleXY ? 1.0f : scaleX; } /** Included in case you want to support anisotropic scaling */ public float getScaleY() { return !updateScaleXY ? 1.0f : scaleY; } public float getAngle() { return !updateAngle ? 0.0f : angle; } } // --------------------------------------------------------------------------- public static interface MultiTouchObjectCanvas<T> { /** * See if there is a draggable object at the current point. Returns the * object at the point, or null if nothing to drag. To start a multitouch * drag/stretch operation, this routine must return some non-null reference * to an object. This object is passed into the other methods in this interface * when they are called. * * @param touchPoint * The point being tested (in object coordinates). Return the * topmost object under this point, or if dragging/stretching * the whole canvas, just return a reference to the canvas. * @return a reference to the object under the point being tested, or * null to cancel the drag operation. If dragging/stretching the whole * canvas (e.g. in a photo viewer), always return non-null, otherwise * the stretch operation won't work. */ public T getDraggableObjectAtPoint(PointInfo touchPoint); /** * TODO: document */ public boolean pointInObjectGrabArea(PointInfo touchPoint, T obj); /** * Get the screen coords of the dragged object's origin, and scale * multiplier to convert screen coords to obj coords. The job of this routine * is to call the .set() method on the passed PositionAndScale object to * record the initial position and scale of the object (in object coordinates) * before any dragging/stretching takes place. * * @param obj * The object being dragged/stretched. * @param objPosAndScaleOut * Output parameter: You need to call objPosAndScaleOut.set() * to record the current position and scale of obj. */ public void getPositionAndScale(T obj, PositionAndScale objPosAndScaleOut); /** * Callback to update the position and scale (in object coords) of the * currently-dragged object. * * @param obj * The object being dragged/stretched. * @param newObjPosAndScale * The new position and scale of the object, in object * coordinates. Use this to move/resize the object before returning. * @param touchPoint * Info about the current touch point, including multitouch * information and utilities to calculate and cache multitouch pinch * diameter etc. (Note: touchPoint is volatile, if you want to * keep any fields of touchPoint, you must copy them before the method * body exits.) * @return true * if setting the position and scale of the object was successful, * or false if the position or scale parameters are out of range * for this object. */ public boolean setPositionAndScale(T 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 on touch-up. * * @param obj * The object being selected by single-touch, or null on touch-up. * @param touchPoint * The current touch point. */ public void selectObject(T obj, PointInfo touchPoint); } }