├── .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