package org.metalev.multitouch.controller; /** * MultiTouchController.java * Source https://github.com/lukehutch/android-multitouch-controller/blob/master/MTController/src/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) * * TODO: * - Add inertia (flick-pinch-zoom or flick-scroll) * - Merge in Paul Bourke's "grab" support for single-finger drag of objects: git://github.com/brk3/android-multitouch-controller.git * (Initial concern are the two lines of the form "newScale = mCurrXform.scale - 0.04f", and the line in pastThreshold() that says * "if (newScale == mCurrXform.scale)" -- this doesn't look like a robust solution to convey state, by changing scale by a tiny * amount, but maybe I'm not understanding the intent behind the code or its behavior). * * Known usages: see http://code.google.com/p/android-multitouch-controller/ * * -- * * Released under the MIT license (but please notify me if you use this code, so that I can give your project credit at * http://code.google.com/p/android-multitouch-controller ). * * MIT license: http://www.opensource.org/licenses/MIT * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ 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 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; // ---------------------------------------------------------------------------------------------------------------------- /** No touch points down. */ private static final int MODE_NOTHING = 0; /** One touch point down, dragging an object. */ private static final int MODE_DRAG = 1; /** Two or more touch points down, stretching/rotating an object using the first two touch points. */ private static final int MODE_PINCH = 2; /** 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 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; float newScale = startScaleOverPinchDiam * mCurrPtDiam; 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 } /** Indicate if we are in the middle of a pinch action or not. osmdroid addon*/ public boolean isPinching() { return mMode == MODE_PINCH; } /** * 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) { // 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_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); } 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 greater than or equal to 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); /** * 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); } }