├── .gitignore ├── MTController ├── .classpath ├── .project ├── AndroidManifest.xml ├── project.properties └── src │ └── org │ └── metalev │ └── multitouch │ └── controller │ └── MultiTouchController.java ├── MTPhotoSortr ├── .classpath ├── .project ├── AndroidManifest.xml ├── project.properties ├── res │ ├── drawable │ │ ├── catarina.jpg │ │ ├── icon.png │ │ ├── lake.jpg │ │ ├── m74hubble.jpg │ │ ├── sunset.jpg │ │ └── tahiti.jpg │ └── values │ │ └── strings.xml └── src │ └── org │ └── metalev │ └── multitouch │ └── photosortr │ ├── PhotoSortrActivity.java │ └── PhotoSortrView.java ├── MTVisualizer ├── .classpath ├── .project ├── AndroidManifest.xml ├── project.properties ├── res │ ├── drawable-hdpi │ │ └── icon.png │ ├── drawable-ldpi │ │ └── icon.png │ ├── drawable-mdpi │ │ └── icon.png │ └── values │ │ └── strings.xml └── src │ └── org │ └── metalev │ └── multitouch │ └── visualizer2 │ ├── MultiTouchVisualizerActivity.java │ └── MultiTouchVisualizerView.java └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen 3 | -------------------------------------------------------------------------------- /MTController/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MTController/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | MTController 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /MTController/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MTController/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | android.library=true 14 | # Project target. 15 | target=android-3 16 | -------------------------------------------------------------------------------- /MTController/src/org/metalev/multitouch/controller/MultiTouchController.java: -------------------------------------------------------------------------------- 1 | package org.metalev.multitouch.controller; 2 | 3 | /** 4 | * MultiTouchController.java 5 | * 6 | * Author: Luke Hutchison (luke.hutch@mit.edu) 7 | * Please drop me an email if you use this code so I can list your project here! 8 | * 9 | * Usage: 10 | * 11 | * public class MyMTView extends View implements MultiTouchObjectCanvas { 12 | * 13 | * private MultiTouchController multiTouchController = new MultiTouchController(this); 14 | * 15 | * // Pass touch events to the MT controller 16 | * public boolean onTouchEvent(MotionEvent event) { 17 | * return multiTouchController.onTouchEvent(event); 18 | * } 19 | * 20 | * // ... then implement the MultiTouchObjectCanvas interface here, see details in the comments of that interface. 21 | * } 22 | * 23 | * 24 | * Changelog: 25 | * 2010-06-09 v1.5.1 Some API changes to make it possible to selectively update or not update scale / rotation. 26 | * Fixed anisotropic zoom. Cleaned up rotation code. Added more comments. Better var names. (LH) 27 | * 2010-06-09 v1.4 Added ability to track pinch rotation (Mickael Despesse, author of "Face Frenzy") and anisotropic pinch-zoom (LH) 28 | * 2010-06-09 v1.3.3 Bugfixes for Android-2.1; added optional debug info (LH) 29 | * 2010-06-09 v1.3 Ported to Android-2.2 (handle ACTION_POINTER_* actions); fixed several bugs; refactoring; documentation (LH) 30 | * 2010-05-17 v1.2.1 Dual-licensed under Apache and GPL licenses 31 | * 2010-02-18 v1.2 Support for compilation under Android 1.5/1.6 using introspection (mmin, author of handyCalc) 32 | * 2010-01-08 v1.1.1 Bugfixes to Cyanogen's patch that only showed up in more complex uses of controller (LH) 33 | * 2010-01-06 v1.1 Modified for official level 5 MT API (Cyanogen) 34 | * 2009-01-25 v1.0 Original MT controller, released for hacked G1 kernel (LH) 35 | * 36 | * TODO: 37 | * - Add inertia (flick-pinch-zoom or flick-scroll) 38 | * - Merge in Paul Bourke's "grab" support for single-finger drag of objects: git://github.com/brk3/android-multitouch-controller.git 39 | * (Initial concern are the two lines of the form "newScale = mCurrXform.scale - 0.04f", and the line in pastThreshold() that says 40 | * "if (newScale == mCurrXform.scale)" -- this doesn't look like a robust solution to convey state, by changing scale by a tiny 41 | * amount, but maybe I'm not understanding the intent behind the code or its behavior). 42 | * 43 | * Known usages: see http://code.google.com/p/android-multitouch-controller/ 44 | * 45 | * -- 46 | * 47 | * Released under the MIT license (but please notify me if you use this code, so that I can give your project credit at 48 | * http://code.google.com/p/android-multitouch-controller ). 49 | * 50 | * MIT license: http://www.opensource.org/licenses/MIT 51 | * 52 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 53 | * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 54 | * 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: 55 | * 56 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 57 | * 58 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 59 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 60 | * 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 61 | * DEALINGS IN THE SOFTWARE. 62 | */ 63 | 64 | import java.lang.reflect.Method; 65 | 66 | import android.util.Log; 67 | import android.view.MotionEvent; 68 | 69 | /** 70 | * A class that simplifies the implementation of multitouch in applications. Subclass this and read the fields here as needed in subclasses. 71 | * 72 | * @author Luke Hutchison 73 | */ 74 | public class MultiTouchController { 75 | 76 | /** 77 | * 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 -- 78 | * helps eliminate noisy jumps that happen on change of status 79 | */ 80 | private static final long EVENT_SETTLE_TIME_INTERVAL = 20; 81 | 82 | /** 83 | * 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 84 | * pointer position on finger 2 up/down. 85 | */ 86 | private static final float MAX_MULTITOUCH_POS_JUMP_SIZE = 30.0f; 87 | 88 | /** 89 | * The biggest possible abs val of the change in multitouchWidth or multitouchHeight between multitouch events (larger-jump events are ignored) -- 90 | * helps eliminate jumps in pointer position on finger 2 up/down. 91 | */ 92 | private static final float MAX_MULTITOUCH_DIM_JUMP_SIZE = 40.0f; 93 | 94 | /** The smallest possible distance between multitouch points (used to avoid div-by-zero errors and display glitches) */ 95 | private static final float MIN_MULTITOUCH_SEPARATION = 30.0f; 96 | 97 | /** The max number of touch points that can be present on the screen at once */ 98 | public static final int MAX_TOUCH_POINTS = 20; 99 | 100 | /** Generate tons of log entries for debugging */ 101 | public static final boolean DEBUG = false; 102 | 103 | // ---------------------------------------------------------------------------------------------------------------------- 104 | 105 | MultiTouchObjectCanvas objectCanvas; 106 | 107 | /** The current touch point */ 108 | private PointInfo mCurrPt; 109 | 110 | /** The previous touch point */ 111 | private PointInfo mPrevPt; 112 | 113 | /** Fields extracted from mCurrPt */ 114 | private float mCurrPtX, mCurrPtY, mCurrPtDiam, mCurrPtWidth, mCurrPtHeight, mCurrPtAng; 115 | 116 | /** 117 | * Extract fields from mCurrPt, respecting the update* fields of mCurrPt. This just avoids code duplication. I hate that Java doesn't support 118 | * higher-order functions, tuples or multiple return values from functions. 119 | */ 120 | private void extractCurrPtInfo() { 121 | // Get new drag/pinch params. Only read multitouch fields that are needed, 122 | // to avoid unnecessary computation (diameter and angle are expensive operations). 123 | mCurrPtX = mCurrPt.getX(); 124 | mCurrPtY = mCurrPt.getY(); 125 | mCurrPtDiam = Math.max(MIN_MULTITOUCH_SEPARATION * .71f, !mCurrXform.updateScale ? 0.0f : mCurrPt.getMultiTouchDiameter()); 126 | mCurrPtWidth = Math.max(MIN_MULTITOUCH_SEPARATION, !mCurrXform.updateScaleXY ? 0.0f : mCurrPt.getMultiTouchWidth()); 127 | mCurrPtHeight = Math.max(MIN_MULTITOUCH_SEPARATION, !mCurrXform.updateScaleXY ? 0.0f : mCurrPt.getMultiTouchHeight()); 128 | mCurrPtAng = !mCurrXform.updateAngle ? 0.0f : mCurrPt.getMultiTouchAngle(); 129 | } 130 | 131 | // ---------------------------------------------------------------------------------------------------------------------- 132 | 133 | /** Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses */ 134 | private boolean handleSingleTouchEvents; 135 | 136 | /** The object being dragged/stretched */ 137 | private T selectedObject = null; 138 | 139 | /** Current position and scale of the dragged object */ 140 | private PositionAndScale mCurrXform = new PositionAndScale(); 141 | 142 | /** Drag/pinch start time and time to ignore spurious events until (to smooth over event noise) */ 143 | private long mSettleStartTime, mSettleEndTime; 144 | 145 | /** Conversion from object coords to screen coords */ 146 | private float startPosX, startPosY; 147 | 148 | /** Conversion between scale and width, and object angle and start pinch angle */ 149 | private float startScaleOverPinchDiam, startAngleMinusPinchAngle; 150 | 151 | /** Conversion between X scale and width, and Y scale and height */ 152 | private float startScaleXOverPinchWidth, startScaleYOverPinchHeight; 153 | 154 | // ---------------------------------------------------------------------------------------------------------------------- 155 | 156 | /** No touch points down. */ 157 | public static final int MODE_NOTHING = 0; 158 | 159 | /** One touch point down, dragging an object. */ 160 | public static final int MODE_DRAG = 1; 161 | 162 | /** Two or more touch points down, stretching/rotating an object using the first two touch points. */ 163 | public static final int MODE_PINCH = 2; 164 | 165 | /** Current drag mode */ 166 | private int mMode = MODE_NOTHING; 167 | 168 | // ---------------------------------------------------------------------------------------------------------------------- 169 | 170 | /** Constructor that sets handleSingleTouchEvents to true */ 171 | public MultiTouchController(MultiTouchObjectCanvas objectCanvas) { 172 | this(objectCanvas, true); 173 | } 174 | 175 | /** Full constructor */ 176 | public MultiTouchController(MultiTouchObjectCanvas objectCanvas, boolean handleSingleTouchEvents) { 177 | this.mCurrPt = new PointInfo(); 178 | this.mPrevPt = new PointInfo(); 179 | this.handleSingleTouchEvents = handleSingleTouchEvents; 180 | this.objectCanvas = objectCanvas; 181 | } 182 | 183 | // ------------------------------------------------------------------------------------ 184 | 185 | /** 186 | * Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses. Default: true 187 | */ 188 | protected void setHandleSingleTouchEvents(boolean handleSingleTouchEvents) { 189 | this.handleSingleTouchEvents = handleSingleTouchEvents; 190 | } 191 | 192 | /** 193 | * Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses. Default: true 194 | */ 195 | protected boolean getHandleSingleTouchEvents() { 196 | return handleSingleTouchEvents; 197 | } 198 | 199 | // ------------------------------------------------------------------------------------ 200 | 201 | public static final boolean multiTouchSupported; 202 | private static Method m_getPointerCount; 203 | private static Method m_getPointerId; 204 | private static Method m_getPressure; 205 | private static Method m_getHistoricalX; 206 | private static Method m_getHistoricalY; 207 | private static Method m_getHistoricalPressure; 208 | private static Method m_getX; 209 | private static Method m_getY; 210 | private static int ACTION_POINTER_UP = 6; 211 | private static int ACTION_POINTER_INDEX_SHIFT = 8; 212 | 213 | static { 214 | boolean succeeded = false; 215 | try { 216 | // Android 2.0.1 stuff: 217 | m_getPointerCount = MotionEvent.class.getMethod("getPointerCount"); 218 | m_getPointerId = MotionEvent.class.getMethod("getPointerId", Integer.TYPE); 219 | m_getPressure = MotionEvent.class.getMethod("getPressure", Integer.TYPE); 220 | m_getHistoricalX = MotionEvent.class.getMethod("getHistoricalX", Integer.TYPE, Integer.TYPE); 221 | m_getHistoricalY = MotionEvent.class.getMethod("getHistoricalY", Integer.TYPE, Integer.TYPE); 222 | m_getHistoricalPressure = MotionEvent.class.getMethod("getHistoricalPressure", Integer.TYPE, Integer.TYPE); 223 | m_getX = MotionEvent.class.getMethod("getX", Integer.TYPE); 224 | m_getY = MotionEvent.class.getMethod("getY", Integer.TYPE); 225 | succeeded = true; 226 | } catch (Exception e) { 227 | Log.e("MultiTouchController", "static initializer failed", e); 228 | } 229 | multiTouchSupported = succeeded; 230 | if (multiTouchSupported) { 231 | // Android 2.2+ stuff (the original Android 2.2 consts are declared above, 232 | // and these actions aren't used previous to Android 2.2): 233 | try { 234 | ACTION_POINTER_UP = MotionEvent.class.getField("ACTION_POINTER_UP").getInt(null); 235 | ACTION_POINTER_INDEX_SHIFT = MotionEvent.class.getField("ACTION_POINTER_INDEX_SHIFT").getInt(null); 236 | } catch (Exception e) { 237 | } 238 | } 239 | } 240 | 241 | // ------------------------------------------------------------------------------------ 242 | 243 | private static final float[] xVals = new float[MAX_TOUCH_POINTS]; 244 | private static final float[] yVals = new float[MAX_TOUCH_POINTS]; 245 | private static final float[] pressureVals = new float[MAX_TOUCH_POINTS]; 246 | private static final int[] pointerIds = new int[MAX_TOUCH_POINTS]; 247 | 248 | /** Process incoming touch events */ 249 | @SuppressWarnings("unused") 250 | public boolean onTouchEvent(MotionEvent event) { 251 | try { 252 | int pointerCount = multiTouchSupported ? (Integer) m_getPointerCount.invoke(event) : 1; 253 | if (DEBUG) 254 | Log.i("MultiTouch", "Got here 1 - " + multiTouchSupported + " " + mMode + " " + handleSingleTouchEvents + " " + pointerCount); 255 | if (mMode == MODE_NOTHING && !handleSingleTouchEvents && pointerCount == 1) 256 | // Not handling initial single touch events, just pass them on 257 | return false; 258 | if (DEBUG) 259 | Log.i("MultiTouch", "Got here 2"); 260 | 261 | // Handle history first (we sometimes get history with ACTION_MOVE events) 262 | int action = event.getAction(); 263 | int histLen = event.getHistorySize() / pointerCount; 264 | for (int histIdx = 0; histIdx <= histLen; histIdx++) { 265 | // Read from history entries until histIdx == histLen, then read from current event 266 | boolean processingHist = histIdx < histLen; 267 | if (!multiTouchSupported || pointerCount == 1) { 268 | // Use single-pointer methods -- these are needed as a special case (for some weird reason) even if 269 | // multitouch is supported but there's only one touch point down currently -- event.getX(0) etc. throw 270 | // an exception if there's only one point down. 271 | if (DEBUG) 272 | Log.i("MultiTouch", "Got here 3"); 273 | xVals[0] = processingHist ? event.getHistoricalX(histIdx) : event.getX(); 274 | yVals[0] = processingHist ? event.getHistoricalY(histIdx) : event.getY(); 275 | pressureVals[0] = processingHist ? event.getHistoricalPressure(histIdx) : event.getPressure(); 276 | } else { 277 | // Read x, y and pressure of each pointer 278 | if (DEBUG) 279 | Log.i("MultiTouch", "Got here 4"); 280 | int numPointers = Math.min(pointerCount, MAX_TOUCH_POINTS); 281 | if (DEBUG && pointerCount > MAX_TOUCH_POINTS) 282 | Log.i("MultiTouch", "Got more pointers than MAX_TOUCH_POINTS"); 283 | for (int ptrIdx = 0; ptrIdx < numPointers; ptrIdx++) { 284 | int ptrId = (Integer) m_getPointerId.invoke(event, ptrIdx); 285 | pointerIds[ptrIdx] = ptrId; 286 | // N.B. if pointerCount == 1, then the following methods throw an array index out of range exception, 287 | // and the code above is therefore required not just for Android 1.5/1.6 but also for when there is 288 | // only one touch point on the screen -- pointlessly inconsistent :( 289 | xVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalX.invoke(event, ptrIdx, histIdx) : m_getX.invoke(event, ptrIdx)); 290 | yVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalY.invoke(event, ptrIdx, histIdx) : m_getY.invoke(event, ptrIdx)); 291 | pressureVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalPressure.invoke(event, ptrIdx, histIdx) : m_getPressure 292 | .invoke(event, ptrIdx)); 293 | } 294 | } 295 | // Decode event 296 | decodeTouchEvent(pointerCount, xVals, yVals, pressureVals, pointerIds, // 297 | /* action = */processingHist ? MotionEvent.ACTION_MOVE : action, // 298 | /* down = */processingHist ? true : action != MotionEvent.ACTION_UP // 299 | && (action & ((1 << ACTION_POINTER_INDEX_SHIFT) - 1)) != ACTION_POINTER_UP // 300 | && action != MotionEvent.ACTION_CANCEL, // 301 | processingHist ? event.getHistoricalEventTime(histIdx) : event.getEventTime()); 302 | } 303 | 304 | return true; 305 | } catch (Exception e) { 306 | // In case any of the introspection stuff fails (it shouldn't) 307 | Log.e("MultiTouchController", "onTouchEvent() failed", e); 308 | return false; 309 | } 310 | } 311 | 312 | private void decodeTouchEvent(int pointerCount, float[] x, float[] y, float[] pressure, int[] pointerIds, int action, boolean down, long eventTime) { 313 | if (DEBUG) 314 | Log.i("MultiTouch", "Got here 5 - " + pointerCount + " " + action + " " + down); 315 | 316 | // Swap curr/prev points 317 | PointInfo tmp = mPrevPt; 318 | mPrevPt = mCurrPt; 319 | mCurrPt = tmp; 320 | // Overwrite old prev point 321 | mCurrPt.set(pointerCount, x, y, pressure, pointerIds, action, down, eventTime); 322 | multiTouchController(); 323 | } 324 | 325 | // ------------------------------------------------------------------------------------ 326 | 327 | /** Start dragging/pinching, or reset drag/pinch to current point if something goes out of range */ 328 | private void anchorAtThisPositionAndScale() { 329 | if (DEBUG) 330 | Log.i("MulitTouch", "anchorAtThisPositionAndScale()"); 331 | if (selectedObject == null) 332 | return; 333 | 334 | // Get selected object's current position and scale 335 | objectCanvas.getPositionAndScale(selectedObject, mCurrXform); 336 | 337 | // Figure out the object coords of the drag start point's screen coords. 338 | // All stretching should be around this point in object-coord-space. 339 | // Also figure out out ratio between object scale factor and multitouch 340 | // diameter at beginning of drag; same for angle and optional anisotropic 341 | // scale. 342 | float currScaleInv = 1.0f / (!mCurrXform.updateScale ? 1.0f : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale); 343 | extractCurrPtInfo(); 344 | startPosX = (mCurrPtX - mCurrXform.xOff) * currScaleInv; 345 | startPosY = (mCurrPtY - mCurrXform.yOff) * currScaleInv; 346 | startScaleOverPinchDiam = mCurrXform.scale / mCurrPtDiam; 347 | startScaleXOverPinchWidth = mCurrXform.scaleX / mCurrPtWidth; 348 | startScaleYOverPinchHeight = mCurrXform.scaleY / mCurrPtHeight; 349 | startAngleMinusPinchAngle = mCurrXform.angle - mCurrPtAng; 350 | } 351 | 352 | /** Drag/stretch/rotate the selected object using the current touch position(s) relative to the anchor position(s). */ 353 | private void performDragOrPinch() { 354 | // Don't do anything if we're not dragging anything 355 | if (selectedObject == null) 356 | return; 357 | 358 | // Calc new position of dragged object 359 | float currScale = !mCurrXform.updateScale ? 1.0f : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale; 360 | extractCurrPtInfo(); 361 | float newPosX = mCurrPtX - startPosX * currScale; 362 | float newPosY = mCurrPtY - startPosY * currScale; 363 | float newScale = startScaleOverPinchDiam * mCurrPtDiam; 364 | float newScaleX = startScaleXOverPinchWidth * mCurrPtWidth; 365 | float newScaleY = startScaleYOverPinchHeight * mCurrPtHeight; 366 | float newAngle = startAngleMinusPinchAngle + mCurrPtAng; 367 | 368 | // Set the new obj coords, scale, and angle as appropriate (notifying the subclass of the change). 369 | mCurrXform.set(newPosX, newPosY, newScale, newScaleX, newScaleY, newAngle); 370 | 371 | boolean success = objectCanvas.setPositionAndScale(selectedObject, mCurrXform, mCurrPt); 372 | if (!success) 373 | ; // If we could't set those params, do nothing currently 374 | } 375 | 376 | /** 377 | * State-based controller for tracking switches between no-touch, single-touch and multi-touch situations. Includes logic for cleaning up the 378 | * event stream, as events around touch up/down are noisy at least on early Synaptics sensors. 379 | */ 380 | private void multiTouchController() { 381 | if (DEBUG) 382 | Log.i("MultiTouch", "Got here 6 - " + mMode + " " + mCurrPt.getNumTouchPoints() + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch()); 383 | 384 | switch (mMode) { 385 | case MODE_NOTHING: 386 | if (DEBUG) 387 | Log.i("MultiTouch", "MODE_NOTHING"); 388 | // Not doing anything currently 389 | if (mCurrPt.isDown()) { 390 | // Start a new single-point drag 391 | selectedObject = objectCanvas.getDraggableObjectAtPoint(mCurrPt); 392 | if (selectedObject != null) { 393 | // Started a new single-point drag 394 | mMode = MODE_DRAG; 395 | objectCanvas.selectObject(selectedObject, mCurrPt); 396 | anchorAtThisPositionAndScale(); 397 | // Don't need any settling time if just placing one finger, there is no noise 398 | mSettleStartTime = mSettleEndTime = mCurrPt.getEventTime(); 399 | } 400 | } 401 | break; 402 | 403 | case MODE_DRAG: 404 | if (DEBUG) 405 | Log.i("MultiTouch", "MODE_DRAG"); 406 | // Currently in a single-point drag 407 | if (!mCurrPt.isDown()) { 408 | // First finger was released, stop dragging 409 | mMode = MODE_NOTHING; 410 | objectCanvas.selectObject((selectedObject = null), mCurrPt); 411 | 412 | } else if (mCurrPt.isMultiTouch()) { 413 | // Point 1 was already down and point 2 was just placed down 414 | mMode = MODE_PINCH; 415 | // Restart the drag with the new drag position (that is at the midpoint between the touchpoints) 416 | anchorAtThisPositionAndScale(); 417 | // Need to let events settle before moving things, to help with event noise on touchdown 418 | mSettleStartTime = mCurrPt.getEventTime(); 419 | mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; 420 | 421 | } else { 422 | // Point 1 is still down and point 2 did not change state, just do single-point drag to new location 423 | if (mCurrPt.getEventTime() < mSettleEndTime) { 424 | // Ignore the first few events if we just stopped stretching, because if finger 2 was kept down while 425 | // finger 1 is lifted, then point 1 gets mapped to finger 2. Restart the drag from the new position. 426 | anchorAtThisPositionAndScale(); 427 | } else { 428 | // Keep dragging, move to new point 429 | performDragOrPinch(); 430 | } 431 | } 432 | break; 433 | 434 | case MODE_PINCH: 435 | if (DEBUG) 436 | Log.i("MultiTouch", "MODE_PINCH"); 437 | // Two-point pinch-scale/rotate/translate 438 | if (!mCurrPt.isMultiTouch() || !mCurrPt.isDown()) { 439 | // Dropped one or both points, stop stretching 440 | 441 | if (!mCurrPt.isDown()) { 442 | // Dropped both points, go back to doing nothing 443 | mMode = MODE_NOTHING; 444 | objectCanvas.selectObject((selectedObject = null), mCurrPt); 445 | 446 | } else { 447 | // Just dropped point 2, downgrade to a single-point drag 448 | mMode = MODE_DRAG; 449 | // Restart the pinch with the single-finger position 450 | anchorAtThisPositionAndScale(); 451 | // Ignore the first few events after the drop, in case we dropped finger 1 and left finger 2 down 452 | mSettleStartTime = mCurrPt.getEventTime(); 453 | mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; 454 | } 455 | 456 | } else { 457 | // Still pinching 458 | if (Math.abs(mCurrPt.getX() - mPrevPt.getX()) > MAX_MULTITOUCH_POS_JUMP_SIZE 459 | || Math.abs(mCurrPt.getY() - mPrevPt.getY()) > MAX_MULTITOUCH_POS_JUMP_SIZE 460 | || Math.abs(mCurrPt.getMultiTouchWidth() - mPrevPt.getMultiTouchWidth()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE 461 | || Math.abs(mCurrPt.getMultiTouchHeight() - mPrevPt.getMultiTouchHeight()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE) { 462 | // Jumped too far, probably event noise, reset and ignore events for a bit 463 | anchorAtThisPositionAndScale(); 464 | mSettleStartTime = mCurrPt.getEventTime(); 465 | mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; 466 | 467 | } else if (mCurrPt.eventTime < mSettleEndTime) { 468 | // Events have not yet settled, reset 469 | anchorAtThisPositionAndScale(); 470 | } else { 471 | // Stretch to new position and size 472 | performDragOrPinch(); 473 | } 474 | } 475 | break; 476 | } 477 | if (DEBUG) 478 | Log.i("MultiTouch", "Got here 7 - " + mMode + " " + mCurrPt.getNumTouchPoints() + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch()); 479 | } 480 | 481 | public int getMode() { 482 | return mMode; 483 | } 484 | 485 | /** A class that packages up all MotionEvent information with all derived multitouch information (if available) */ 486 | public static class PointInfo { 487 | // Multitouch information 488 | private int numPoints; 489 | private float[] xs = new float[MAX_TOUCH_POINTS]; 490 | private float[] ys = new float[MAX_TOUCH_POINTS]; 491 | private float[] pressures = new float[MAX_TOUCH_POINTS]; 492 | private int[] pointerIds = new int[MAX_TOUCH_POINTS]; 493 | 494 | // Midpoint of pinch operations 495 | private float xMid, yMid, pressureMid; 496 | 497 | // Width/diameter/angle of pinch operations 498 | private float dx, dy, diameter, diameterSq, angle; 499 | 500 | // Whether or not there is at least one finger down (isDown) and/or at least two fingers down (isMultiTouch) 501 | private boolean isDown, isMultiTouch; 502 | 503 | // Whether or not these fields have already been calculated, for caching purposes 504 | private boolean diameterSqIsCalculated, diameterIsCalculated, angleIsCalculated; 505 | 506 | // Event action code and event time 507 | private int action; 508 | private long eventTime; 509 | 510 | // ------------------------------------------------------------------------------------------------------------------------------------------- 511 | 512 | /** Set all point info */ 513 | private void set(int numPoints, float[] x, float[] y, float[] pressure, int[] pointerIds, int action, boolean isDown, long eventTime) { 514 | if (DEBUG) 515 | Log.i("MultiTouch", "Got here 8 - " + +numPoints + " " + x[0] + " " + y[0] + " " + (numPoints > 1 ? x[1] : x[0]) + " " 516 | + (numPoints > 1 ? y[1] : y[0]) + " " + action + " " + isDown); 517 | this.eventTime = eventTime; 518 | this.action = action; 519 | this.numPoints = numPoints; 520 | for (int i = 0; i < numPoints; i++) { 521 | this.xs[i] = x[i]; 522 | this.ys[i] = y[i]; 523 | this.pressures[i] = pressure[i]; 524 | this.pointerIds[i] = pointerIds[i]; 525 | } 526 | this.isDown = isDown; 527 | this.isMultiTouch = numPoints >= 2; 528 | 529 | if (isMultiTouch) { 530 | xMid = (x[0] + x[1]) * .5f; 531 | yMid = (y[0] + y[1]) * .5f; 532 | pressureMid = (pressure[0] + pressure[1]) * .5f; 533 | dx = Math.abs(x[1] - x[0]); 534 | dy = Math.abs(y[1] - y[0]); 535 | 536 | } else { 537 | // Single-touch event 538 | xMid = x[0]; 539 | yMid = y[0]; 540 | pressureMid = pressure[0]; 541 | dx = dy = 0.0f; 542 | } 543 | // Need to re-calculate the expensive params if they're needed 544 | diameterSqIsCalculated = diameterIsCalculated = angleIsCalculated = false; 545 | } 546 | 547 | /** 548 | * 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 549 | * last touch event in your own code. 550 | */ 551 | public void set(PointInfo other) { 552 | this.numPoints = other.numPoints; 553 | for (int i = 0; i < numPoints; i++) { 554 | this.xs[i] = other.xs[i]; 555 | this.ys[i] = other.ys[i]; 556 | this.pressures[i] = other.pressures[i]; 557 | this.pointerIds[i] = other.pointerIds[i]; 558 | } 559 | this.xMid = other.xMid; 560 | this.yMid = other.yMid; 561 | this.pressureMid = other.pressureMid; 562 | this.dx = other.dx; 563 | this.dy = other.dy; 564 | this.diameter = other.diameter; 565 | this.diameterSq = other.diameterSq; 566 | this.angle = other.angle; 567 | this.isDown = other.isDown; 568 | this.action = other.action; 569 | this.isMultiTouch = other.isMultiTouch; 570 | this.diameterIsCalculated = other.diameterIsCalculated; 571 | this.diameterSqIsCalculated = other.diameterSqIsCalculated; 572 | this.angleIsCalculated = other.angleIsCalculated; 573 | this.eventTime = other.eventTime; 574 | } 575 | 576 | // ------------------------------------------------------------------------------------------------------------------------------------------- 577 | 578 | /** True if number of touch points >= 2. */ 579 | public boolean isMultiTouch() { 580 | return isMultiTouch; 581 | } 582 | 583 | /** Difference between x coords of touchpoint 0 and 1. */ 584 | public float getMultiTouchWidth() { 585 | return isMultiTouch ? dx : 0.0f; 586 | } 587 | 588 | /** Difference between y coords of touchpoint 0 and 1. */ 589 | public float getMultiTouchHeight() { 590 | return isMultiTouch ? dy : 0.0f; 591 | } 592 | 593 | /** Fast integer sqrt, by Jim Ulery. Much faster than Math.sqrt() for integers. */ 594 | private int julery_isqrt(int val) { 595 | int temp, g = 0, b = 0x8000, bshft = 15; 596 | do { 597 | if (val >= (temp = (((g << 1) + b) << bshft--))) { 598 | g += b; 599 | val -= temp; 600 | } 601 | } while ((b >>= 1) > 0); 602 | return g; 603 | } 604 | 605 | /** Calculate the squared diameter of the multitouch event, and cache it. Use this if you don't need to perform the sqrt. */ 606 | public float getMultiTouchDiameterSq() { 607 | if (!diameterSqIsCalculated) { 608 | diameterSq = (isMultiTouch ? dx * dx + dy * dy : 0.0f); 609 | diameterSqIsCalculated = true; 610 | } 611 | return diameterSq; 612 | } 613 | 614 | /** Calculate the diameter of the multitouch event, and cache it. Uses fast int sqrt but gives accuracy to 1/16px. */ 615 | public float getMultiTouchDiameter() { 616 | if (!diameterIsCalculated) { 617 | if (!isMultiTouch) { 618 | diameter = 0.0f; 619 | } else { 620 | // Get 1/16 pixel's worth of subpixel accuracy, works on screens up to 2048x2048 621 | // before we get overflow (at which point you can reduce or eliminate subpix 622 | // accuracy, or use longs in julery_isqrt()) 623 | float diamSq = getMultiTouchDiameterSq(); 624 | diameter = (diamSq == 0.0f ? 0.0f : (float) julery_isqrt((int) (256 * diamSq)) / 16.0f); 625 | // Make sure diameter is never less than dx or dy, for trig purposes 626 | if (diameter < dx) 627 | diameter = dx; 628 | if (diameter < dy) 629 | diameter = dy; 630 | } 631 | diameterIsCalculated = true; 632 | } 633 | return diameter; 634 | } 635 | 636 | /** 637 | * 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 638 | * between the two touchpoints, so range is [0,Math.PI/2]. Uses Math.atan2(). 639 | */ 640 | public float getMultiTouchAngle() { 641 | if (!angleIsCalculated) { 642 | if (!isMultiTouch) 643 | angle = 0.0f; 644 | else 645 | angle = (float) Math.atan2(ys[1] - ys[0], xs[1] - xs[0]); 646 | angleIsCalculated = true; 647 | } 648 | return angle; 649 | } 650 | 651 | // ------------------------------------------------------------------------------------------------------------------------------------------- 652 | 653 | /** Return the total number of touch points */ 654 | public int getNumTouchPoints() { 655 | return numPoints; 656 | } 657 | 658 | /** 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. */ 659 | public float getX() { 660 | return xMid; 661 | } 662 | 663 | /** Return the array of X coords -- only the first getNumTouchPoints() of these is defined. */ 664 | public float[] getXs() { 665 | return xs; 666 | } 667 | 668 | /** 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. */ 669 | public float getY() { 670 | return yMid; 671 | } 672 | 673 | /** Return the array of Y coords -- only the first getNumTouchPoints() of these is defined. */ 674 | public float[] getYs() { 675 | return ys; 676 | } 677 | 678 | /** 679 | * 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 680 | * getNumTouchPoints()-1 inclusive, numbers can be skipped if a finger is lifted and the touch sensor is capable of detecting that that 681 | * 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 682 | * becomes the new finger 1. However in theory these IDs can correct for that. Convert back to indices using MotionEvent.findPointerIndex(). 683 | */ 684 | public int[] getPointerIds() { 685 | return pointerIds; 686 | } 687 | 688 | /** 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. */ 689 | public float getPressure() { 690 | return pressureMid; 691 | } 692 | 693 | /** Return the array of pressures -- only the first getNumTouchPoints() of these is defined. */ 694 | public float[] getPressures() { 695 | return pressures; 696 | } 697 | 698 | // ------------------------------------------------------------------------------------------------------------------------------------------- 699 | 700 | public boolean isDown() { 701 | return isDown; 702 | } 703 | 704 | public int getAction() { 705 | return action; 706 | } 707 | 708 | public long getEventTime() { 709 | return eventTime; 710 | } 711 | } 712 | 713 | // ------------------------------------------------------------------------------------ 714 | 715 | /** 716 | * A class that is used to store scroll offsets and scale information for objects that are managed by the multitouch controller 717 | */ 718 | public static class PositionAndScale { 719 | private float xOff, yOff, scale, scaleX, scaleY, angle; 720 | private boolean updateScale, updateScaleXY, updateAngle; 721 | 722 | /** 723 | * Set position and optionally scale, anisotropic scale, and/or angle. Where if the corresponding "update" flag is set to false, the field's 724 | * 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 725 | * 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 726 | * application, the value should still be specified and the update flag should be false (e.g. angle of the object being dragged should still 727 | * be specified even if the program is in "resize" mode rather than "rotate" mode). 728 | */ 729 | public void set(float xOff, float yOff, boolean updateScale, float scale, boolean updateScaleXY, float scaleX, float scaleY, 730 | boolean updateAngle, float angle) { 731 | this.xOff = xOff; 732 | this.yOff = yOff; 733 | this.updateScale = updateScale; 734 | this.scale = scale == 0.0f ? 1.0f : scale; 735 | this.updateScaleXY = updateScaleXY; 736 | this.scaleX = scaleX == 0.0f ? 1.0f : scaleX; 737 | this.scaleY = scaleY == 0.0f ? 1.0f : scaleY; 738 | this.updateAngle = updateAngle; 739 | this.angle = angle; 740 | } 741 | 742 | /** Set position and optionally scale, anisotropic scale, and/or angle, without changing the "update" flags. */ 743 | protected void set(float xOff, float yOff, float scale, float scaleX, float scaleY, float angle) { 744 | this.xOff = xOff; 745 | this.yOff = yOff; 746 | this.scale = scale == 0.0f ? 1.0f : scale; 747 | this.scaleX = scaleX == 0.0f ? 1.0f : scaleX; 748 | this.scaleY = scaleY == 0.0f ? 1.0f : scaleY; 749 | this.angle = angle; 750 | } 751 | 752 | public float getXOff() { 753 | return xOff; 754 | } 755 | 756 | public float getYOff() { 757 | return yOff; 758 | } 759 | 760 | public float getScale() { 761 | return !updateScale ? 1.0f : scale; 762 | } 763 | 764 | /** Included in case you want to support anisotropic scaling */ 765 | public float getScaleX() { 766 | return !updateScaleXY ? 1.0f : scaleX; 767 | } 768 | 769 | /** Included in case you want to support anisotropic scaling */ 770 | public float getScaleY() { 771 | return !updateScaleXY ? 1.0f : scaleY; 772 | } 773 | 774 | public float getAngle() { 775 | return !updateAngle ? 0.0f : angle; 776 | } 777 | } 778 | 779 | // ------------------------------------------------------------------------------------ 780 | 781 | public static interface MultiTouchObjectCanvas { 782 | 783 | /** 784 | * 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 785 | * drag/stretch operation, this routine must return some non-null reference to an object. This object is passed into the other methods in this 786 | * interface when they are called. 787 | * 788 | * @param touchPoint 789 | * The point being tested (in object coordinates). Return the topmost object under this point, or if dragging/stretching the whole 790 | * canvas, just return a reference to the canvas. 791 | * @return a reference to the object under the point being tested, or null to cancel the drag operation. If dragging/stretching the whole 792 | * canvas (e.g. in a photo viewer), always return non-null, otherwise the stretch operation won't work. 793 | */ 794 | public T getDraggableObjectAtPoint(PointInfo touchPoint); 795 | 796 | /** 797 | * 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 798 | * is to call the .set() method on the passed PositionAndScale object to record the initial position and scale of the object (in object 799 | * coordinates) before any dragging/stretching takes place. 800 | * 801 | * @param obj 802 | * The object being dragged/stretched. 803 | * @param objPosAndScaleOut 804 | * Output parameter: You need to call objPosAndScaleOut.set() to record the current position and scale of obj. 805 | */ 806 | public void getPositionAndScale(T obj, PositionAndScale objPosAndScaleOut); 807 | 808 | /** 809 | * Callback to update the position and scale (in object coords) of the currently-dragged object. 810 | * 811 | * @param obj 812 | * The object being dragged/stretched. 813 | * @param newObjPosAndScale 814 | * The new position and scale of the object, in object coordinates. Use this to move/resize the object before returning. 815 | * @param touchPoint 816 | * Info about the current touch point, including multitouch information and utilities to calculate and cache multitouch pinch 817 | * diameter etc. (Note: touchPoint is volatile, if you want to keep any fields of touchPoint, you must copy them before the method 818 | * body exits.) 819 | * @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 820 | * for this object. 821 | */ 822 | public boolean setPositionAndScale(T obj, PositionAndScale newObjPosAndScale, PointInfo touchPoint); 823 | 824 | /** 825 | * 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 826 | * multitouch is initiated. Also called with null on touch-up. 827 | * 828 | * @param obj 829 | * The object being selected by single-touch, or null on touch-up. 830 | * @param touchPoint 831 | * The current touch point. 832 | */ 833 | public void selectObject(T obj, PointInfo touchPoint); 834 | } 835 | } 836 | -------------------------------------------------------------------------------- /MTPhotoSortr/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MTPhotoSortr/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | MTPhotoSortrDemo 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /MTPhotoSortr/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /MTPhotoSortr/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Indicates whether an apk should be generated for each density. 14 | split.density=false 15 | # Project target. 16 | target=android-4 17 | android.library.reference.1=../MTController 18 | -------------------------------------------------------------------------------- /MTPhotoSortr/res/drawable/catarina.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukehutch/android-multitouch-controller/38a6a0dce972930071e41cd9cecb28270e85d874/MTPhotoSortr/res/drawable/catarina.jpg -------------------------------------------------------------------------------- /MTPhotoSortr/res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukehutch/android-multitouch-controller/38a6a0dce972930071e41cd9cecb28270e85d874/MTPhotoSortr/res/drawable/icon.png -------------------------------------------------------------------------------- /MTPhotoSortr/res/drawable/lake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukehutch/android-multitouch-controller/38a6a0dce972930071e41cd9cecb28270e85d874/MTPhotoSortr/res/drawable/lake.jpg -------------------------------------------------------------------------------- /MTPhotoSortr/res/drawable/m74hubble.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukehutch/android-multitouch-controller/38a6a0dce972930071e41cd9cecb28270e85d874/MTPhotoSortr/res/drawable/m74hubble.jpg -------------------------------------------------------------------------------- /MTPhotoSortr/res/drawable/sunset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukehutch/android-multitouch-controller/38a6a0dce972930071e41cd9cecb28270e85d874/MTPhotoSortr/res/drawable/sunset.jpg -------------------------------------------------------------------------------- /MTPhotoSortr/res/drawable/tahiti.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukehutch/android-multitouch-controller/38a6a0dce972930071e41cd9cecb28270e85d874/MTPhotoSortr/res/drawable/tahiti.jpg -------------------------------------------------------------------------------- /MTPhotoSortr/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Drag or stretch photos 4 | MultiTouch PhotoSortr 5 | 6 | -------------------------------------------------------------------------------- /MTPhotoSortr/src/org/metalev/multitouch/photosortr/PhotoSortrActivity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * PhotoSorterActivity.java 3 | * 4 | * (c) Luke Hutchison (luke.hutch@mit.edu) 5 | * 6 | * -- 7 | * 8 | * Released under the MIT license (but please notify me if you use this code, so that I can give your project credit at 9 | * http://code.google.com/p/android-multitouch-controller ). 10 | * 11 | * MIT license: http://www.opensource.org/licenses/MIT 12 | * 13 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 14 | * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * 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: 16 | * 17 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * 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 22 | * DEALINGS IN THE SOFTWARE. 23 | */ 24 | package org.metalev.multitouch.photosortr; 25 | 26 | import android.app.Activity; 27 | import android.os.Bundle; 28 | import android.view.KeyEvent; 29 | 30 | public class PhotoSortrActivity extends Activity { 31 | 32 | PhotoSortrView photoSorter; 33 | 34 | @Override 35 | public void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | this.setTitle(R.string.instructions); 38 | photoSorter = new PhotoSortrView(this); 39 | setContentView(photoSorter); 40 | } 41 | 42 | @Override 43 | protected void onResume() { 44 | super.onResume(); 45 | photoSorter.loadImages(this); 46 | } 47 | 48 | @Override 49 | protected void onPause() { 50 | super.onPause(); 51 | photoSorter.unloadImages(); 52 | } 53 | 54 | @Override 55 | public boolean onKeyDown(int keyCode, KeyEvent event) { 56 | if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 57 | photoSorter.trackballClicked(); 58 | return true; 59 | } 60 | return super.onKeyDown(keyCode, event); 61 | } 62 | } -------------------------------------------------------------------------------- /MTPhotoSortr/src/org/metalev/multitouch/photosortr/PhotoSortrView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * PhotoSorterView.java 3 | * 4 | * (c) Luke Hutchison (luke.hutch@mit.edu) 5 | * 6 | * TODO: Add OpenGL acceleration. 7 | * 8 | * -- 9 | * 10 | * Released under the MIT license (but please notify me if you use this code, so that I can give your project credit at 11 | * http://code.google.com/p/android-multitouch-controller ). 12 | * 13 | * MIT license: http://www.opensource.org/licenses/MIT 14 | * 15 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 16 | * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 17 | * 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: 18 | * 19 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * 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 24 | * DEALINGS IN THE SOFTWARE. 25 | */ 26 | package org.metalev.multitouch.photosortr; 27 | 28 | import java.util.ArrayList; 29 | 30 | import org.metalev.multitouch.controller.MultiTouchController; 31 | import org.metalev.multitouch.controller.MultiTouchController.MultiTouchObjectCanvas; 32 | import org.metalev.multitouch.controller.MultiTouchController.PointInfo; 33 | import org.metalev.multitouch.controller.MultiTouchController.PositionAndScale; 34 | 35 | import android.content.Context; 36 | import android.content.res.Configuration; 37 | import android.content.res.Resources; 38 | import android.graphics.Canvas; 39 | import android.graphics.Color; 40 | import android.graphics.Paint; 41 | import android.graphics.Paint.Style; 42 | import android.graphics.drawable.Drawable; 43 | import android.util.AttributeSet; 44 | import android.util.DisplayMetrics; 45 | import android.view.MotionEvent; 46 | import android.view.View; 47 | 48 | public class PhotoSortrView extends View implements MultiTouchObjectCanvas { 49 | 50 | private static final int[] IMAGES = { R.drawable.m74hubble, R.drawable.catarina, R.drawable.tahiti, R.drawable.sunset, R.drawable.lake }; 51 | 52 | private ArrayList mImages = new ArrayList(); 53 | 54 | // -- 55 | 56 | private MultiTouchController multiTouchController = new MultiTouchController(this); 57 | 58 | // -- 59 | 60 | private PointInfo currTouchPoint = new PointInfo(); 61 | 62 | private boolean mShowDebugInfo = true; 63 | 64 | private static final int UI_MODE_ROTATE = 1, UI_MODE_ANISOTROPIC_SCALE = 2; 65 | 66 | private int mUIMode = UI_MODE_ROTATE; 67 | 68 | // -- 69 | 70 | private Paint mLinePaintTouchPointCircle = new Paint(); 71 | 72 | // --------------------------------------------------------------------------------------------------- 73 | 74 | public PhotoSortrView(Context context) { 75 | this(context, null); 76 | } 77 | 78 | public PhotoSortrView(Context context, AttributeSet attrs) { 79 | this(context, attrs, 0); 80 | } 81 | 82 | public PhotoSortrView(Context context, AttributeSet attrs, int defStyle) { 83 | super(context, attrs, defStyle); 84 | init(context); 85 | } 86 | 87 | private void init(Context context) { 88 | Resources res = context.getResources(); 89 | for (int i = 0; i < IMAGES.length; i++) 90 | mImages.add(new Img(IMAGES[i], res)); 91 | 92 | mLinePaintTouchPointCircle.setColor(Color.YELLOW); 93 | mLinePaintTouchPointCircle.setStrokeWidth(5); 94 | mLinePaintTouchPointCircle.setStyle(Style.STROKE); 95 | mLinePaintTouchPointCircle.setAntiAlias(true); 96 | setBackgroundColor(Color.BLACK); 97 | } 98 | 99 | /** Called by activity's onResume() method to load the images */ 100 | public void loadImages(Context context) { 101 | Resources res = context.getResources(); 102 | int n = mImages.size(); 103 | for (int i = 0; i < n; i++) 104 | mImages.get(i).load(res); 105 | } 106 | 107 | /** Called by activity's onPause() method to free memory used for loading the images */ 108 | public void unloadImages() { 109 | int n = mImages.size(); 110 | for (int i = 0; i < n; i++) 111 | mImages.get(i).unload(); 112 | } 113 | 114 | // --------------------------------------------------------------------------------------------------- 115 | 116 | @Override 117 | protected void onDraw(Canvas canvas) { 118 | super.onDraw(canvas); 119 | int n = mImages.size(); 120 | for (int i = 0; i < n; i++) 121 | mImages.get(i).draw(canvas); 122 | if (mShowDebugInfo) 123 | drawMultitouchDebugMarks(canvas); 124 | } 125 | 126 | // --------------------------------------------------------------------------------------------------- 127 | 128 | public void trackballClicked() { 129 | mUIMode = (mUIMode + 1) % 3; 130 | invalidate(); 131 | } 132 | 133 | private void drawMultitouchDebugMarks(Canvas canvas) { 134 | if (currTouchPoint.isDown()) { 135 | float[] xs = currTouchPoint.getXs(); 136 | float[] ys = currTouchPoint.getYs(); 137 | float[] pressures = currTouchPoint.getPressures(); 138 | int numPoints = Math.min(currTouchPoint.getNumTouchPoints(), 2); 139 | for (int i = 0; i < numPoints; i++) 140 | canvas.drawCircle(xs[i], ys[i], 50 + pressures[i] * 80, mLinePaintTouchPointCircle); 141 | if (numPoints == 2) 142 | canvas.drawLine(xs[0], ys[0], xs[1], ys[1], mLinePaintTouchPointCircle); 143 | } 144 | } 145 | 146 | // --------------------------------------------------------------------------------------------------- 147 | 148 | /** Pass touch events to the MT controller */ 149 | @Override 150 | public boolean onTouchEvent(MotionEvent event) { 151 | return multiTouchController.onTouchEvent(event); 152 | } 153 | 154 | /** Get the image that is under the single-touch point, or return null (canceling the drag op) if none */ 155 | public Img getDraggableObjectAtPoint(PointInfo pt) { 156 | float x = pt.getX(), y = pt.getY(); 157 | int n = mImages.size(); 158 | for (int i = n - 1; i >= 0; i--) { 159 | Img im = mImages.get(i); 160 | if (im.containsPoint(x, y)) 161 | return im; 162 | } 163 | return null; 164 | } 165 | 166 | /** 167 | * Select an object for dragging. Called whenever an object is found to be under the point (non-null is returned by getDraggableObjectAtPoint()) 168 | * and a drag operation is starting. Called with null when drag op ends. 169 | */ 170 | public void selectObject(Img img, PointInfo touchPoint) { 171 | currTouchPoint.set(touchPoint); 172 | if (img != null) { 173 | // Move image to the top of the stack when selected 174 | mImages.remove(img); 175 | mImages.add(img); 176 | } else { 177 | // Called with img == null when drag stops. 178 | } 179 | invalidate(); 180 | } 181 | 182 | /** Get the current position and scale of the selected image. Called whenever a drag starts or is reset. */ 183 | public void getPositionAndScale(Img img, PositionAndScale objPosAndScaleOut) { 184 | // FIXME affine-izem (and fix the fact that the anisotropic_scale part requires averaging the two scale factors) 185 | objPosAndScaleOut.set(img.getCenterX(), img.getCenterY(), (mUIMode & UI_MODE_ANISOTROPIC_SCALE) == 0, 186 | (img.getScaleX() + img.getScaleY()) / 2, (mUIMode & UI_MODE_ANISOTROPIC_SCALE) != 0, img.getScaleX(), img.getScaleY(), 187 | (mUIMode & UI_MODE_ROTATE) != 0, img.getAngle()); 188 | } 189 | 190 | /** Set the position and scale of the dragged/stretched image. */ 191 | public boolean setPositionAndScale(Img img, PositionAndScale newImgPosAndScale, PointInfo touchPoint) { 192 | currTouchPoint.set(touchPoint); 193 | boolean ok = img.setPos(newImgPosAndScale); 194 | if (ok) 195 | invalidate(); 196 | return ok; 197 | } 198 | 199 | // ---------------------------------------------------------------------------------------------- 200 | 201 | class Img { 202 | private int resId; 203 | 204 | private Drawable drawable; 205 | 206 | private boolean firstLoad; 207 | 208 | private int width, height, displayWidth, displayHeight; 209 | 210 | private float centerX, centerY, scaleX, scaleY, angle; 211 | 212 | private float minX, maxX, minY, maxY; 213 | 214 | private static final float SCREEN_MARGIN = 100; 215 | 216 | public Img(int resId, Resources res) { 217 | this.resId = resId; 218 | this.firstLoad = true; 219 | getMetrics(res); 220 | } 221 | 222 | private void getMetrics(Resources res) { 223 | DisplayMetrics metrics = res.getDisplayMetrics(); 224 | // The DisplayMetrics don't seem to always be updated on screen rotate, so we hard code a portrait 225 | // screen orientation for the non-rotated screen here... 226 | // this.displayWidth = metrics.widthPixels; 227 | // this.displayHeight = metrics.heightPixels; 228 | this.displayWidth = res.getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? Math.max(metrics.widthPixels, 229 | metrics.heightPixels) : Math.min(metrics.widthPixels, metrics.heightPixels); 230 | this.displayHeight = res.getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? Math.min(metrics.widthPixels, 231 | metrics.heightPixels) : Math.max(metrics.widthPixels, metrics.heightPixels); 232 | } 233 | 234 | /** Called by activity's onResume() method to load the images */ 235 | public void load(Resources res) { 236 | getMetrics(res); 237 | this.drawable = res.getDrawable(resId); 238 | this.width = drawable.getIntrinsicWidth(); 239 | this.height = drawable.getIntrinsicHeight(); 240 | float cx, cy, sx, sy; 241 | if (firstLoad) { 242 | cx = SCREEN_MARGIN + (float) (Math.random() * (displayWidth - 2 * SCREEN_MARGIN)); 243 | cy = SCREEN_MARGIN + (float) (Math.random() * (displayHeight - 2 * SCREEN_MARGIN)); 244 | float sc = (float) (Math.max(displayWidth, displayHeight) / (float) Math.max(width, height) * Math.random() * 0.3 + 0.2); 245 | sx = sy = sc; 246 | firstLoad = false; 247 | } else { 248 | // Reuse position and scale information if it is available 249 | // FIXME this doesn't actually work because the whole activity is torn down and re-created on rotate 250 | cx = this.centerX; 251 | cy = this.centerY; 252 | sx = this.scaleX; 253 | sy = this.scaleY; 254 | // Make sure the image is not off the screen after a screen rotation 255 | if (this.maxX < SCREEN_MARGIN) 256 | cx = SCREEN_MARGIN; 257 | else if (this.minX > displayWidth - SCREEN_MARGIN) 258 | cx = displayWidth - SCREEN_MARGIN; 259 | if (this.maxY > SCREEN_MARGIN) 260 | cy = SCREEN_MARGIN; 261 | else if (this.minY > displayHeight - SCREEN_MARGIN) 262 | cy = displayHeight - SCREEN_MARGIN; 263 | } 264 | setPos(cx, cy, sx, sy, 0.0f); 265 | } 266 | 267 | /** Called by activity's onPause() method to free memory used for loading the images */ 268 | public void unload() { 269 | this.drawable = null; 270 | } 271 | 272 | /** Set the position and scale of an image in screen coordinates */ 273 | public boolean setPos(PositionAndScale newImgPosAndScale) { 274 | return setPos(newImgPosAndScale.getXOff(), newImgPosAndScale.getYOff(), (mUIMode & UI_MODE_ANISOTROPIC_SCALE) != 0 ? newImgPosAndScale 275 | .getScaleX() : newImgPosAndScale.getScale(), (mUIMode & UI_MODE_ANISOTROPIC_SCALE) != 0 ? newImgPosAndScale.getScaleY() 276 | : newImgPosAndScale.getScale(), newImgPosAndScale.getAngle()); 277 | // FIXME: anisotropic scaling jumps when axis-snapping 278 | // FIXME: affine-ize 279 | // return setPos(newImgPosAndScale.getXOff(), newImgPosAndScale.getYOff(), newImgPosAndScale.getScaleAnisotropicX(), 280 | // newImgPosAndScale.getScaleAnisotropicY(), 0.0f); 281 | } 282 | 283 | /** Set the position and scale of an image in screen coordinates */ 284 | private boolean setPos(float centerX, float centerY, float scaleX, float scaleY, float angle) { 285 | float ws = (width / 2) * scaleX, hs = (height / 2) * scaleY; 286 | float newMinX = centerX - ws, newMinY = centerY - hs, newMaxX = centerX + ws, newMaxY = centerY + hs; 287 | if (newMinX > displayWidth - SCREEN_MARGIN || newMaxX < SCREEN_MARGIN || newMinY > displayHeight - SCREEN_MARGIN 288 | || newMaxY < SCREEN_MARGIN) 289 | return false; 290 | this.centerX = centerX; 291 | this.centerY = centerY; 292 | this.scaleX = scaleX; 293 | this.scaleY = scaleY; 294 | this.angle = angle; 295 | this.minX = newMinX; 296 | this.minY = newMinY; 297 | this.maxX = newMaxX; 298 | this.maxY = newMaxY; 299 | return true; 300 | } 301 | 302 | /** Return whether or not the given screen coords are inside this image */ 303 | public boolean containsPoint(float scrnX, float scrnY) { 304 | // FIXME: need to correctly account for image rotation 305 | return (scrnX >= minX && scrnX <= maxX && scrnY >= minY && scrnY <= maxY); 306 | } 307 | 308 | public void draw(Canvas canvas) { 309 | canvas.save(); 310 | float dx = (maxX + minX) / 2; 311 | float dy = (maxY + minY) / 2; 312 | drawable.setBounds((int) minX, (int) minY, (int) maxX, (int) maxY); 313 | canvas.translate(dx, dy); 314 | canvas.rotate(angle * 180.0f / (float) Math.PI); 315 | canvas.translate(-dx, -dy); 316 | drawable.draw(canvas); 317 | canvas.restore(); 318 | } 319 | 320 | public Drawable getDrawable() { 321 | return drawable; 322 | } 323 | 324 | public int getWidth() { 325 | return width; 326 | } 327 | 328 | public int getHeight() { 329 | return height; 330 | } 331 | 332 | public float getCenterX() { 333 | return centerX; 334 | } 335 | 336 | public float getCenterY() { 337 | return centerY; 338 | } 339 | 340 | public float getScaleX() { 341 | return scaleX; 342 | } 343 | 344 | public float getScaleY() { 345 | return scaleY; 346 | } 347 | 348 | public float getAngle() { 349 | return angle; 350 | } 351 | 352 | // FIXME: these need to be updated for rotation 353 | public float getMinX() { 354 | return minX; 355 | } 356 | 357 | public float getMaxX() { 358 | return maxX; 359 | } 360 | 361 | public float getMinY() { 362 | return minY; 363 | } 364 | 365 | public float getMaxY() { 366 | return maxY; 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /MTVisualizer/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MTVisualizer/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | MTVisualizer 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /MTVisualizer/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 10 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /MTVisualizer/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Indicates whether an apk should be generated for each density. 14 | split.density=false 15 | # Project target. 16 | target=android-4 17 | apk-configurations= 18 | android.library.reference.1=../MTController 19 | -------------------------------------------------------------------------------- /MTVisualizer/res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukehutch/android-multitouch-controller/38a6a0dce972930071e41cd9cecb28270e85d874/MTVisualizer/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /MTVisualizer/res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukehutch/android-multitouch-controller/38a6a0dce972930071e41cd9cecb28270e85d874/MTVisualizer/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /MTVisualizer/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukehutch/android-multitouch-controller/38a6a0dce972930071e41cd9cecb28270e85d874/MTVisualizer/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /MTVisualizer/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MultiTouch Visualizer 2 4 | MultiTouch Visualizer 2 5 | 6 | -------------------------------------------------------------------------------- /MTVisualizer/src/org/metalev/multitouch/visualizer2/MultiTouchVisualizerActivity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * MultiTouchVisualizerActivity.java 3 | * 4 | * (c) Luke Hutchison (luke.hutch@mit.edu) 5 | * 6 | * -- 7 | * 8 | * Released under the MIT license (but please notify me if you use this code, so that I can give your project credit at 9 | * http://code.google.com/p/android-multitouch-controller ). 10 | * 11 | * MIT license: http://www.opensource.org/licenses/MIT 12 | * 13 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 14 | * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * 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: 16 | * 17 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * 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 22 | * DEALINGS IN THE SOFTWARE. 23 | */ 24 | package org.metalev.multitouch.visualizer2; 25 | 26 | import android.app.Activity; 27 | import android.os.Bundle; 28 | 29 | public class MultiTouchVisualizerActivity extends Activity { 30 | @Override 31 | public void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | this.setTitle(R.string.instructions); 34 | setContentView(new MultiTouchVisualizerView(this)); 35 | } 36 | } -------------------------------------------------------------------------------- /MTVisualizer/src/org/metalev/multitouch/visualizer2/MultiTouchVisualizerView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * MultiTouchVisualizerView.java 3 | * 4 | * (c) Luke Hutchison (luke.hutch@mit.edu) 5 | * 6 | * -- 7 | * 8 | * Released under the MIT license (but please notify me if you use this code, so that I can give your project credit at 9 | * http://code.google.com/p/android-multitouch-controller ). 10 | * 11 | * MIT license: http://www.opensource.org/licenses/MIT 12 | * 13 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 14 | * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * 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: 16 | * 17 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * 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 22 | * DEALINGS IN THE SOFTWARE. 23 | */ 24 | package org.metalev.multitouch.visualizer2; 25 | 26 | import org.metalev.multitouch.controller.MultiTouchController; 27 | import org.metalev.multitouch.controller.MultiTouchController.MultiTouchObjectCanvas; 28 | import org.metalev.multitouch.controller.MultiTouchController.PointInfo; 29 | import org.metalev.multitouch.controller.MultiTouchController.PositionAndScale; 30 | 31 | import android.content.Context; 32 | import android.graphics.Canvas; 33 | import android.graphics.Color; 34 | import android.graphics.Paint; 35 | import android.graphics.Rect; 36 | import android.graphics.Typeface; 37 | import android.graphics.Paint.Align; 38 | import android.graphics.Paint.Style; 39 | import android.util.AttributeSet; 40 | import android.util.Log; 41 | import android.view.MotionEvent; 42 | import android.view.View; 43 | 44 | public class MultiTouchVisualizerView extends View implements MultiTouchObjectCanvas { 45 | 46 | private MultiTouchController multiTouchController; 47 | 48 | private PointInfo mCurrTouchPoint; 49 | 50 | // -- 51 | 52 | private static final int[] TOUCH_COLORS = { Color.YELLOW, Color.GREEN, Color.CYAN, Color.MAGENTA, Color.YELLOW, Color.BLUE, Color.WHITE, 53 | Color.GRAY, Color.LTGRAY, Color.DKGRAY }; 54 | 55 | private Paint mLinePaintSingleTouch = new Paint(); 56 | 57 | private Paint mLinePaintCoords = new Paint(); 58 | 59 | private Paint mLinePaintSnapped = new Paint(); 60 | 61 | private Paint mLinePaintSecondTouch = new Paint(); 62 | 63 | private Paint mLinePaintMultiTouch = new Paint(); 64 | 65 | private Paint mLinePaintCrossHairs = new Paint(); 66 | 67 | private Paint mPointLabelPaint = new Paint(); 68 | 69 | private Paint mTouchTheScreenLabelPaint = new Paint(); 70 | 71 | private Paint mPointLabelBg = new Paint(); 72 | 73 | private Paint mAngLabelPaint = new Paint(); 74 | 75 | private Paint mAngLabelBg = new Paint(); 76 | 77 | private int[] mTouchPointColors = new int[MultiTouchController.MAX_TOUCH_POINTS]; 78 | 79 | // ------------------------------------------------------------------------------------ 80 | 81 | public MultiTouchVisualizerView(Context context) { 82 | this(context, null); 83 | } 84 | 85 | public MultiTouchVisualizerView(Context context, AttributeSet attrs) { 86 | this(context, attrs, 0); 87 | } 88 | 89 | public MultiTouchVisualizerView(Context context, AttributeSet attrs, int defStyle) { 90 | super(context, attrs, defStyle); 91 | 92 | multiTouchController = new MultiTouchController(this); 93 | mCurrTouchPoint = new PointInfo(); 94 | 95 | mLinePaintSingleTouch.setColor(TOUCH_COLORS[0]); 96 | mLinePaintSingleTouch.setStrokeWidth(5); 97 | mLinePaintSingleTouch.setStyle(Style.STROKE); 98 | mLinePaintSingleTouch.setAntiAlias(true); 99 | mLinePaintCoords.setColor(Color.RED); 100 | mLinePaintCoords.setStrokeWidth(5); 101 | mLinePaintCoords.setStyle(Style.STROKE); 102 | mLinePaintCoords.setAntiAlias(true); 103 | mLinePaintSnapped.setColor(Color.BLUE); 104 | mLinePaintSnapped.setAlpha(128); 105 | mLinePaintSnapped.setStyle(Style.STROKE); 106 | mLinePaintSnapped.setStrokeWidth(40); 107 | mLinePaintCoords.setAntiAlias(false); 108 | mLinePaintSecondTouch.setColor(TOUCH_COLORS[1]); 109 | mLinePaintSecondTouch.setStrokeWidth(5); 110 | mLinePaintSecondTouch.setStyle(Style.STROKE); 111 | mLinePaintSecondTouch.setAntiAlias(true); 112 | mLinePaintMultiTouch.setStrokeWidth(5); 113 | mLinePaintMultiTouch.setStyle(Style.STROKE); 114 | mLinePaintMultiTouch.setAntiAlias(true); 115 | mLinePaintCrossHairs.setColor(Color.BLUE); 116 | mLinePaintCrossHairs.setStrokeWidth(5); 117 | mLinePaintCrossHairs.setStyle(Style.STROKE); 118 | mLinePaintCrossHairs.setAntiAlias(true); 119 | mPointLabelPaint.setTextSize(82); 120 | mPointLabelPaint.setTypeface(Typeface.DEFAULT_BOLD); 121 | mPointLabelPaint.setAntiAlias(true); 122 | mTouchTheScreenLabelPaint.setColor(Color.GRAY); 123 | mTouchTheScreenLabelPaint.setTextSize(24); 124 | mTouchTheScreenLabelPaint.setTypeface(Typeface.DEFAULT_BOLD); 125 | mTouchTheScreenLabelPaint.setAntiAlias(true); 126 | mPointLabelBg.set(mPointLabelPaint); 127 | mPointLabelBg.setColor(Color.BLACK); 128 | mPointLabelBg.setAlpha(180); 129 | mPointLabelBg.setStyle(Style.STROKE); 130 | mPointLabelBg.setStrokeWidth(15); 131 | mAngLabelPaint.setTextSize(32); 132 | mAngLabelPaint.setTypeface(Typeface.SANS_SERIF); 133 | mAngLabelPaint.setColor(mLinePaintCrossHairs.getColor()); 134 | mAngLabelPaint.setTextAlign(Align.CENTER); 135 | mAngLabelPaint.setAntiAlias(true); 136 | mAngLabelBg.set(mAngLabelPaint); 137 | mAngLabelBg.setColor(Color.BLACK); 138 | mAngLabelBg.setAlpha(180); 139 | mAngLabelBg.setStyle(Style.STROKE); 140 | mAngLabelBg.setStrokeWidth(15); 141 | setBackgroundColor(Color.BLACK); 142 | 143 | for (int i = 0; i < MultiTouchController.MAX_TOUCH_POINTS; i++) 144 | mTouchPointColors[i] = i < TOUCH_COLORS.length ? TOUCH_COLORS[i] : (int) (Math.random() * 0xffffff) + 0xff000000; 145 | } 146 | 147 | @Override 148 | public boolean onTouchEvent(MotionEvent event) { 149 | // Pass the event on to the controller 150 | return multiTouchController.onTouchEvent(event); 151 | } 152 | 153 | public Object getDraggableObjectAtPoint(PointInfo pt) { 154 | // IMPORTANT: to start a multitouch drag operation, this routine must return non-null 155 | return this; 156 | } 157 | 158 | public void getPositionAndScale(Object obj, PositionAndScale objPosAndScaleOut) { 159 | // We aren't dragging any objects, so this doesn't do anything in this app 160 | } 161 | 162 | public void selectObject(Object obj, PointInfo touchPoint) { 163 | // We aren't dragging any objects in this particular app, but this is called when the point goes up (obj == null) or down (obj != null), 164 | // save the touch point info 165 | touchPointChanged(touchPoint); 166 | } 167 | 168 | public boolean setPositionAndScale(Object obj, PositionAndScale newObjPosAndScale, PointInfo touchPoint) { 169 | // Called during a drag or stretch operation, update the touch point info 170 | touchPointChanged(touchPoint); 171 | return true; 172 | } 173 | 174 | /** 175 | * Called when the touch point info changes, causes a redraw. 176 | * 177 | * @param touchPoint 178 | */ 179 | private void touchPointChanged(PointInfo touchPoint) { 180 | // Take a snapshot of touch point info, the touch point is volatile 181 | mCurrTouchPoint.set(touchPoint); 182 | invalidate(); 183 | } 184 | 185 | private void paintText(Canvas canvas, String msg, float vPos) { 186 | Rect bounds = new Rect(); 187 | int msgLen = msg.length(); 188 | mTouchTheScreenLabelPaint.getTextBounds(msg, 0, msgLen, bounds); 189 | canvas.drawText(msg, (canvas.getWidth() - bounds.width()) * .5f, vPos, mTouchTheScreenLabelPaint); 190 | } 191 | 192 | private static final String[] infoLines = { "Touch the screen", "with one or more", "fingers to test", "multitouch", "characteristics" }; 193 | 194 | @Override 195 | protected void onDraw(Canvas canvas) { 196 | super.onDraw(canvas); 197 | if (mCurrTouchPoint.isDown()) { 198 | int numPoints = mCurrTouchPoint.getNumTouchPoints(); 199 | float[] xs = mCurrTouchPoint.getXs(); 200 | float[] ys = mCurrTouchPoint.getYs(); 201 | float[] pressures = mCurrTouchPoint.getPressures(); 202 | int[] pointerIds = mCurrTouchPoint.getPointerIds(); 203 | float x = mCurrTouchPoint.getX(), y = mCurrTouchPoint.getY(); 204 | float wd = getWidth(), ht = getHeight(); 205 | 206 | if (numPoints == 1) { 207 | // Draw ordinate lines for single touch point 208 | canvas.drawLine(0, y, wd, y, mLinePaintCoords); 209 | canvas.drawLine(x, 0, x, ht, mLinePaintCoords); 210 | 211 | } else if (numPoints == 2) { 212 | float dx2 = mCurrTouchPoint.getMultiTouchWidth() / 2; 213 | float dy2 = mCurrTouchPoint.getMultiTouchHeight() / 2; 214 | 215 | // Horiz/vert ordinate lines 216 | if (dx2 < 0.85f) { 217 | // Snapped (for some reason, it's not precise on the Nexus One due to event noise) 218 | canvas.drawLine(x, 0, x, ht, mLinePaintSnapped); 219 | } else { 220 | canvas.drawLine(x + dx2, 0, x + dx2, ht, mLinePaintCoords); 221 | canvas.drawLine(x - dx2, 0, x - dx2, ht, mLinePaintCoords); 222 | } 223 | if (dy2 < 0.85f) { 224 | // Snapped 225 | canvas.drawLine(0, y, wd, y, mLinePaintSnapped); 226 | } else { 227 | canvas.drawLine(0, y + dy2, wd, y + dy2, mLinePaintCoords); 228 | canvas.drawLine(0, y - dy2, wd, y - dy2, mLinePaintCoords); 229 | } 230 | 231 | // Diag lines 232 | canvas.drawLine(x + dx2, y + dy2, x - dx2, y - dy2, mLinePaintCrossHairs); 233 | canvas.drawLine(x + dx2, y - dy2, x - dx2, y + dy2, mLinePaintCrossHairs); 234 | 235 | // Crosshairs 236 | canvas.drawLine(0, y, wd, y, mLinePaintCrossHairs); 237 | canvas.drawLine(x, 0, x, ht, mLinePaintCrossHairs); 238 | 239 | // Circle 240 | canvas.drawCircle(x, y, mCurrTouchPoint.getMultiTouchDiameter() / 2, mLinePaintCrossHairs); 241 | } 242 | 243 | // Show touch circles 244 | for (int i = 0; i < numPoints; i++) { 245 | mLinePaintMultiTouch.setColor(mTouchPointColors[i]); 246 | float r = 70 + pressures[i] * 120; 247 | canvas.drawCircle(xs[i], ys[i], r, mLinePaintMultiTouch); 248 | } 249 | 250 | // Label pinch distance 251 | if (numPoints == 2) { 252 | float ang = mCurrTouchPoint.getMultiTouchAngle() * 180.0f / (float) Math.PI; 253 | // Keep text rightway up 254 | if (ang < -91.0f) 255 | ang += 180.0f; 256 | else if (ang > 91.0f) 257 | ang -= 180.0f; 258 | String angStr = "Pinch dist: " + Math.round(mCurrTouchPoint.getMultiTouchDiameter()); 259 | canvas.save(); 260 | canvas.translate(x, y); 261 | canvas.rotate(ang); 262 | canvas.drawText(angStr, 0, -10, mAngLabelBg); 263 | canvas.drawText(angStr, 0, -10, mAngLabelPaint); 264 | canvas.restore(); 265 | } 266 | 267 | // Log touch point indices 268 | if (MultiTouchController.DEBUG) { 269 | StringBuilder buf = new StringBuilder(); 270 | for (int i = 0; i < numPoints; i++) 271 | buf.append(" " + i + "->" + pointerIds[i]); 272 | Log.i("MultiTouchVisualizer", buf.toString()); 273 | } 274 | 275 | // Label touch points on top of everything else 276 | for (int idx = 0; idx < numPoints; idx++) { 277 | int id = pointerIds[idx]; 278 | mPointLabelPaint.setColor(mTouchPointColors[idx]); 279 | float r = 70 + pressures[idx] * 120, d = r * .71f; 280 | String label = (idx + 1) + (idx == id ? "" : "(id:" + (id + 1) + ")"); 281 | canvas.drawText(label, xs[idx] + d, ys[idx] - d, mPointLabelBg); 282 | canvas.drawText(label, xs[idx] + d, ys[idx] - d, mPointLabelPaint); 283 | } 284 | } else { 285 | float spacing = mTouchTheScreenLabelPaint.getFontSpacing(); 286 | float totHeight = spacing * infoLines.length; 287 | for (int i = 0; i < infoLines.length; i++) 288 | paintText(canvas, infoLines[i], (canvas.getHeight() - totHeight) * .5f + i * spacing); 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Welcome to the android-multitouch-controller project. 2 | 3 | This project currently comprises three Android sub-projects: 4 | 1. MTController, the MultiTouch Controller class for Android (see below); 5 | 2. MTVisualizer, the source code for the app "MultiTouch Visualizer 2" on Google Play; 6 | 3. MTPhotoSortr, a demo app showing how to use the MultiTouch Controller class. 7 | 8 | This MultiTouch Controller class makes it much easier to write multitouch applications for Android: 9 | - It filters out "event noise" on Synaptics touch screens (G1, MyTouch, Nexus One) -- for example, when you have two touch points down and lift just one finger, each of the ordinates X and Y can be lifted in separate touch events, meaning you get a spurious motion event (or several events) consisting of a sudden fast snap of the touch point to the other axis before the correct single touch event is generated. 10 | - It simplifies the somewhat messy and inconsistent MotionEvent touch point API -- this API has grown from handling single touch points, potentially with packaged event history (Android 1.6 and earlier) to multiple indistinguised touch points (Android 2.0) to the potential for handling multiple touch points that are kept distinct even if lower-indexed touchpoints are raised (and thus each point has an index and generates its own indexed touch-up/touch-down event). All this means there are a lot of API quirks you have to be aware of. This MultiTouch Controller class simplifies getting access to these events for applications that just want event positions and up/down status. 11 | - The controller also supports pinch-zoom, including tracking the transformation between screen coordinates and object coordinates. It correctly centers the pinch operation about the center of the pinch (not about the center of the screen, as with most of the "Google Experience" apps that added their own pinch-zoom capability in Android-2.x). This also means that you can do a combined pinch-drag operation that will simultaneously translate and scale an object. This is the only natural way to implement pinch-zoom, and subconsciously feels much more natural than scaling about the center of the screen. Compare pinch-zoom in Google Maps to Fractoid (in Market) to see what I mean -- Fractoid uses this multitouch controller code. 12 | - The controller was recently updated to support pinch-rotate, allowing you to physically twist objects using two touch points on the screen. In fact all of rotate, scale and translate can be simultaneously adjusted based on relative movements of the first two touch points. NOTE: rotation is quirky on older touchscreen devices that use a Synaptics or Synaptics-like "2x1D" sensor (G1, MyTouch, Droid, Nexus One) and not a true 2D sensor like the HTC Incredible or HTC EVO 4G. The quirky behavior results from "axis snapping" when the two points get close together in X or Y, and "ordinate confusion" where (x1,y1) and (x2,y2) get confused for (x1,y2) and (x2,y1). There is no way around this other than to keep the two fingers in the same two relative quadrants (i.e. keep them on a leading or a trailing diagonal), or to disallow rotation on these devices. (In spite of misinformation on the Web, there is also no firmware or software update that can fix this problem, it is a hardware limitation. Hopefully all newer phones will have a true 2D touch sensor.) 13 | - I also added anisotropic scaling as an alternative to using the rotation and scale information, so that if you are scaling something like a graph which has a different X and Y scale, you can dynamically change both scales by simultaneously stretching in horizontal and vertical directions. 14 | - The controller makes it very easy to work with a canvas of separate objects (e.g. a stack of photos), each of which can be separately dragged with a single touch point or scaled with a pinch operation. 15 | 16 | An example of how to use the API is included in the "MTPhotoSortr" demo app in the source repository linked above. (The source is not very polished but it shows you the basics of how to use the controller.) A second example is the app in the Android Market called "MultiTouch Visualizer 2". The source for this app is available in this source code repository too. 17 | 18 | # Known bugs 19 | For pinch-zoom, currently the center of the scaling operation is the center of the pinched object, not the midpoint between the two touch points on the screen, due to an error in converting between screen and object coordinates and vice versa. This needs to be fixed, and would be a nice (and relatively simple) contribution if anyone is willing to submit a patch :-) It requires some understanding of composition of transformations. The current code does not use matrix transformations, but it might be worth converting the code to do matrix math. (There are also a few other issues listed on the Issues page.) 20 | 21 | # License 22 | Licensed under the MIT license. 23 | 24 | # In use by 25 | Please send me a note if you use this code, I'm interested to see where it is used. So far I know of (in approximate reverse chronological order of receiving notification): 26 | - FloraMe: Landscaping made easy, by Paulo Cordeiro 27 | - Photo Designs : Photo Editor by ANH2 team 28 | - Instachaka by Rodrigo Arjona 29 | - Photo Editor : Photo Collage by Vinicorp 30 | - Avare app for aviators, available on Play and github 31 | - OSMDroid 32 | - Wifi Compass by Thomas Konrad and Paul Wölfel, for triangulating Wifi access point locations indoors by using the accelerometer to trace how far you have walked. 33 | - KiwiViewer 3D isosurface visualizer by Pat Marion and Kitware 34 | - Compass VO by Alin Berce 35 | - mCatalog by AHG (Alex Heiphetz) 36 | - File Manager and File Manager HD by Rhythm Software 37 | - Collage by David Erosa 38 | - RoidRage by Paul Bourke 39 | - The Fundroid development platform by Ignacio Bautiste Cuervo 40 | - Androzic by Andrey Novikov 41 | - Go Scoring Camera by Jim Babcock 42 | - Fractoid by Dave Byrne 43 | - Face Frenzy by Mickael Despesse 44 | - Yuan Chin's fork of ADW Launcher that supports multitouch 45 | - mmin's handyCalc calculator 46 | - My own app "MultiTouch Visualizer 2" 47 | - 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 Gallery3D as of API level 5. 48 | - Other mentions: Paul Bourke's blog post on using android-multitouch-controller 49 | 50 | # Author: 51 | Luke Hutchison -- luke dot hutch at gmail dot com (@LH on Twitter) 52 | --------------------------------------------------------------------------------