From b445a5cbe7e8ee37acb8ea8175735486be2d7e84 Mon Sep 17 00:00:00 2001 From: Pavel fljot Date: Tue, 7 Aug 2012 20:48:02 +0300 Subject: [PATCH] v0.4 Squashed commit of the following: commit 1eb3dfa9e10ba56dd1083f06393c6baa14eb2690 Author: Pavel fljot Date: Tue Aug 7 17:47:43 2012 +0300 Bumped version to 0.4 commit 3acafd1dfb5208407755528eeace21eb3db8663c Author: Pavel fljot Date: Tue Aug 7 17:46:53 2012 +0300 Minor fix for Gesture state machine to dispatch STATE_CHANGE even when cycling around CHANGED state commit 2678e12de8fb3273b57b9f38c5b73e25076c6db2 Author: Pavel fljot Date: Tue Aug 7 17:46:12 2012 +0300 Add protected from a mistyped event listening commit a3b618e90a86d54984b89346aedcd956992f750c Author: Pavel fljot Date: Tue Aug 7 17:38:10 2012 +0300 Move native adapters to extensions package commit 9c817b7472cbf441bf75b2ef0ffc3c0ee9c9be8e Author: Pavel fljot Date: Wed Aug 1 19:41:58 2012 +0300 Implement touch cancelation handling commit 7bfb1fae365a1bee549876a156a02bd45326fef5 Author: Pavel fljot Date: Fri Jul 20 14:52:18 2012 +0300 Minor type fix commit 7bacbf087b9368221c363597f95d689acf038daa Author: Pavel fljot Date: Fri Jul 20 14:49:48 2012 +0300 Minor changes commit 7278e863d46d69756970f9b9f3fa26e6234a7d2d Author: Pavel fljot Date: Fri Jul 20 14:48:52 2012 +0300 Refactor dirty gestures reset commit eab6cb4a1c2bd4e121c14444bed741575d3c9236 Author: Pavel fljot Date: Fri Jul 20 14:47:52 2012 +0300 Remove unnecessary casting commit 4bdefd12bba3cea720b64ebbd58d2e6b182b8d40 Author: Pavel fljot Date: Fri Jul 13 18:27:20 2012 +0300 Minor cleanup in LongPressGesture commit c5f91e6de1b11da28ce9798c7f2be01156fb65df Author: Pavel fljot Date: Fri Jul 13 18:08:07 2012 +0300 Improve internal algorithm for TapGesture commit ed2efc19541a9e96f38cf6dbbce94ef8ca81f16f Author: Pavel fljot Date: Fri Jul 13 16:51:06 2012 +0300 Improve internal algorithm for SwipeGesture commit c2125a06e1c5a1817451cbe95dc4979496678e26 Author: Pavel fljot Date: Thu Jul 12 12:46:11 2012 +0300 Remove unnecessary imports commit 4a8e6feada2f738c187348f8a55b00eb1afd6a7a Author: Pavel fljot Date: Wed Jul 11 22:47:55 2012 +0300 Improve internal algorithm for SwipeGesture but still under question commit 850ed9849f2f78ffc9b01ed42b55fc7ea09226a0 Author: Pavel fljot Date: Wed Jul 11 13:34:50 2012 +0300 Change rotation values from degrees to radians commit 7be7c8c40a639ba1ad3678f4404a36d3b7b513f3 Author: Pavel fljot Date: Wed Jul 11 13:28:43 2012 +0300 Improve internal algorithm for TransformGesture commit f8149935dbc13be9eb2e94f5122618e9e04c163a Author: Pavel fljot Date: Tue Jul 10 17:11:46 2012 +0300 Improve internal algorithm for ZoomGesture commit fd55468579d05f6a7c3643187b17d3262ec10810 Author: Pavel fljot Date: Tue Jul 10 17:11:30 2012 +0300 Improve internal algorithm for RotateGesture commit 0555813e2596c8bb14c6a3566d2171d23d8d3a0e Author: Pavel fljot Date: Mon Jul 9 16:47:09 2012 +0300 Hotfix for gesture state machine validation commit c2d31b743bbe0821d92dd815fc90f2b2b0071f83 Author: Pavel fljot Date: Thu Jul 5 10:17:53 2012 +0300 Fix Stage + Starling gestures simultaneous recognition commit 37f6220eb53761ed2916acdf4ec244929606dc24 Author: Pavel fljot Date: Wed Jul 4 23:44:16 2012 +0300 Changed default PanGesture#maxNumTouchesRequired to uint.MAX_VALUE commit 2e02b13581a68ba508cb4b80cb738c787a678020 Author: Pavel fljot Date: Wed Jul 4 23:43:01 2012 +0300 Improved early failing strategy implementation as in iOS UIGestureRecognizers. Also fixes bugs with new validating state machine. commit 6273cc33e693cea2b9fc0b0bd2b8ff3965bde67f Author: Pavel fljot Date: Wed Jul 4 21:55:34 2012 +0300 Bumped version to 0.4-beta commit 00040bb2e23a578d9f42de03cafb18a81ee48bcc Merge: 62fa492 963c660 Author: Pavel fljot Date: Wed Jul 4 21:41:26 2012 +0300 Merge branch 'refs/heads/features/starling' into develop commit 963c66024ef81cd9f3354089b9ef6d2b11008387 Author: Pavel fljot Date: Wed Jul 4 21:40:55 2012 +0300 Update README commit 2d52729f7c7d273e028feb6de4854a9bb3c3c0f6 Author: Pavel fljot Date: Wed Jul 4 21:36:55 2012 +0300 Update "requireGestureToFail" API implementation commit 60d9cb6744e498d4986b01373bd30f8c56dbbfea Author: Pavel fljot Date: Wed Jul 4 21:34:49 2012 +0300 Fixed potential bug with registering gesture target in case when previous gesture target was GC-ed, but gesture instance was reused to set a new target commit bbfb3fc34c9a49978110b4c2150b0a657c14c197 Author: Pavel fljot Date: Tue Jul 3 23:23:48 2012 +0300 Minor performance improvement via "use namespace" access commit 193332b9d0ce7e818072b9a7411f983b40106cee Author: Pavel fljot Date: Tue Jul 3 23:12:38 2012 +0300 Change GestureState to "real" enum commit 039a7d79f41a6c9236cd288db98ea83b1805edc7 Author: Pavel fljot Date: Mon Jul 2 22:33:50 2012 +0300 Fixed and slightly improved gesture reset commit 09c35a5d979b9ad0ef1b8c67846aa6635c1a3411 Author: Pavel fljot Date: Mon Jul 2 18:29:33 2012 +0300 Minor tabs and spaces cleanup commit cdd90d747932e978d92ce7bb6255064b6e725d43 Author: Pavel fljot Date: Mon Jul 2 18:28:30 2012 +0300 Moved IDisplayListAdapter creating and retrieving logic to Gestouch class commit 32e9ff979c543a8636eeede607eadb64c85a8233 Author: Pavel fljot Date: Sun Jul 1 12:57:31 2012 +0300 Update to always include Stage in hierarchy so that gestures registered on Stage will always react on touch commit d072f6e478b8740793228110c6da5d1950a88cfe Author: Pavel fljot Date: Sun Jul 1 11:48:10 2012 +0300 Fix for potential RTE related to "contains" logic commit 33108a1bc761f5ff38af68641593a34a4c25b3d4 Author: Pavel fljot Date: Fri Jun 1 13:51:15 2012 +0300 Changed global system gesture slop to variable commit 8d870f4e93e9879fd060ccddb1ae6229083ca114 Author: Pavel fljot Date: Fri Jun 1 13:49:29 2012 +0300 Improved event handling autocompletion (for FD) commit 54cc5d42bd8fadfac7111d6ca07ac9a9c4aad313 Merge: e9132fe 62fa492 Author: Pavel fljot Date: Wed May 30 20:54:47 2012 +0300 Merge branch 'refs/heads/develop' into features/starling commit e9132fec9bd6c03d02999c8fc81996818c307ed3 Author: Pavel fljot Date: Tue May 29 17:03:16 2012 +0300 Massive refactoring of input layer commit e0a892654dd019c399316e489a6484f93410c329 Author: Pavel fljot Date: Tue May 29 17:01:54 2012 +0300 Untyped globalToLocal call fix commit 0adfac4c5d0227967c8dd64a4de9f51033867cf6 Author: Pavel fljot Date: Fri May 4 14:54:25 2012 +0300 Fixed touch/mouse event handling condition in StarlingInputAdapter commit 62fa492d663c45972e6f232c9dc2a4daa4328fd0 Author: Pavel fljot Date: Mon Apr 2 17:17:48 2012 +0300 Fixed initial offset calculation for PanGesture commit 696b6367f2e12ff7e3efc9b6153db89409dcf4d7 Author: Pavel fljot Date: Mon Apr 2 17:16:48 2012 +0300 Fixed location for TapGesture commit 86439e76273dcb1c1329b88f5e5ddfda43cff24e Author: Pavel fljot Date: Sun Mar 18 12:19:43 2012 +0200 README update for simplified API commit d1150b2a35bf6cd4a40cf527e9984602d9efd886 Author: Pavel fljot Date: Sun Mar 18 12:17:01 2012 +0200 Simplified API for Gesture target commit 63a5df87618fdc6dc9f97a14b3ff0b2964dc6ba1 Author: Pavel fljot Date: Fri Mar 16 01:04:50 2012 +0200 Bumped version to 0.4-alpha commit c983ebe32aabd6116764ac87c9739cf45ef1f696 Author: Pavel fljot Date: Fri Mar 16 01:03:40 2012 +0200 Readme quick update for Starling commit 13dbd61014447b28b47cd8cefa5270f4f435ff4f Author: Pavel fljot Date: Tue Mar 13 14:05:50 2012 +0200 starling initial commit commit e15c7e731827ae1172a75105f62b5eee8f8f938a Author: Pavel fljot Date: Wed Mar 14 18:36:05 2012 +0200 Bumped version to 0.3.1 Releasing bunch of fixes commit ffb8ce1ec346a9b33946fed6e273cfd2ea68115e Author: Pavel fljot Date: Wed Mar 14 18:00:38 2012 +0200 Proper state changing on calling Gesture#reset() commit 13231a4708f10c749b85eb1e5d36866161c7de2f Author: Pavel fljot Date: Wed Mar 14 17:49:49 2012 +0200 Fix for LongPressGesture to work correctly with minPressDuration of zero (and the new IDLE state) commit a449965e39256fcb1bd48328681006bd8bb01cd1 Author: Pavel fljot Date: Wed Mar 14 17:34:24 2012 +0200 Fix to output GestureStateEvent#toString() propely commit bcb3dfb61ff2cb0c38aaa37fcfbe4a05a77ddb63 Author: Pavel fljot Date: Wed Mar 14 16:58:39 2012 +0200 Removed some redundant code commit 82742a84655a4bc4e67f953e088cd56623c67767 Author: Pavel fljot Date: Wed Mar 14 15:32:51 2012 +0200 Made Gesture weak-referencing target commit e14bbd11bbe3bae3653914aedb4ef0b43f063962 Author: Pavel fljot Date: Thu Mar 8 13:40:04 2012 +0200 Input adapters fix to catch events on empty stage commit 97486ba2fe984549fe6b9cc078387bbe0e42b8cc Author: Pavel fljot Date: Wed Mar 7 01:12:50 2012 +0200 Bumped version to 0.3 commit 764ca1522f48d8230889e25a991d6fbb30c2ebd2 Author: Pavel fljot Date: Wed Mar 7 01:05:04 2012 +0200 Readme updates commit 2efa95b85c7b580db3640723a7f48c4b8f239b8b Author: Pavel fljot Date: Tue Mar 6 23:28:12 2012 +0200 Experimental requireGestureToFail API implemented commit 4e02d4ae63dddcbaccdde8cb4d4d918706497bdc Author: Pavel fljot Date: Tue Mar 6 23:27:39 2012 +0200 Reformat condition commit 7cdec34be4427a06fa7acfdb1393cd0fab492a35 Author: Pavel fljot Date: Tue Mar 6 23:17:23 2012 +0200 Swipe gesture algorithm rewritten for better recognition and failing commit 9edcd048781582850a589d66bf3d49c2f5aa7566 Author: Pavel fljot Date: Tue Mar 6 12:00:38 2012 +0200 Tiny cleanup commit b92205784589ce7ec9efa108e393173bf2d23a0a Author: Pavel fljot Date: Tue Mar 6 01:22:42 2012 +0200 New gesture for free transformation more precise and performant then combination of 3 commit 5f28227c75b0c937f239dfa725bb190183eabd17 Author: Pavel fljot Date: Tue Mar 6 01:12:46 2012 +0200 Using custom GestureEvent and TransformGestureEvent from now on commit 06df91ce04f4ed3bb6710f8538b33856733db9fb Author: Pavel fljot Date: Tue Mar 6 00:52:50 2012 +0200 Custom GestureEvent and TransformGestureEvent because native one have useless phase and stupid constants commit 398e41f610814539d3757ccafefe60896ab732a6 Author: Pavel fljot Date: Tue Mar 6 00:51:25 2012 +0200 TouchesManager should not clone touches because they must persist during touch session. also good for performance. commit 49b1139b4f3717a82651b699d0a215e21a1d520f Author: Pavel fljot Date: Tue Mar 6 00:50:34 2012 +0200 Touch properties update commit 242966790a281b89acb2e68a34549c61ac8aede2 Author: Pavel fljot Date: Sat Mar 3 17:41:29 2012 +0200 New gesture state commit 4d5bef0252291f6c77a2f2726636547bac7fda6d Author: Pavel fljot Date: Fri Mar 2 20:33:16 2012 +0200 Touch properties updates (and corresponding gestures fixes) commit b56107e059974878b1822fdf6717b232a40498e3 Author: Pavel fljot Date: Thu Mar 1 23:56:45 2012 +0200 Minor cleanup for Gesture class commit 51e8435b2dfc2eb9ff4fb6d3e4cedb663feabc67 Author: Pavel fljot Date: Thu Mar 1 18:12:35 2012 +0200 Input adapters initialization and disposing commit 895e662bd54bdff630c3cc4a2ccf1a2e18274ab8 Author: Pavel fljot Date: Wed Feb 29 22:20:09 2012 +0200 Minor performance fix for SwipeGesture commit 3dcc78c2674c1878f4a1ad0b1b9cd54d2c0fa8d1 Author: Pavel fljot Date: Wed Feb 29 13:54:59 2012 +0200 Added direction for PanGesture commit 09dc1ddfc42a39cb892df06faf371df344cbc7fc Author: Pavel fljot Date: Tue Feb 21 20:14:13 2012 +0200 Touch#time fix (affects SwipeGesture) commit 0532f4bfbbba9cb66fd00add2a48f6a0fa9a2fcd Author: Pavel fljot Date: Mon Feb 20 17:43:43 2012 +0200 Moved some IGesturesManager methods under gestouch_internal namespace commit 3ba8a3df861baf03c4798bcf4c504a9cc376ec52 Author: Pavel fljot Date: Fri Feb 17 17:53:15 2012 +0200 Put back automatic input adapter initialization commit 6d8b733d51bb4cc90dc71065c5a4853fd39fd302 Author: Pavel fljot Date: Fri Feb 3 15:57:11 2012 +0200 Moved input logic out to separate classes commit ad767a4937f853d028097a1620ed517a14f34fbe Author: Pavel fljot Date: Thu Feb 2 16:57:16 2012 +0200 Bugfix for mouse/finger release out of stage commit 47f2f848e43518f4ce61158df2b5c42cacb8d036 Author: Pavel fljot Date: Fri Dec 30 18:26:16 2011 +0200 Optimized event dispatching commit 3e1b5948b2d898da78baa1e3a0f6ea26c33d3805 Author: Pavel fljot Date: Fri Dec 30 16:47:52 2011 +0200 Small optimization for PanGesture commit d950550d160becb703f39d726d452e3f14f5d5e8 Author: Pavel fljot Date: Fri Dec 30 03:19:56 2011 +0200 Catch all the Touch/Mouse events in capture phase GesturesManager now captures TOUCH_BEGIN or MOUSE_DOWN from the stage in capture phase to be able to prevent their propagation at the target without affecting the gesture regoznition. commit 9d9fcd20bad796c5ab136b5b010616ed63038695 Author: Pavel fljot Date: Tue Nov 22 10:40:42 2011 +0200 Fix central point calculation for more precise transformations commit a036db1aef59660cc46503a9bfaef2acd80d9597 Author: Pavel fljot Date: Tue Nov 22 02:54:11 2011 +0200 Initial commit for the new architecture commit 9144538e46378e8a4c7ea560484e0f44530642dd Author: Pavel fljot Date: Tue Nov 1 14:10:05 2011 +0200 Added Gesture#enabled property commit d3ddb825b5dc6fca959c4a1273223e70347b6e7e Author: Pavel fljot Date: Tue Oct 25 15:19:05 2011 +0300 Fix condition for dispatching GestureTrackingEvent.GESTURE_TRACKING_END commit fbc4ab7422cd32f125416d87843a7c96646c8108 Merge: dc489ba e508862 Author: Pavel fljot Date: Tue Oct 25 13:47:51 2011 +0300 Merge branch 'refs/heads/master' into develop --- .settings/com.powerflasher.fdt.classpath | 2 + README.textile | 46 ++- build.xml | 3 + libs/starling.swc | Bin 0 -> 94776 bytes src/org/gestouch/core/Gestouch.as | 125 +++++++ src/org/gestouch/core/GestureState.as | 101 +++++- src/org/gestouch/core/GesturesManager.as | 308 ++++++++---------- src/org/gestouch/core/IDisplayListAdapter.as | 12 + .../gestouch/core/IGestureTargetAdapter.as | 15 + src/org/gestouch/core/IGesturesManager.as | 17 - src/org/gestouch/core/IInputAdapter.as | 12 +- src/org/gestouch/core/ITouchHitTester.as | 14 + src/org/gestouch/core/ITouchesManager.as | 16 - src/org/gestouch/core/Touch.as | 17 +- src/org/gestouch/core/TouchesManager.as | 251 ++++++++++---- src/org/gestouch/events/GestureEvent.as | 6 +- src/org/gestouch/events/GestureStateEvent.as | 8 +- .../gestouch/events/LongPressGestureEvent.as | 4 +- src/org/gestouch/events/PanGestureEvent.as | 4 +- src/org/gestouch/events/RotateGestureEvent.as | 4 +- src/org/gestouch/events/SwipeGestureEvent.as | 4 +- src/org/gestouch/events/TapGestureEvent.as | 5 +- .../gestouch/events/TransformGestureEvent.as | 4 +- src/org/gestouch/events/ZoomGestureEvent.as | 4 +- .../native/NativeDisplayListAdapter.as | 104 ++++++ .../extensions/native/NativeTouchHitTester.as | 19 ++ .../starling/StarlingDisplayListAdapter.as | 77 +++++ .../starling/StarlingTouchHitTester.as | 36 ++ .../extensions/starling/StarlingUtils.as | 38 +++ src/org/gestouch/gestures/Gesture.as | 227 +++++++++---- src/org/gestouch/gestures/LongPressGesture.as | 40 +-- src/org/gestouch/gestures/PanGesture.as | 28 +- src/org/gestouch/gestures/RotateGesture.as | 75 +++-- src/org/gestouch/gestures/SwipeGesture.as | 198 +++++++---- src/org/gestouch/gestures/TapGesture.as | 31 +- src/org/gestouch/gestures/TransformGesture.as | 104 +++--- src/org/gestouch/gestures/ZoomGesture.as | 104 +++--- .../gestouch/input/AbstractInputAdapter.as | 51 --- src/org/gestouch/input/MouseInputAdapter.as | 136 -------- src/org/gestouch/input/NativeInputAdapter.as | 224 +++++++++++++ src/org/gestouch/input/TUIOInputAdapter.as | 28 +- src/org/gestouch/input/TouchInputAdapter.as | 186 ----------- src/org/gestouch/utils/GestureUtils.as | 2 + version.properties | 2 +- 44 files changed, 1705 insertions(+), 987 deletions(-) create mode 100644 libs/starling.swc create mode 100644 src/org/gestouch/core/Gestouch.as create mode 100644 src/org/gestouch/core/IDisplayListAdapter.as create mode 100644 src/org/gestouch/core/IGestureTargetAdapter.as delete mode 100644 src/org/gestouch/core/IGesturesManager.as create mode 100644 src/org/gestouch/core/ITouchHitTester.as delete mode 100644 src/org/gestouch/core/ITouchesManager.as create mode 100644 src/org/gestouch/extensions/native/NativeDisplayListAdapter.as create mode 100644 src/org/gestouch/extensions/native/NativeTouchHitTester.as create mode 100644 src/org/gestouch/extensions/starling/StarlingDisplayListAdapter.as create mode 100644 src/org/gestouch/extensions/starling/StarlingTouchHitTester.as create mode 100644 src/org/gestouch/extensions/starling/StarlingUtils.as delete mode 100644 src/org/gestouch/input/AbstractInputAdapter.as delete mode 100644 src/org/gestouch/input/MouseInputAdapter.as create mode 100644 src/org/gestouch/input/NativeInputAdapter.as delete mode 100644 src/org/gestouch/input/TouchInputAdapter.as diff --git a/.settings/com.powerflasher.fdt.classpath b/.settings/com.powerflasher.fdt.classpath index 75e92cc..9292307 100644 --- a/.settings/com.powerflasher.fdt.classpath +++ b/.settings/com.powerflasher.fdt.classpath @@ -1,5 +1,6 @@ + libs src frameworks/libs/air/airglobal.swc frameworks/libs/mobile/mobilecomponents.swc @@ -10,4 +11,5 @@ frameworks/libs/air/servicemonitor.swc frameworks/themes/Mobile/mobile.swc frameworks/libs/mx/mx.swc + libs/starling.swc diff --git a/README.textile b/README.textile index 9076e0a..70152a2 100644 --- a/README.textile +++ b/README.textile @@ -1,6 +1,6 @@ -h1. Gestouch: NUI gestures detection framework for mouse, touch and multitouch AS3 development. +h1. Gestouch: multitouch gesture recognition library for Flash (ActionScript) development. -Gestouch is a ActionScript library/framework that helps you to deal with single- and multitouch gestures for building better NUI (Natural User Interface). +Gestouch is a ActionScript (AS3) library that helps you to deal with single- and multitouch gestures for building better NUI (Natural User Interface). h3. Why? There's already gesture support in Flash/AIR! @@ -13,9 +13,9 @@ _Upd:_ With "native way" you also won't get anything out of Stage3D and of custo h3. What Gestouch does in short? Well basically there's 3 distinctive tasks to solve. -# To provide various input. It can be standard MouseEvents, TouchEvents or more complex things like custom input via TUIO protocol for your hand-made installation. So what we get here is Touches (touch points). -# To recognize gesture out of touch points. Each type of Gesture has it's own inner algorithms that ... -# To manage gestures relations. Because they may "overlap" and once some has been recognized probably we don't want other to do so. +# To provide various input. It can be native MouseEvents, TouchEvents or more complex things like custom input via TUIO protocol for your hand-made installation. So what we get here is Touches (touch points). +# To recognize gesture analyzing touches. Each type of Gesture has it's own inner algorithms that ... +# To manage gestures conflicts. As multiple gestures may be recognized simultaneously, we need to be able to control whether it's allowed or some of them should not be recognized (fail). Gestouch solves these 3 tasks. I was hardly inspired by Apple team, how they solved this (quite recently to my big surprise! I thought they had it right from the beginning) in they Cocoa-touch UIKit framework. Gestouch is very similar in many ways. But I wouldn't call it "direct port" because 1) the whole architecture was implemented based just on conference videos and user documentation 2) flash platform is a different platform with own specialization, needs, etc. @@ -23,12 +23,11 @@ So I want Gestouch to go far beyond that. Features: * Pretty neat architecture! Very similar to Apple's UIGestureRecognizers (Cocoa-Touch UIKit) +* Works with any display list hierarchy structures: native DisplayList (pure AS3/Flex/your UI framework), Starling or ND2D (Stage3D) and 3D libs... * Doesn't require any additional software (may use runtime's build-in touch support) * Works across all platforms (where Flash Player or AIR run of course) in exactly same way -* Doesn't break your DisplayList architecture (could be easily used for Flex development) * Extendable. You can write your own application-specific gestures * Open-source and free -* *_+Planning to make it work with Stage3D. Hello Starling!+_* @@ -69,9 +68,40 @@ private function onFreeTransform(event:TransformGestureEvent):void +h3. Advanced usage: Starling, ... + +Recent changes made it possible to work with "Starling":http://www.starling-framework.org display list objects as well as any other display list hierarchical structures, e.g. other Stage3D frameworks that have display objects hierarchy like "ND2D":https://github.com/nulldesign/nd2d or even 3D libraries. +In order to use Gestouch with Starling do the following: +
starling = new Starling(MyStarlingRootClass, stage);
+/* setup & start your Starling instance here */
+
+// Gestouch initialization step 1 of 3:
+// Initialize native (default) input adapter. Needed for non-DisplayList usage.
+Gestouch.inputAdapter ||= new NativeInputAdapter(stage);
+
+// Gestouch initialization step 2 of 3:
+// Register instance of StarlingDisplayListAdapter to be used for objects of type starling.display.DisplayObject.
+// What it does: helps to build hierarchy (chain of parents) for any Starling display object and
+// acts as a adapter for gesture target to provide strong-typed access to methods like globalToLocal() and contains().
+Gestouch.addDisplayListAdapter(starling.display.DisplayObject, new StarlingDisplayListAdapter());
+
+// Gestouch initialization step 3 of 3:
+// Initialize and register StarlingTouchHitTester.
+// What it does: finds appropriate target for the new touches (uses Starling Stage#hitTest() method)
+// What does "-1" mean: priority for this hit-tester. Since Stage3D layer sits behind native DisplayList
+// we give it lower priority in the sense of interactivity.
+Gestouch.addTouchHitTester(new StarlingTouchHitTester(starling), -1);
+// NB! Use Gestouch#removeTouchHitTester() method if you manage multiple Starling instances during
+// your application lifetime.
+
+ +Now you can register gesture in familiar, exactly same way: +
var tap:TapGesture = new TapGesture(starlingSprite);
+ + + h3. Roadmap, TODOs -* *Stage3D support.* Hello Starling! Must move away from target as InteractiveObject to some abstract adapters. * "Massive gestures" & Clusters. For bigger form-factor multitouch usage, when gestures must be a bit less about separate fingers but rather touch clusters (massive multitouch) * -Simulator (for testing multitouch gestures without special devices)- With new architecture it must be relatively easy to create SimulatorInputAdapter * Chained gestures concept? To transfer touches from one gesture to another. Example: press/hold for circular menu, then drag it around. diff --git a/build.xml b/build.xml index a094265..63664da 100644 --- a/build.xml +++ b/build.xml @@ -21,6 +21,8 @@ + + @@ -44,6 +46,7 @@ + diff --git a/libs/starling.swc b/libs/starling.swc new file mode 100644 index 0000000000000000000000000000000000000000..6fc810d696c6788883ce1ff047fa1f6e5a8f9bb5 GIT binary patch literal 94776 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

$w*hcE55x-efr}3kL(}s zDEJ%LLTE}yU$E=^f@XaQr!=tV z$bO)K%~K7Cj{I5r32Qh%-h#<_1Tgz;;#lQy{EFaM0FIlSOa5J(Bg*7h0f;v{#9tG{ z^tG*ah`(+VqfCzFgLsQW{4GJe1H?5B@gLa4D3fD5K)lr<{*EAC2jXq?IQns6-xIrb zMRvz`KZ5>xKt#^9ZjYRZ+!Vhl6geJ0E;8Y>Re>W?V?R_ZRq&29Vr7keN&zBN z_nPt={&HX^ZU23dT|?rRI)6nCd4?ZMmaSqJ>N4rxp`2VR-j^+5U8*xqHRZY#=2r?E zmnzIt+^$1&Zc4|JOv{D!eJiS&$b_M#c?}i}*D#}4E~M%bqA6+vNeg2$OV!K>%}QE0 zh+ua)qhn>Ie5w;x>1lFgb6Bx)TOsYJ-mlb|t=uO|W5)`ep9gX=!M8gDb=tn^tK6mr z=M+N#c3mN(HDP=Lf4dBcs_3HSN>ih|udlyvsZvx*6@B8!>7hfXj}CS3?O#n4>p2pg zIujF`@VtSkbASmPwRN7NT}{{9;m+lB&yn=nBf3R6&}BntF8W6P%CKwbo=f9pU!$`! z&f>Op13%J#;L!2@zJOhx4j(ymKwfL>^)@<#ZTFmK7+`u+qLryQyVo!|NRPJ+P8g=w z-D^(Ybk7M1@pV_{oY!-Klp>~7b~r{A*M0d87kSOo6Ee2&Qk#s@gHVY(YH%pM=f=jN z-FDQ_*IgA91L;9viqX+1izb+dz|XQ#7EQj{yP{_WDf+Q{E{(KzjS-T08e!WB7Tt5# z-J_Z3chBWe5D(~l97uTqnRoqdfw6>h2`T)WRTTMIBN8)R&=+L>NDVHR-QBZkhq!BW zhI2xu1yF_=Oxk2@H3OcsvzK&(T;`EXzNxmdUBWe`ja&4|@u{2i$>@1x<5MST>!fY) z;XcrP)9K^g`;YY-GvjB*=Ah)6o130&yxyl3cU2nDtYlte3a!;mvN-8zX42Xi8_(WR zbzS$%-d2q*wSln>6J&AHlZIAG_nS18xB^qD{(3JKqaR>QS2LrLMludw|JI(F>FDSP z%-G8DlnM}kU>}uBl}gbuE=$>&^v{6^?)9TWzssj)eG$-7u@Z)t7J%6$6)Kk#g8d%RiMJ`W-& z*_+HYsf9ebNf{8f?_eqd2xxxc?stL6e{9;1Lu|8vyr>2cn)xcsyPRs zPz~o>xf|#}wVXF_-pF|q=PjJKa^A-I4uTBw?B=|O^8wCp;GTn=ALgzbIUnNcO`M

y_fU* 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%0DrGFom95F$@_?>@yhh!cGWz=9t_ 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

nI11M&x3u5^>(od09Mf*?_ihF2ZS*kfFg#v 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 S|G5?9XFC1-YaM}qzW)bu`ma0y literal 0 HcmV?d00001 diff --git a/src/org/gestouch/core/Gestouch.as b/src/org/gestouch/core/Gestouch.as new file mode 100644 index 0000000..718f811 --- /dev/null +++ b/src/org/gestouch/core/Gestouch.as @@ -0,0 +1,125 @@ +package org.gestouch.core +{ + import org.gestouch.extensions.native.NativeDisplayListAdapter; + import org.gestouch.extensions.native.NativeTouchHitTester; + + import flash.display.DisplayObject; + import flash.utils.Dictionary; + import flash.utils.getQualifiedClassName; + + + /** + * @author Pavel fljot + */ + final public class Gestouch + { + private static const _displayListAdaptersMap:Dictionary = new Dictionary(); + + { + initClass(); + } + + + /** @private */ + private static var _inputAdapter:IInputAdapter; + + /** + * + */ + public static function get inputAdapter():IInputAdapter + { + return _inputAdapter; + } + public static function set inputAdapter(value:IInputAdapter):void + { + if (_inputAdapter == value) + return; + + _inputAdapter = value; + if (inputAdapter) + { + inputAdapter.touchesManager = touchesManager; + inputAdapter.init(); + } + } + + + private static var _touchesManager:TouchesManager; + /** + * + */ + public static function get touchesManager():TouchesManager + { + return _touchesManager ||= new TouchesManager(gesturesManager); + } + + + private static var _gesturesManager:GesturesManager; + public static function get gesturesManager():GesturesManager + { + return _gesturesManager ||= new GesturesManager(); + } + + + public static function addDisplayListAdapter(targetClass:Class, adapter:IDisplayListAdapter):void + { + if (!targetClass || !adapter) + { + throw new Error("Argument error: both arguments required."); + } + + _displayListAdaptersMap[targetClass] = adapter; + } + + + public static function addTouchHitTester(hitTester:ITouchHitTester, priority:int = 0):void + { + touchesManager.gestouch_internal::addTouchHitTester(hitTester, priority); + } + + + public static function removeTouchHitTester(hitTester:ITouchHitTester):void + { + touchesManager.gestouch_internal::removeInputAdapter(hitTester); + } + + +// public static function getTouches(target:Object = null):Array +// { +// return touchesManager.getTouches(target); +// } + + gestouch_internal static function createGestureTargetAdapter(target:Object):IDisplayListAdapter + { + const adapter:IDisplayListAdapter = Gestouch.gestouch_internal::getDisplayListAdapter(target); + if (adapter) + { + return new (adapter.reflect())(target); + } + + throw new Error("Cannot create adapter for target " + target + " of type " + getQualifiedClassName(target) + "."); + } + + + gestouch_internal static function getDisplayListAdapter(object:Object):IDisplayListAdapter + { + for (var key:Object in _displayListAdaptersMap) + { + var targetClass:Class = key as Class; + if (object is targetClass) + { + return _displayListAdaptersMap[key] as IDisplayListAdapter; + } + } + + return null; + } + + + private static function initClass():void + { + addTouchHitTester(new NativeTouchHitTester()); + addDisplayListAdapter(DisplayObject, new NativeDisplayListAdapter()); + } + } +} diff --git a/src/org/gestouch/core/GestureState.as b/src/org/gestouch/core/GestureState.as index 07c9e75..eca6adc 100644 --- a/src/org/gestouch/core/GestureState.as +++ b/src/org/gestouch/core/GestureState.as @@ -1,17 +1,100 @@ package org.gestouch.core { + import flash.errors.IllegalOperationError; + + /** * @author Pavel fljot */ public class GestureState { - public static const IDLE:uint = 1 << 0;//1 - public static const POSSIBLE:uint = 1 << 1;//2 - public static const RECOGNIZED:uint = 1 << 2;//4 - public static const BEGAN:uint = 1 << 3;//8 - public static const CHANGED:uint = 1 << 4;//16 - public static const ENDED:uint = 1 << 5;//32 - public static const CANCELLED:uint = 1 << 6;//64 - public static const FAILED:uint = 1 << 7;//128 + public static const IDLE:GestureState = new GestureState(1 << 0, "IDLE"); + public static const POSSIBLE:GestureState = new GestureState(1 << 1, "POSSIBLE"); + public static const RECOGNIZED:GestureState = new GestureState(1 << 2, "RECOGNIZED"); + public static const BEGAN:GestureState = new GestureState(1 << 3, "BEGAN"); + public static const CHANGED:GestureState = new GestureState(1 << 4, "CHANGED"); + public static const ENDED:GestureState = new GestureState(1 << 5, "ENDED"); + public static const CANCELLED:GestureState = new GestureState(1 << 6, "CANCELLED"); + public static const FAILED:GestureState = new GestureState(1 << 7, "FAILED"); + + private static const endStatesBitMask:uint = + GestureState.CANCELLED.toUint() | + GestureState.RECOGNIZED.toUint() | + GestureState.ENDED.toUint() | + GestureState.FAILED.toUint(); + + private static var allStatesInitialized:Boolean; + + + private var value:uint; + private var name:String; + private var validTransitionsBitMask:uint; + + { + _initClass(); + } + + + public function GestureState(value:uint, name:String) + { + if (allStatesInitialized) + { + throw new IllegalOperationError("You cannot create gesture states." + + "Use predefined constats like GestureState.RECOGNIZED"); + } + + this.value = value; + this.name = name; + } + + + private static function _initClass():void + { + IDLE.setValidNextStates(POSSIBLE); + POSSIBLE.setValidNextStates(RECOGNIZED, BEGAN, FAILED); + RECOGNIZED.setValidNextStates(IDLE); + BEGAN.setValidNextStates(CHANGED, ENDED, CANCELLED); + CHANGED.setValidNextStates(CHANGED, ENDED, CANCELLED); + ENDED.setValidNextStates(IDLE); + FAILED.setValidNextStates(IDLE); + CANCELLED.setValidNextStates(IDLE); + + allStatesInitialized = true; + } + + + public function toString():String + { + return "GestureState." + name; + } + + + public function toUint():uint + { + return value; + } + + + private function setValidNextStates(...states):void + { + var mask:uint; + for each (var state:GestureState in states) + { + mask = mask | state.value; + } + validTransitionsBitMask = mask; + } + + + gestouch_internal function canTransitionTo(state:GestureState):Boolean + { + return (validTransitionsBitMask & state.value) > 0; + } + + + gestouch_internal function get isEndState():Boolean + { + return (endStatesBitMask & value) > 0; + } } -} \ No newline at end of file +} diff --git a/src/org/gestouch/core/GesturesManager.as b/src/org/gestouch/core/GesturesManager.as index 8cae7a2..1820be0 100644 --- a/src/org/gestouch/core/GesturesManager.as +++ b/src/org/gestouch/core/GesturesManager.as @@ -1,117 +1,37 @@ package org.gestouch.core { + import flash.errors.IllegalOperationError; import org.gestouch.gestures.Gesture; - import org.gestouch.input.MouseInputAdapter; - import org.gestouch.input.TouchInputAdapter; + import org.gestouch.input.NativeInputAdapter; import flash.display.DisplayObject; - import flash.display.DisplayObjectContainer; - import flash.display.InteractiveObject; import flash.display.Shape; import flash.display.Stage; import flash.events.Event; - import flash.ui.Multitouch; + import flash.events.IEventDispatcher; import flash.utils.Dictionary; + import flash.utils.getQualifiedClassName; /** * @author Pavel fljot */ - public class GesturesManager implements IGesturesManager + public class GesturesManager { - public static var initDefaultInputAdapter:Boolean = true; - private static var _instance:IGesturesManager; - private static var _allowInstantiation:Boolean; - - protected const _touchesManager:ITouchesManager = TouchesManager.getInstance(); protected const _frameTickerShape:Shape = new Shape(); protected var _inputAdapters:Vector. = new Vector.(); - protected var _stage:Stage; protected var _gesturesMap:Dictionary = new Dictionary(true); - protected var _gesturesForTouchMap:Array = []; + protected var _gesturesForTouchMap:Dictionary = new Dictionary(); protected var _gesturesForTargetMap:Dictionary = new Dictionary(true); - protected var _dirtyGestures:Vector. = new Vector.(); - protected var _dirtyGesturesLength:uint = 0; + protected var _dirtyGesturesCount:uint = 0; protected var _dirtyGesturesMap:Dictionary = new Dictionary(true); + protected var _stage:Stage; + + use namespace gestouch_internal; public function GesturesManager() { - if (Object(this).constructor == GesturesManager && !_allowInstantiation) - { - throw new Error("Do not instantiate GesturesManager directly."); - } - } - - - public function get inputAdapters():Vector. - { - return _inputAdapters.concat(); - } - - - public static function setImplementation(value:IGesturesManager):void - { - if (!value) - { - throw new ArgumentError("value cannot be null."); - } - if (_instance) - { - throw new Error("Instance of GesturesManager is already created. If you want to have own implementation of single GesturesManager instace, you should set it earlier."); - } - _instance = value; - } - - - public static function getInstance():IGesturesManager - { - if (!_instance) - { - _allowInstantiation = true; - _instance = new GesturesManager(); - _allowInstantiation = false; - } - - return _instance; - } - - - - public function addInputAdapter(inputAdapter:IInputAdapter):void - { - if (!inputAdapter) - { - throw new Error("Input adapter must be non null."); - } - - if (_inputAdapters.indexOf(inputAdapter) > -1) - return;//TODO: throw Error or ignore? - - _inputAdapters.push(inputAdapter); - inputAdapter.touchesManager = _touchesManager; - inputAdapter.gesturesManager = this; - inputAdapter.init(); - } - - - public function removeInputAdapter(inputAdapter:IInputAdapter, dispose:Boolean = true):void - { - if (!inputAdapter) - { - throw new Error("Input adapter must be non null."); - } - var index:int = _inputAdapters.indexOf(inputAdapter); - if (index == -1) - { - throw new Error("This input manager is not registered."); - } - - _inputAdapters.splice(index, 1); - if (dispose) - { - inputAdapter.dispose(); - } } @@ -123,29 +43,21 @@ package org.gestouch.core // //-------------------------------------------------------------------------- - protected function installStage(stage:Stage):void + protected function onStageAvailable(stage:Stage):void { _stage = stage; - if (Multitouch.supportsTouchEvents) - { - addInputAdapter(new TouchInputAdapter(stage)); - } - else - { - addInputAdapter(new MouseInputAdapter(stage)); - } + Gestouch.inputAdapter ||= new NativeInputAdapter(stage); } protected function resetDirtyGestures():void { - for each (var gesture:Gesture in _dirtyGestures) + for (var gesture:Object in _dirtyGesturesMap) { - gesture.reset(); + (gesture as Gesture).reset(); } - _dirtyGestures.length = 0; - _dirtyGesturesLength = 0; + _dirtyGesturesCount = 0; _dirtyGesturesMap = new Dictionary(true); _frameTickerShape.removeEventListener(Event.ENTER_FRAME, enterFrameHandler); } @@ -157,30 +69,43 @@ package org.gestouch.core { throw new ArgumentError("Argument 'gesture' must be not null."); } - if (_gesturesMap[gesture]) + + const target:Object = gesture.target; + if (!target) { - throw new Error("This gesture is already registered.. something wrong."); + throw new IllegalOperationError("Gesture must have target."); } - var target:Object = gesture.target; var targetGestures:Vector. = _gesturesForTargetMap[target] as Vector.; - if (!targetGestures) + if (targetGestures) { - targetGestures = _gesturesForTargetMap[gesture.target] = new Vector.(); + if (targetGestures.indexOf(gesture) == -1) + { + targetGestures.push(gesture); + } } - targetGestures.push(gesture); + else + { + targetGestures = _gesturesForTargetMap[target] = new Vector.(); + targetGestures[0] = gesture; + } + _gesturesMap[gesture] = true; - if (GesturesManager.initDefaultInputAdapter) + if (!_stage) { - if (!_stage && gesture.target.stage) + var targetAsDO:DisplayObject = target as DisplayObject; + if (targetAsDO) { - installStage(gesture.target.stage); - } - else - { - gesture.target.addEventListener(Event.ADDED_TO_STAGE, gestureTarget_addedToStageHandler); + if (targetAsDO.stage) + { + onStageAvailable(targetAsDO.stage); + } + else + { + targetAsDO.addEventListener(Event.ADDED_TO_STAGE, gestureTarget_addedToStageHandler); + } } } } @@ -194,21 +119,28 @@ package org.gestouch.core } - var target:InteractiveObject = gesture.target; - var targetGestures:Vector. = _gesturesForTargetMap[target] as Vector.; - if (targetGestures.length > 1) + var target:Object = gesture.target; + // check for target because it could be already GC-ed (since target reference is weak) + if (target) { - targetGestures.splice(targetGestures.indexOf(gesture), 1); - } - else - { - delete _gesturesForTargetMap[target]; - target.removeEventListener(Event.ADDED_TO_STAGE, gestureTarget_addedToStageHandler); + var targetGestures:Vector. = _gesturesForTargetMap[target] as Vector.; + if (targetGestures.length > 1) + { + targetGestures.splice(targetGestures.indexOf(gesture), 1); + } + else + { + delete _gesturesForTargetMap[target]; + if (target is IEventDispatcher) + { + (target as IEventDispatcher).removeEventListener(Event.ADDED_TO_STAGE, gestureTarget_addedToStageHandler); + } + } } delete _gesturesMap[gesture]; - //TODO: decide about gesture state and _dirtyGestures + gesture.reset(); } @@ -216,8 +148,8 @@ package org.gestouch.core { if (!_dirtyGesturesMap[gesture]) { - _dirtyGestures.push(gesture); - _dirtyGesturesLength++; + _dirtyGesturesMap[gesture] = true; + _dirtyGesturesCount++; _frameTickerShape.addEventListener(Event.ENTER_FRAME, enterFrameHandler); } } @@ -225,11 +157,12 @@ package org.gestouch.core gestouch_internal function onGestureRecognized(gesture:Gesture):void { + const target:Object = gesture.target; + for (var key:Object in _gesturesMap) { var otherGesture:Gesture = key as Gesture; - var target:DisplayObject = gesture.target; - var otherTarget:DisplayObject = otherGesture.target; + var otherTarget:Object = otherGesture.target; // conditions for otherGesture "own properties" if (otherGesture != gesture && @@ -237,10 +170,10 @@ package org.gestouch.core otherGesture.enabled && otherGesture.state == GestureState.POSSIBLE) { - // conditions for otherGesture target if (otherTarget == target || - (target is DisplayObjectContainer && (target as DisplayObjectContainer).contains(otherTarget)) || - (otherTarget is DisplayObjectContainer && (otherTarget as DisplayObjectContainer).contains(target))) + gesture.targetAdapter.contains(otherTarget) || + otherGesture.targetAdapter.contains(target) + ) { var gestureDelegate:IGestureDelegate = gesture.delegate; var otherGestureDelegate:IGestureDelegate = otherGesture.delegate; @@ -250,9 +183,9 @@ package org.gestouch.core (!gestureDelegate || !gestureDelegate.gesturesShouldRecognizeSimultaneously(gesture, otherGesture)) && (!otherGestureDelegate || !otherGestureDelegate.gesturesShouldRecognizeSimultaneously(otherGesture, gesture))) { - otherGesture.gestouch_internal::setState_internal(GestureState.FAILED); + otherGesture.setState_internal(GestureState.FAILED); } - } + } } } } @@ -260,32 +193,46 @@ package org.gestouch.core gestouch_internal function onTouchBegin(touch:Touch):void { - if (_dirtyGesturesLength > 0) - { - resetDirtyGestures(); - } - var gesture:Gesture; var i:uint; - // This vector will contain active gestures for specific touch (ID) during all touch session. - var gesturesForTouch:Vector. = _gesturesForTouchMap[touch.id] as Vector.; + // This vector will contain active gestures for specific touch during all touch session. + var gesturesForTouch:Vector. = _gesturesForTouchMap[touch] as Vector.; if (!gesturesForTouch) { - gesturesForTouch = new Vector.(); - _gesturesForTouchMap[touch.id] = gesturesForTouch; + gesturesForTouch = new Vector.(); + _gesturesForTouchMap[touch] = gesturesForTouch; } else { + // touch object may be pooled in the future gesturesForTouch.length = 0; - } + } + var target:Object = touch.target; + const displayListAdapter:IDisplayListAdapter = Gestouch.gestouch_internal::getDisplayListAdapter(target); + if (!displayListAdapter) + { + throw new Error("Display list adapter not found for target of type '" + getQualifiedClassName(target) + "'."); + } + const hierarchy:Vector. = displayListAdapter.getHierarchy(target); + const hierarchyLength:uint = hierarchy.length; + if (hierarchyLength == 0) + { + throw new Error("No hierarchy build for target '" + target +"'. Something is wrong with that IDisplayListAdapter."); + } + if (_stage && !(hierarchy[hierarchyLength - 1] is Stage)) + { + // Looks like some non-native (non DisplayList) hierarchy + // but we must always handle gestures with Stage target + // since Stage is anyway the top-most parent + hierarchy[hierarchyLength] = _stage; + } // Create a sorted(!) list of gestures which are interested in this touch. // Sorting priority: deeper target has higher priority, recently added gesture has higher priority. - var target:InteractiveObject = touch.target; var gesturesForTarget:Vector.; - while (target) + for each (target in hierarchy) { gesturesForTarget = _gesturesForTargetMap[target] as Vector.; if (gesturesForTarget) @@ -293,7 +240,7 @@ package org.gestouch.core i = gesturesForTarget.length; while (i-- > 0) { - gesture = gesturesForTarget[i] as Gesture; + gesture = gesturesForTarget[i]; if (gesture.enabled && (!gesture.delegate || gesture.delegate.gestureShouldReceiveTouch(gesture, touch))) { @@ -302,8 +249,6 @@ package org.gestouch.core } } } - - target = target.parent; } // Then we populate them with this touch and event. @@ -311,11 +256,11 @@ package org.gestouch.core i = gesturesForTouch.length; while (i-- > 0) { - gesture = gesturesForTouch[i] as Gesture; + gesture = gesturesForTouch[i]; // Check for state because previous (i+1) gesture may already abort current (i) one - if (gesture.state != GestureState.FAILED) + if (!_dirtyGesturesMap[gesture]) { - gesture.gestouch_internal::touchBeginHandler(touch); + gesture.touchBeginHandler(touch); } else { @@ -327,21 +272,16 @@ package org.gestouch.core gestouch_internal function onTouchMove(touch:Touch):void { - if (_dirtyGesturesLength > 0) - { - resetDirtyGestures(); - } - - var gesturesForTouch:Vector. = _gesturesForTouchMap[touch.id] as Vector.; + var gesturesForTouch:Vector. = _gesturesForTouchMap[touch] as Vector.; var gesture:Gesture; - var i:int = gesturesForTouch.length; + var i:uint = gesturesForTouch.length; while (i-- > 0) { - gesture = gesturesForTouch[i] as Gesture; + gesture = gesturesForTouch[i]; - if (gesture.state != GestureState.FAILED && gesture.isTrackingTouch(touch.id)) + if (!_dirtyGesturesMap[gesture] && gesture.isTrackingTouch(touch.id)) { - gesture.gestouch_internal::touchMoveHandler(touch); + gesture.touchMoveHandler(touch); } else { @@ -354,29 +294,39 @@ package org.gestouch.core gestouch_internal function onTouchEnd(touch:Touch):void { - if (_dirtyGesturesLength > 0) - { - resetDirtyGestures(); - } - - var gesturesForTouch:Vector. = _gesturesForTouchMap[touch.id] as Vector.; + var gesturesForTouch:Vector. = _gesturesForTouchMap[touch] as Vector.; var gesture:Gesture; - var i:int = gesturesForTouch.length; + var i:uint = gesturesForTouch.length; while (i-- > 0) { - gesture = gesturesForTouch[i] as Gesture; - - if (gesture.state != GestureState.FAILED && gesture.isTrackingTouch(touch.id)) - { - gesture.gestouch_internal::touchEndHandler(touch); + gesture = gesturesForTouch[i]; + + if (!_dirtyGesturesMap[gesture] && gesture.isTrackingTouch(touch.id)) + { + gesture.touchEndHandler(touch); } } + + gesturesForTouch.length = 0;// release for GC } gestouch_internal function onTouchCancel(touch:Touch):void { - //TODO + var gesturesForTouch:Vector. = _gesturesForTouchMap[touch] as Vector.; + var gesture:Gesture; + var i:uint = gesturesForTouch.length; + while (i-- > 0) + { + gesture = gesturesForTouch[i]; + + if (!_dirtyGesturesMap[gesture] && gesture.isTrackingTouch(touch.id)) + { + gesture.touchCancelHandler(touch); + } + } + + gesturesForTouch.length = 0;// release for GC } @@ -386,15 +336,15 @@ package org.gestouch.core // // Event handlers // - //-------------------------------------------------------------------------- + //-------------------------------------------------------------------------- protected function gestureTarget_addedToStageHandler(event:Event):void { var target:DisplayObject = event.target as DisplayObject; target.removeEventListener(Event.ADDED_TO_STAGE, gestureTarget_addedToStageHandler); - if (!_stage && GesturesManager.initDefaultInputAdapter) + if (!_stage) { - installStage(target.stage); + onStageAvailable(target.stage); } } diff --git a/src/org/gestouch/core/IDisplayListAdapter.as b/src/org/gestouch/core/IDisplayListAdapter.as new file mode 100644 index 0000000..d5f69e2 --- /dev/null +++ b/src/org/gestouch/core/IDisplayListAdapter.as @@ -0,0 +1,12 @@ +package org.gestouch.core +{ + /** + * @author Pavel fljot + */ + public interface IDisplayListAdapter extends IGestureTargetAdapter + { + function getHierarchy(target:Object):Vector.; + + function reflect():Class; + } +} \ No newline at end of file diff --git a/src/org/gestouch/core/IGestureTargetAdapter.as b/src/org/gestouch/core/IGestureTargetAdapter.as new file mode 100644 index 0000000..fdf70e5 --- /dev/null +++ b/src/org/gestouch/core/IGestureTargetAdapter.as @@ -0,0 +1,15 @@ +package org.gestouch.core +{ + import flash.geom.Point; + /** + * @author Pavel fljot + */ + public interface IGestureTargetAdapter + { + function get target():Object; + + function globalToLocal(point:Point):Point; + + function contains(object:Object):Boolean; + } +} \ No newline at end of file diff --git a/src/org/gestouch/core/IGesturesManager.as b/src/org/gestouch/core/IGesturesManager.as deleted file mode 100644 index 297f39a..0000000 --- a/src/org/gestouch/core/IGesturesManager.as +++ /dev/null @@ -1,17 +0,0 @@ -package org.gestouch.core -{ - /** - * The class that implements this interface must also - * implement next methods under gestouch_internal namespace: - * - * function addGesture(gesture:Gesture):void; - * function removeGesture(gesture:Gesture):void; - * function scheduleGestureStateReset(gesture:Gesture):void; - * function onGestureRecognized(gesture:Gesture):void; - */ - public interface IGesturesManager - { - function addInputAdapter(inputAdapter:IInputAdapter):void; - function removeInputAdapter(inputAdapter:IInputAdapter, dispose:Boolean = true):void; - } -} \ No newline at end of file diff --git a/src/org/gestouch/core/IInputAdapter.as b/src/org/gestouch/core/IInputAdapter.as index 337ff0e..7d42732 100644 --- a/src/org/gestouch/core/IInputAdapter.as +++ b/src/org/gestouch/core/IInputAdapter.as @@ -5,10 +5,14 @@ package org.gestouch.core */ public interface IInputAdapter { - function set touchesManager(value:ITouchesManager):void; - function set gesturesManager(value:IGesturesManager):void; + /** + * @private + */ + function set touchesManager(value:TouchesManager):void; + /** + * Called when input adapter is set. + */ function init():void; - function dispose():void; } -} \ No newline at end of file +} diff --git a/src/org/gestouch/core/ITouchHitTester.as b/src/org/gestouch/core/ITouchHitTester.as new file mode 100644 index 0000000..b56cbe4 --- /dev/null +++ b/src/org/gestouch/core/ITouchHitTester.as @@ -0,0 +1,14 @@ +package org.gestouch.core +{ + import flash.display.InteractiveObject; + import flash.geom.Point; + + + /** + * @author Pavel fljot + */ + public interface ITouchHitTester + { + function hitTest(point:Point, nativeTarget:InteractiveObject):Object; + } +} diff --git a/src/org/gestouch/core/ITouchesManager.as b/src/org/gestouch/core/ITouchesManager.as deleted file mode 100644 index 8ae9639..0000000 --- a/src/org/gestouch/core/ITouchesManager.as +++ /dev/null @@ -1,16 +0,0 @@ -package org.gestouch.core -{ - /** - * @author Pavel fljot - */ - public interface ITouchesManager - { - function get activeTouchesCount():uint; - - function createTouch():Touch; - function addTouch(touch:Touch):Touch; - function removeTouch(touch:Touch):Touch; - function getTouch(touchPointID:int):Touch; - function hasTouch(touchPointID:int):Boolean; - } -} \ No newline at end of file diff --git a/src/org/gestouch/core/Touch.as b/src/org/gestouch/core/Touch.as index 9a707fa..0679792 100644 --- a/src/org/gestouch/core/Touch.as +++ b/src/org/gestouch/core/Touch.as @@ -1,6 +1,5 @@ package org.gestouch.core { - import flash.display.InteractiveObject; import flash.geom.Point; @@ -19,13 +18,15 @@ package org.gestouch.core /** * The original event target for this touch (touch began with). */ - public var target:InteractiveObject; + public var target:Object; public var sizeX:Number; public var sizeY:Number; public var pressure:Number; // public var lastMove:Point; + + use namespace gestouch_internal; public function Touch(id:uint = 0) @@ -39,13 +40,16 @@ package org.gestouch.core { return _location.clone(); } - gestouch_internal function setLocation(value:Point):void + gestouch_internal function setLocation(x:Number, y:Number, time:uint):void { - _location = value; + _location = new Point(x, y); _beginLocation = _location.clone(); _previousLocation = _location.clone(); + + _time = time; + _beginTime = time; } - gestouch_internal function updateLocation(x:Number, y:Number):void + gestouch_internal function updateLocation(x:Number, y:Number, time:uint):void { if (_location) { @@ -53,10 +57,11 @@ package org.gestouch.core _previousLocation.y = _location.y; _location.x = x; _location.y = y; + _time = time; } else { - gestouch_internal::setLocation(new Point(x, y)); + setLocation(x, y, time); } } diff --git a/src/org/gestouch/core/TouchesManager.as b/src/org/gestouch/core/TouchesManager.as index aa42691..fb8b262 100644 --- a/src/org/gestouch/core/TouchesManager.as +++ b/src/org/gestouch/core/TouchesManager.as @@ -1,22 +1,28 @@ package org.gestouch.core { + import flash.display.InteractiveObject; + import flash.display.Stage; + import flash.geom.Point; + import flash.utils.Dictionary; + import flash.utils.getTimer; + + /** * @author Pavel fljot */ - public class TouchesManager implements ITouchesManager + public class TouchesManager { - private static var _instance:ITouchesManager; - private static var _allowInstantiation:Boolean; - + protected var _gesturesManager:GesturesManager; protected var _touchesMap:Object = {}; + protected var _hitTesters:Vector. = new Vector.(); + protected var _hitTesterPrioritiesMap:Dictionary = new Dictionary(true); + + use namespace gestouch_internal; - public function TouchesManager() + public function TouchesManager(gesturesManager:GesturesManager) { - if (Object(this).constructor == TouchesManager && !_allowInstantiation) - { - throw new Error("Do not instantiate TouchesManager directly."); - } + _gesturesManager = gesturesManager; } @@ -27,77 +33,192 @@ package org.gestouch.core } - public static function setImplementation(value:ITouchesManager):void + public function getTouches(target:Object = null):Array { - if (!value) + const touches:Array = []; + if (!target || target is Stage) { - throw new ArgumentError("value cannot be null."); + // return all touches + var i:uint = 0; + for each (var touch:Touch in _touchesMap) + { + touches[i++] = touch; + } } - if (_instance) + else { - throw new Error("Instance of TouchesManager is already created. If you want to have own implementation of single TouchesManager instace, you should set it earlier."); + //TODO } - _instance = value; - } - - - public static function getInstance():ITouchesManager - { - if (!_instance) - { - _allowInstantiation = true; - _instance = new TouchesManager(); - _allowInstantiation = false; - } - - return _instance; + + return touches; } - public function createTouch():Touch + gestouch_internal function addTouchHitTester(touchHitTester:ITouchHitTester, priority:int = 0):void + { + if (!touchHitTester) + { + throw new ArgumentError("Argument must be non null."); + } + + if (_hitTesters.indexOf(touchHitTester) == -1) + { + _hitTesters.push(touchHitTester); + } + + _hitTesterPrioritiesMap[touchHitTester] = priority; + // Sort hit testers using their priorities + _hitTesters.sort(hitTestersSorter); + } + + + gestouch_internal function removeInputAdapter(touchHitTester:ITouchHitTester):void + { + if (!touchHitTester) + { + throw new ArgumentError("Argument must be non null."); + } + + var index:int = _hitTesters.indexOf(touchHitTester); + if (index == -1) + { + throw new Error("This touchHitTester is not registered."); + } + + _hitTesters.splice(index, 1); + delete _hitTesterPrioritiesMap[touchHitTester]; + } + + + gestouch_internal function onTouchBegin(touchID:uint, x:Number, y:Number, nativeTarget:InteractiveObject = null):Boolean + { + if (touchID in _touchesMap) + return false;// touch with specified ID is already registered and being tracked + + const location:Point = new Point(x, y); + + for each (var registeredTouch:Touch in _touchesMap) + { + // Check if touch at the same location exists. + // In case we listen to both TouchEvents and MouseEvents, one of them will come first + // (right now looks like MouseEvent dispatched first, but who know what Adobe will + // do tomorrow). This check helps to filter out the one comes after. + + // NB! According to the tests with some IR multitouch frame and Windows computer + // TouchEvent comes first, but the following MouseEvent has slightly offset location + // (1px both axis). That is why Point#distance() used instead of Point#equals() + + if (Point.distance(registeredTouch.location, location) < 2) + return false; + } + + const touch:Touch = createTouch(); + touch.id = touchID; + + var target:Object; + var altTarget:Object; + for each (var hitTester:ITouchHitTester in _hitTesters) + { + target = hitTester.hitTest(location, nativeTarget); + if (target) + { + if ((target is Stage)) + { + // NB! Target is flash.display::Stage is a special case. If it is true, we want + // to give a try to a lower-priority (Stage3D) hit-testers. + altTarget = target; + continue; + } + else + { + // We found a target. + break; + } + } + } + if (!target && !altTarget) + { + throw new Error("Not touch target found (hit test)." + + "Something is wrong, at least flash.display::Stage should be found." + + "See Gestouch#addTouchHitTester() and Gestouch#inputAdapter."); + } + + touch.target = target || altTarget; + touch.setLocation(x, y, getTimer()); + + _touchesMap[touchID] = touch; + _activeTouchesCount++; + + _gesturesManager.onTouchBegin(touch); + + return true; + } + + + gestouch_internal function onTouchMove(touchID:uint, x:Number, y:Number):void + { + const touch:Touch = _touchesMap[touchID] as Touch; + if (!touch) + return;// touch with specified ID isn't registered + + touch.updateLocation(x, y, getTimer()); + + _gesturesManager.onTouchMove(touch); + } + + + gestouch_internal function onTouchEnd(touchID:uint, x:Number, y:Number):void + { + const touch:Touch = _touchesMap[touchID] as Touch; + if (!touch) + return;// touch with specified ID isn't registered + + touch.updateLocation(x, y, getTimer()); + + delete _touchesMap[touchID]; + _activeTouchesCount--; + + _gesturesManager.onTouchEnd(touch); + } + + + gestouch_internal function onTouchCancel(touchID:uint, x:Number, y:Number):void + { + const touch:Touch = _touchesMap[touchID] as Touch; + if (!touch) + return;// touch with specified ID isn't registered + + touch.updateLocation(x, y, getTimer()); + + delete _touchesMap[touchID]; + _activeTouchesCount--; + + _gesturesManager.onTouchCancel(touch); + } + + + protected function createTouch():Touch { //TODO: pool return new Touch(); } - public function addTouch(touch:Touch):Touch + /** + * Sorts from higher priority to lower. Items with the same priority keep the order + * of addition, e.g.: + * add(a), add(b), add(c, -1), add(d, 1) will be ordered to + * d, a, b, c + */ + protected function hitTestersSorter(x:ITouchHitTester, y:ITouchHitTester):Number { - if (_touchesMap.hasOwnProperty(touch.id)) - { - throw new Error("Touch with id " + touch.id + " is already registered."); - } + const d:int = int(_hitTesterPrioritiesMap[x]) - int(_hitTesterPrioritiesMap[y]); + if (d > 0) + return -1; + else if (d < 0) + return 1; - _touchesMap[touch.id] = touch; - _activeTouchesCount++; - - return touch; - } - - - public function removeTouch(touch:Touch):Touch - { - if (!_touchesMap.hasOwnProperty(touch.id)) - { - throw new Error("Touch with id " + touch.id + " is not registered."); - } - - delete _touchesMap[touch.id]; - _activeTouchesCount--; - - return touch; - } - - - public function hasTouch(touchPointID:int):Boolean - { - return _touchesMap.hasOwnProperty(touchPointID); - } - - - public function getTouch(touchPointID:int):Touch - { - return _touchesMap[touchPointID] as Touch; + return _hitTesters.indexOf(x) > _hitTesters.indexOf(y) ? 1 : -1; } } -} \ No newline at end of file +} diff --git a/src/org/gestouch/events/GestureEvent.as b/src/org/gestouch/events/GestureEvent.as index 253258d..4dbdd74 100644 --- a/src/org/gestouch/events/GestureEvent.as +++ b/src/org/gestouch/events/GestureEvent.as @@ -1,5 +1,7 @@ package org.gestouch.events { + import org.gestouch.core.GestureState; + import flash.events.Event; @@ -8,7 +10,7 @@ package org.gestouch.events */ public class GestureEvent extends Event { - public var gestureState:uint; + public var gestureState:GestureState; public var stageX:Number; public var stageY:Number; public var localX:Number; @@ -16,7 +18,7 @@ package org.gestouch.events public function GestureEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, - gestureState:uint = 0, + gestureState:GestureState = null, stageX:Number = 0, stageY:Number = 0, localX:Number = 0, localY:Number = 0) { diff --git a/src/org/gestouch/events/GestureStateEvent.as b/src/org/gestouch/events/GestureStateEvent.as index 3791f4a..29e6cea 100644 --- a/src/org/gestouch/events/GestureStateEvent.as +++ b/src/org/gestouch/events/GestureStateEvent.as @@ -1,5 +1,7 @@ package org.gestouch.events { + import org.gestouch.core.GestureState; + import flash.events.Event; @@ -10,11 +12,11 @@ package org.gestouch.events { public static const STATE_CHANGE:String = "stateChange"; - public var newState:uint; - public var oldState:uint; + public var newState:GestureState; + public var oldState:GestureState; - public function GestureStateEvent(type:String, newState:uint, oldState:uint) + public function GestureStateEvent(type:String, newState:GestureState, oldState:GestureState) { super(type, false, false); diff --git a/src/org/gestouch/events/LongPressGestureEvent.as b/src/org/gestouch/events/LongPressGestureEvent.as index 790b979..e880ddf 100644 --- a/src/org/gestouch/events/LongPressGestureEvent.as +++ b/src/org/gestouch/events/LongPressGestureEvent.as @@ -1,5 +1,7 @@ package org.gestouch.events { + import org.gestouch.core.GestureState; + import flash.events.Event; @@ -12,7 +14,7 @@ package org.gestouch.events public function LongPressGestureEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, - gestureState:uint = 0, + gestureState:GestureState = null, stageX:Number = 0, stageY:Number = 0, localX:Number = 0, localY:Number = 0) { diff --git a/src/org/gestouch/events/PanGestureEvent.as b/src/org/gestouch/events/PanGestureEvent.as index ad6bfba..1802800 100644 --- a/src/org/gestouch/events/PanGestureEvent.as +++ b/src/org/gestouch/events/PanGestureEvent.as @@ -1,5 +1,7 @@ package org.gestouch.events { + import org.gestouch.core.GestureState; + import flash.events.Event; @@ -12,7 +14,7 @@ package org.gestouch.events public function PanGestureEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, - gestureState:uint = 0, + gestureState:GestureState = null, stageX:Number = 0, stageY:Number = 0, localX:Number = 0, localY:Number = 0, offsetX:Number = 0, offsetY:Number = 0) diff --git a/src/org/gestouch/events/RotateGestureEvent.as b/src/org/gestouch/events/RotateGestureEvent.as index 44a2074..2d96c4b 100644 --- a/src/org/gestouch/events/RotateGestureEvent.as +++ b/src/org/gestouch/events/RotateGestureEvent.as @@ -1,5 +1,7 @@ package org.gestouch.events { + import org.gestouch.core.GestureState; + import flash.events.Event; @@ -12,7 +14,7 @@ package org.gestouch.events public function RotateGestureEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, - gestureState:uint = 0, + gestureState:GestureState = null, stageX:Number = 0, stageY:Number = 0, localX:Number = 0, localY:Number = 0, rotation:Number = 0) diff --git a/src/org/gestouch/events/SwipeGestureEvent.as b/src/org/gestouch/events/SwipeGestureEvent.as index d591b01..b3ac3b8 100644 --- a/src/org/gestouch/events/SwipeGestureEvent.as +++ b/src/org/gestouch/events/SwipeGestureEvent.as @@ -1,5 +1,7 @@ package org.gestouch.events { + import org.gestouch.core.GestureState; + import flash.events.Event; @@ -12,7 +14,7 @@ package org.gestouch.events public function SwipeGestureEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, - gestureState:uint = 0, + gestureState:GestureState = null, stageX:Number = 0, stageY:Number = 0, localX:Number = 0, localY:Number = 0, offsetX:Number = 0, offsetY:Number = 0) diff --git a/src/org/gestouch/events/TapGestureEvent.as b/src/org/gestouch/events/TapGestureEvent.as index 6539013..3951357 100644 --- a/src/org/gestouch/events/TapGestureEvent.as +++ b/src/org/gestouch/events/TapGestureEvent.as @@ -1,5 +1,7 @@ package org.gestouch.events { + import org.gestouch.core.GestureState; + import flash.events.Event; @@ -12,7 +14,8 @@ package org.gestouch.events public function TapGestureEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, - gestureState:uint = 0, stageX:Number = 0, stageY:Number = 0, + gestureState:GestureState = null, + stageX:Number = 0, stageY:Number = 0, localX:Number = 0, localY:Number = 0) { super(type, bubbles, cancelable, gestureState, stageX, stageY, localX, localY); diff --git a/src/org/gestouch/events/TransformGestureEvent.as b/src/org/gestouch/events/TransformGestureEvent.as index 1af2a5e..49942e0 100644 --- a/src/org/gestouch/events/TransformGestureEvent.as +++ b/src/org/gestouch/events/TransformGestureEvent.as @@ -1,5 +1,7 @@ package org.gestouch.events { + import org.gestouch.core.GestureState; + import flash.events.Event; @@ -18,7 +20,7 @@ package org.gestouch.events public function TransformGestureEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, - gestureState:uint = 0, + gestureState:GestureState = null, stageX:Number = 0, stageY:Number = 0, localX:Number = 0, localY:Number = 0, scaleX:Number = 1.0, scaleY:Number = 1.0, diff --git a/src/org/gestouch/events/ZoomGestureEvent.as b/src/org/gestouch/events/ZoomGestureEvent.as index cd2c09b..6e23f16 100644 --- a/src/org/gestouch/events/ZoomGestureEvent.as +++ b/src/org/gestouch/events/ZoomGestureEvent.as @@ -1,5 +1,7 @@ package org.gestouch.events { + import org.gestouch.core.GestureState; + import flash.events.Event; @@ -12,7 +14,7 @@ package org.gestouch.events public function ZoomGestureEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, - gestureState:uint = 0, + gestureState:GestureState = null, stageX:Number = 0, stageY:Number = 0, localX:Number = 0, localY:Number = 0, scaleX:Number = 1.0, scaleY:Number = 1.0) diff --git a/src/org/gestouch/extensions/native/NativeDisplayListAdapter.as b/src/org/gestouch/extensions/native/NativeDisplayListAdapter.as new file mode 100644 index 0000000..1498c90 --- /dev/null +++ b/src/org/gestouch/extensions/native/NativeDisplayListAdapter.as @@ -0,0 +1,104 @@ +package org.gestouch.extensions.native +{ + import flash.display.DisplayObject; + import flash.display.DisplayObjectContainer; + import flash.display.Stage; + import flash.geom.Point; + import flash.utils.Dictionary; + import org.gestouch.core.IDisplayListAdapter; + + + /** + * @author Pavel fljot + */ + final public class NativeDisplayListAdapter implements IDisplayListAdapter + { + private var _targetWeekStorage:Dictionary; + + + public function NativeDisplayListAdapter(target:DisplayObject = null) + { + if (target) + { + _targetWeekStorage = new Dictionary(true); + _targetWeekStorage[target] = true; + } + } + + + public function get target():Object + { + for (var key:Object in _targetWeekStorage) + { + return key; + } + return null; + } + + + public function globalToLocal(point:Point):Point + { + return (target as DisplayObject).globalToLocal(point); + } + + + public function contains(object:Object):Boolean + { + const targetAsDOC:DisplayObjectContainer = this.target as DisplayObjectContainer; + if (targetAsDOC is Stage) + { + return true; + } + const objectAsDO:DisplayObject = object as DisplayObject; + if (objectAsDO) + { + return (targetAsDOC && targetAsDOC.contains(objectAsDO)); + } + /** + * There might be case when we use some old "software" 3D library for instace, + * which viewport is added to classic Display List. So native stage, root and some other + * sprites will actually be parents of 3D objects. To ensure all gestures (both for + * native and 3D objects) work correctly with each other contains() method should be + * a bit more sophisticated. + * But as all 3D engines (at least it looks like that) are moving towards Stage3D layer + * this task doesn't seem significant anymore. So I leave this implementation as + * comments in case someone will actually need it. + * Just uncomment this and it should work. + + // else: more complex case. + // object is not of the same type as this.target (flash.display::DisplayObject) + // it might we some 3D library object in it's viewport (which itself is in DisplayList). + // So we perform more general check: + const adapter:IDisplayListAdapter = Gestouch.gestouch_internal::getDisplayListAdapter(object); + if (adapter) + { + return adapter.getHierarchy(object).indexOf(this.target) > -1; + } + */ + + return false; + } + + + public function getHierarchy(genericTarget:Object):Vector. + { + var list:Vector. = new Vector.(); + var i:uint = 0; + var target:DisplayObject = genericTarget as DisplayObject; + while (target) + { + list[i] = target; + target = target.parent; + i++; + } + + return list; + } + + + public function reflect():Class + { + return NativeDisplayListAdapter; + } + } +} \ No newline at end of file diff --git a/src/org/gestouch/extensions/native/NativeTouchHitTester.as b/src/org/gestouch/extensions/native/NativeTouchHitTester.as new file mode 100644 index 0000000..f7c9273 --- /dev/null +++ b/src/org/gestouch/extensions/native/NativeTouchHitTester.as @@ -0,0 +1,19 @@ +package org.gestouch.extensions.native +{ + import org.gestouch.core.ITouchHitTester; + + import flash.display.InteractiveObject; + import flash.geom.Point; + + + /** + * @author Pavel fljot + */ + final public class NativeTouchHitTester implements ITouchHitTester + { + public function hitTest(point:Point, nativeTarget:InteractiveObject):Object + { + return nativeTarget; + } + } +} diff --git a/src/org/gestouch/extensions/starling/StarlingDisplayListAdapter.as b/src/org/gestouch/extensions/starling/StarlingDisplayListAdapter.as new file mode 100644 index 0000000..adc4e13 --- /dev/null +++ b/src/org/gestouch/extensions/starling/StarlingDisplayListAdapter.as @@ -0,0 +1,77 @@ +package org.gestouch.extensions.starling +{ + import starling.core.Starling; + import starling.display.DisplayObject; + import starling.display.DisplayObjectContainer; + + import org.gestouch.core.IDisplayListAdapter; + + import flash.geom.Point; + import flash.utils.Dictionary; + + + /** + * @author Pavel fljot + */ + final public class StarlingDisplayListAdapter implements IDisplayListAdapter + { + private var targetWeekStorage:Dictionary; + + + public function StarlingDisplayListAdapter(target:DisplayObject = null) + { + if (target) + { + targetWeekStorage = new Dictionary(true); + targetWeekStorage[target] = true; + } + } + + + public function get target():Object + { + for (var key:Object in targetWeekStorage) + { + return key; + } + return null; + } + + + public function globalToLocal(point:Point):Point + { + point = StarlingUtils.adjustGlobalPoint(Starling.current, point); + return (target as DisplayObject).globalToLocal(point); + } + + + public function contains(object:Object):Boolean + { + const targetAsDOC:DisplayObjectContainer = this.target as DisplayObjectContainer; + const objectAsDO:DisplayObject = object as DisplayObject; + return (targetAsDOC && objectAsDO && targetAsDOC.contains(objectAsDO)); + } + + + public function getHierarchy(genericTarget:Object):Vector. + { + var list:Vector. = new Vector.(); + var i:uint = 0; + var target:DisplayObject = genericTarget as DisplayObject; + while (target) + { + list[i] = target; + target = target.parent; + i++; + } + + return list; + } + + + public function reflect():Class + { + return StarlingDisplayListAdapter; + } + } +} \ No newline at end of file diff --git a/src/org/gestouch/extensions/starling/StarlingTouchHitTester.as b/src/org/gestouch/extensions/starling/StarlingTouchHitTester.as new file mode 100644 index 0000000..3dbb079 --- /dev/null +++ b/src/org/gestouch/extensions/starling/StarlingTouchHitTester.as @@ -0,0 +1,36 @@ +package org.gestouch.extensions.starling +{ + import starling.core.Starling; + + import org.gestouch.core.ITouchHitTester; + + import flash.display.InteractiveObject; + import flash.geom.Point; + + + /** + * @author Pavel fljot + */ + final public class StarlingTouchHitTester implements ITouchHitTester + { + private var starling:Starling; + + + public function StarlingTouchHitTester(starling:Starling) + { + if (!starling) + { + throw ArgumentError("Missing starling argument."); + } + + this.starling = starling; + } + + + public function hitTest(point:Point, nativeTarget:InteractiveObject):Object + { + point = StarlingUtils.adjustGlobalPoint(starling, point); + return starling.stage.hitTest(point, true); + } + } +} diff --git a/src/org/gestouch/extensions/starling/StarlingUtils.as b/src/org/gestouch/extensions/starling/StarlingUtils.as new file mode 100644 index 0000000..e5c40d0 --- /dev/null +++ b/src/org/gestouch/extensions/starling/StarlingUtils.as @@ -0,0 +1,38 @@ +package org.gestouch.extensions.starling +{ + import starling.display.Stage; + import starling.core.Starling; + + import flash.geom.Point; + import flash.geom.Rectangle; + + + /** + * @author Pavel fljot + */ + final public class StarlingUtils + { + /** + * Transforms real global point (in the scope of flash.display::Stage) into + * starling stage "global" coordinates. + */ + public static function adjustGlobalPoint(starling:Starling, point:Point):Point + { + const vp:Rectangle = starling.viewPort; + const stStage:Stage = starling.stage; + + if (vp.x != 0 || vp.y != 0 || + stStage.stageWidth != vp.width || stStage.stageHeight != vp.height) + { + point = point.clone(); + + // Same transformation they do in Starling + // WTF!? https://github.com/PrimaryFeather/Starling-Framework/issues/72 + point.x = stStage.stageWidth * (point.x - vp.x) / vp.width; + point.y = stStage.stageHeight * (point.y - vp.y) / vp.height; + } + + return point; + } + } +} diff --git a/src/org/gestouch/gestures/Gesture.as b/src/org/gestouch/gestures/Gesture.as index c2b6cd1..bd12afb 100644 --- a/src/org/gestouch/gestures/Gesture.as +++ b/src/org/gestouch/gestures/Gesture.as @@ -1,28 +1,32 @@ package org.gestouch.gestures { + import org.gestouch.core.Gestouch; import org.gestouch.core.GestureState; import org.gestouch.core.GesturesManager; import org.gestouch.core.IGestureDelegate; - import org.gestouch.core.IGesturesManager; + import org.gestouch.core.IGestureTargetAdapter; import org.gestouch.core.Touch; import org.gestouch.core.gestouch_internal; import org.gestouch.events.GestureStateEvent; - import flash.display.InteractiveObject; + import flash.errors.IllegalOperationError; import flash.events.EventDispatcher; import flash.geom.Point; import flash.system.Capabilities; import flash.utils.Dictionary; + /** + * Dispatched when the state of the gesture changes. + * + * @eventType org.gestouch.events.GestureStateEvent + * @see #state + */ [Event(name="stateChange", type="org.gestouch.events.GestureStateEvent")] /** * Base class for all gestures. Gesture is essentially a detector that tracks touch points * in order detect specific gesture motion and form gesture event on target. * - * TODO: - * - - * * @author Pavel fljot */ public class Gesture extends EventDispatcher @@ -32,10 +36,10 @@ package org.gestouch.gestures * (not an accidental offset on touch), * based on 20 pixels on a 252ppi device. */ - public static const DEFAULT_SLOP:uint = Math.round(20 / 252 * flash.system.Capabilities.screenDPI); + public static var DEFAULT_SLOP:uint = Math.round(20 / 252 * flash.system.Capabilities.screenDPI); - protected const _gesturesManager:IGesturesManager = GesturesManager.getInstance(); + protected const _gesturesManager:GesturesManager = Gestouch.gesturesManager; /** * Map (generic object) of tracking touch points, where keys are touch points IDs. */ @@ -47,10 +51,12 @@ package org.gestouch.gestures * @see requireGestureToFail() */ protected var _gesturesToFail:Dictionary = new Dictionary(true); - protected var _pendingRecognizedState:uint; + protected var _pendingRecognizedState:GestureState; + + use namespace gestouch_internal; - public function Gesture(target:InteractiveObject = null) + public function Gesture(target:Object = null) { super(); @@ -61,9 +67,22 @@ package org.gestouch.gestures /** @private */ - private var _targetWeekStorage:Dictionary; + protected var _targetAdapter:IGestureTargetAdapter; + /** + * + */ + gestouch_internal function get targetAdapter():IGestureTargetAdapter + { + return _targetAdapter; + } + protected function get targetAdapter():IGestureTargetAdapter + { + return _targetAdapter; + } + /** + * FIXME * InteractiveObject (DisplayObject) which this gesture is tracking the actual gesture motion on. * *

Could be some image, component (like map) or the larger view like Stage.

@@ -74,29 +93,18 @@ package org.gestouch.gestures * * @see http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/InteractiveObject.html */ - public function get target():InteractiveObject + public function get target():Object { - for (var key:Object in _targetWeekStorage) - { - return key as InteractiveObject; - } - return null; + return _targetAdapter ? _targetAdapter.target : null; } - public function set target(value:InteractiveObject):void + public function set target(value:Object):void { - var target:InteractiveObject = this.target; + var target:Object = this.target; if (target == value) return; uninstallTarget(target); - for (var key:Object in _targetWeekStorage) - { - delete _targetWeekStorage[key]; - } - if (value) - { - (_targetWeekStorage ||= new Dictionary(true))[value] = true; - } + _targetAdapter = value ? Gestouch.createGestureTargetAdapter(value) : null; installTarget(value); } @@ -117,11 +125,18 @@ package org.gestouch.gestures return; _enabled = value; - //TODO - if (!_enabled && state != GestureState.IDLE) + + if (!_enabled) { - setState(GestureState.CANCELLED); - reset(); + if (state == GestureState.POSSIBLE) + { + setState(GestureState.FAILED); + } + else + if (state == GestureState.BEGAN || state == GestureState.CHANGED) + { + setState(GestureState.CANCELLED); + } } } @@ -148,8 +163,8 @@ package org.gestouch.gestures } - protected var _state:uint = GestureState.IDLE; - public function get state():uint + protected var _state:GestureState = GestureState.IDLE; + public function get state():GestureState { return _state; } @@ -173,7 +188,6 @@ package org.gestouch.gestures */ public function get location():Point { - //TODO: to clone or not clone? performance & convention or ... return _location.clone(); } @@ -186,6 +200,17 @@ package org.gestouch.gestures // //-------------------------------------------------------------------------- + override public function addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void + { + if (!eventTypeIsValid(type)) + { + throw new ArgumentError("Event type does not match any of allowed values."); + } + + super.addEventListener(type, listener, useCapture, priority, useWeakReference); + } + + [Abstract] /** * Reflects gesture class (for better perfomance). @@ -213,7 +238,7 @@ package org.gestouch.gestures */ public function reset():void { - var state:uint = this.state;//caching getter + var state:GestureState = this.state;//caching getter if (state == GestureState.IDLE) return;// Do nothing as we're in IDLE and nothing to reset @@ -228,14 +253,14 @@ package org.gestouch.gestures var gestureToFail:Gesture = key as Gesture; gestureToFail.removeEventListener(GestureStateEvent.STATE_CHANGE, gestureToFail_stateChangeHandler); } - _pendingRecognizedState = 0; + _pendingRecognizedState = null; if (state == GestureState.POSSIBLE) { // manual reset() call. Set to FAILED to keep our State Machine clean and stable setState(GestureState.FAILED); } - else if (state == GestureState.BEGAN || state == GestureState.RECOGNIZED) + else if (state == GestureState.BEGAN || state == GestureState.CHANGED) { // manual reset() call. Set to CANCELLED to keep our State Machine clean and stable setState(GestureState.CANCELLED); @@ -285,6 +310,11 @@ package org.gestouch.gestures public function requireGestureToFail(gesture:Gesture):void { //TODO + if (!gesture) + { + throw new ArgumentError(); + } + _gesturesToFail[gesture] = true; } @@ -310,11 +340,11 @@ package org.gestouch.gestures * * @see http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/InteractiveObject.html */ - protected function installTarget(target:InteractiveObject):void + protected function installTarget(target:Object):void { if (target) { - _gesturesManager.gestouch_internal::addGesture(this); + _gesturesManager.addGesture(this); } } @@ -326,11 +356,11 @@ package org.gestouch.gestures * * @see http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/InteractiveObject.html */ - protected function uninstallTarget(target:InteractiveObject):void + protected function uninstallTarget(target:Object):void { if (target) { - _gesturesManager.gestouch_internal::removeGesture(this); + _gesturesManager.removeGesture(this); } } @@ -348,6 +378,19 @@ package org.gestouch.gestures } + protected function failOrIgnoreTouch(touch:Touch):void + { + if (state == GestureState.POSSIBLE) + { + setState(GestureState.FAILED); + } + else if (state != GestureState.IDLE) + { + ignoreTouch(touch); + } + } + + [Abstract] /** *

NB! This is abstract method and must be overridden.

@@ -375,30 +418,47 @@ package org.gestouch.gestures } - protected function setState(newState:uint):Boolean + /** + * + */ + protected function onTouchCancel(touch:Touch):void + { + } + + + protected function setState(newState:GestureState):Boolean { if (_state == newState && _state == GestureState.CHANGED) { + // shortcut for better performance + if (hasEventListener(GestureStateEvent.STATE_CHANGE)) + { + dispatchEvent(new GestureStateEvent(GestureStateEvent.STATE_CHANGE, _state, _state)); + } + return true; } - //TODO: is state sequence validation needed? e.g.: - //POSSIBLE should be followed by BEGAN or RECOGNIZED or FAILED - //BEGAN should be follwed by CHANGED or ENDED or CANCELLED - //CHANGED should be followed by CHANGED or ENDED or CANCELLED - //... + if (!_state.canTransitionTo(newState)) + { + throw new IllegalOperationError("You cannot change from state " + + _state + " to state " + newState + "."); + } + if (newState == GestureState.BEGAN || newState == GestureState.RECOGNIZED) { var gestureToFail:Gesture; + var key:*; // first we check if other required-to-fail gestures recognized // TODO: is this really necessary? using "requireGestureToFail" API assume that // required-to-fail gesture always recognizes AFTER this one. - for (var key:* in _gesturesToFail) + for (key in _gesturesToFail) { gestureToFail = key as Gesture; - if (gestureToFail.state != GestureState.IDLE && gestureToFail.state != GestureState.POSSIBLE - && gestureToFail.state != GestureState.FAILED) + if (gestureToFail.state != GestureState.IDLE && + gestureToFail.state != GestureState.POSSIBLE && + gestureToFail.state != GestureState.FAILED) { // Looks like other gesture won't fail, // which means the required condition will not happen, so we must fail @@ -406,7 +466,7 @@ package org.gestouch.gestures return false; } } - // then we check of other required-to-fail gestures are actually tracked (not IDLE) + // then we check if other required-to-fail gestures are actually tracked (not IDLE) // and not still not recognized (e.g. POSSIBLE state) for (key in _gesturesToFail) { @@ -415,6 +475,13 @@ package org.gestouch.gestures { // Other gesture might fail soon, so we postpone state change _pendingRecognizedState = newState; + + for (key in _gesturesToFail) + { + gestureToFail = key as Gesture; + gestureToFail.addEventListener(GestureStateEvent.STATE_CHANGE, gestureToFail_stateChangeHandler, false, 0, true); + } + return false; } // else if gesture is in IDLE state it means it doesn't track anything, @@ -430,12 +497,12 @@ package org.gestouch.gestures } } - var oldState:uint = _state; + var oldState:GestureState = _state; _state = newState; - if (((GestureState.CANCELLED | GestureState.RECOGNIZED | GestureState.ENDED | GestureState.FAILED) & _state) > 0) + if (_state.isEndState) { - _gesturesManager.gestouch_internal::scheduleGestureStateReset(this); + _gesturesManager.scheduleGestureStateReset(this); } //TODO: what if RTE happens in event handlers? @@ -447,14 +514,14 @@ package org.gestouch.gestures if (_state == GestureState.BEGAN || _state == GestureState.RECOGNIZED) { - _gesturesManager.gestouch_internal::onGestureRecognized(this); + _gesturesManager.onGestureRecognized(this); } return true; } - gestouch_internal function setState_internal(state:uint):void + gestouch_internal function setState_internal(state:GestureState):void { setState(state); } @@ -481,7 +548,7 @@ package org.gestouch.gestures updateCentralPoint(); _location.x = _centralPoint.x; _location.y = _centralPoint.y; - _localLocation = target.globalToLocal(_location); + _localLocation = targetAdapter.globalToLocal(_location); } @@ -496,6 +563,13 @@ package org.gestouch.gestures } + protected function eventTypeIsValid(type:String):Boolean + { + // propertyChange just in case for bindings? + return type == GestureStateEvent.STATE_CHANGE || type == "propertyChange"; + } + + //-------------------------------------------------------------------------- @@ -513,12 +587,6 @@ package org.gestouch.gestures if (_touchesCount == 1 && state == GestureState.IDLE) { - for (var key:* in _gesturesToFail) - { - var gestureToFail:Gesture = key as Gesture; - gestureToFail.addEventListener(GestureStateEvent.STATE_CHANGE, gestureToFail_stateChangeHandler, false, 0, true); - } - setState(GestureState.POSSIBLE); } } @@ -540,12 +608,30 @@ package org.gestouch.gestures } + gestouch_internal function touchCancelHandler(touch:Touch):void + { + delete _touchesMap[touch.id]; + _touchesCount--; + + onTouchCancel(touch); + + if (!state.isEndState) + { + if (state == GestureState.BEGAN || state == GestureState.CHANGED) + { + setState(GestureState.CANCELLED); + } + else + { + setState(GestureState.FAILED); + } + } + } + + protected function gestureToFail_stateChangeHandler(event:GestureStateEvent):void { - if (state != GestureState.POSSIBLE) - return;//just in case..FIXME? - - if (!_pendingRecognizedState) + if (!_pendingRecognizedState || state != GestureState.POSSIBLE) return; if (event.newState == GestureState.FAILED) @@ -560,15 +646,20 @@ package org.gestouch.gestures } } + // at this point all gestures-to-fail are either in IDLE or in FAILED states if (setState(_pendingRecognizedState)) { onDelayedRecognize(); } } - else if (event.newState != GestureState.POSSIBLE) + else if (event.newState != GestureState.IDLE && event.newState != GestureState.POSSIBLE) { + // NB: _other_ gesture may switch to IDLE state if it was in FAILED when + // _this_ gesture initially attempted to switch to one of recognized state. + // ...and that's OK (we ignore that) + setState(GestureState.FAILED); } } } -} \ No newline at end of file +} diff --git a/src/org/gestouch/gestures/LongPressGesture.as b/src/org/gestouch/gestures/LongPressGesture.as index a99ea6b..7ef5632 100644 --- a/src/org/gestouch/gestures/LongPressGesture.as +++ b/src/org/gestouch/gestures/LongPressGesture.as @@ -4,14 +4,18 @@ package org.gestouch.gestures import org.gestouch.core.Touch; import org.gestouch.events.LongPressGestureEvent; - import flash.display.InteractiveObject; import flash.events.TimerEvent; import flash.utils.Timer; /** - * TODO: -location - * - check on iOS (Obj-C) what happens when numTouchesRequired=2, two finger down, then quickly release one. + * + * @eventType org.gestouch.events.LongPressGestureEvent + */ + [Event(name="gestureLongPress", type="org.gestouch.events.LongPressGestureEvent")] + /** + * TODO: + * - add numTapsRequired * * @author Pavel fljot */ @@ -21,16 +25,16 @@ package org.gestouch.gestures /** * The minimum time interval in millisecond fingers must press on the target for the gesture to be recognized. * - * @default 500 - */ - public var minPressDuration:uint = 500; + * @default 500 + */ + public var minPressDuration:uint = 500; public var slop:Number = Gesture.DEFAULT_SLOP; protected var _timer:Timer; protected var _numTouchesRequiredReached:Boolean; - public function LongPressGesture(target:InteractiveObject = null) + public function LongPressGesture(target:Object = null) { super(target); } @@ -49,7 +53,7 @@ package org.gestouch.gestures return TapGesture; } - + override public function reset():void { super.reset(); @@ -67,6 +71,12 @@ package org.gestouch.gestures // // -------------------------------------------------------------------------- + override protected function eventTypeIsValid(type:String):Boolean + { + return type == LongPressGestureEvent.GESTURE_LONG_PRESS || super.eventTypeIsValid(type); + } + + override protected function preinit():void { super.preinit(); @@ -80,14 +90,7 @@ package org.gestouch.gestures { if (touchesCount > numTouchesRequired) { - if (state == GestureState.BEGAN || state == GestureState.CHANGED) - { - ignoreTouch(touch); - } - else - { - setState(GestureState.FAILED); - } + failOrIgnoreTouch(touch); return; } @@ -121,10 +124,9 @@ package org.gestouch.gestures override protected function onTouchEnd(touch:Touch):void { - //TODO: check proper condition (behavior) on iOS native if (_numTouchesRequiredReached) { - if (((GestureState.BEGAN | GestureState.CHANGED) & state) > 0) + if (state == GestureState.BEGAN || state == GestureState.CHANGED) { updateLocation(); if (setState(GestureState.ENDED) && hasEventListener(LongPressGestureEvent.GESTURE_LONG_PRESS)) @@ -144,7 +146,7 @@ package org.gestouch.gestures } } - + override protected function onDelayedRecognize():void { if (hasEventListener(LongPressGestureEvent.GESTURE_LONG_PRESS)) diff --git a/src/org/gestouch/gestures/PanGesture.as b/src/org/gestouch/gestures/PanGesture.as index ddf74bf..6dbf6e2 100644 --- a/src/org/gestouch/gestures/PanGesture.as +++ b/src/org/gestouch/gestures/PanGesture.as @@ -4,9 +4,13 @@ package org.gestouch.gestures import org.gestouch.core.Touch; import org.gestouch.events.PanGestureEvent; - import flash.display.InteractiveObject; import flash.geom.Point; + + /** + * + * @eventType org.gestouch.events.PanGestureEvent + */ [Event(name="gesturePan", type="org.gestouch.events.PanGestureEvent")] /** * TODO: @@ -27,14 +31,14 @@ package org.gestouch.gestures protected var _gestureBeginOffsetY:Number; - public function PanGesture(target:InteractiveObject = null) + public function PanGesture(target:Object = null) { super(target); } /** @private */ - private var _maxNumTouchesRequired:uint = 1; + private var _maxNumTouchesRequired:uint = uint.MAX_VALUE; /** * @@ -108,19 +112,24 @@ package org.gestouch.gestures // // -------------------------------------------------------------------------- + override protected function eventTypeIsValid(type:String):Boolean + { + return type == PanGestureEvent.GESTURE_PAN || super.eventTypeIsValid(type); + } + + override protected function onTouchBegin(touch:Touch):void { if (touchesCount > maxNumTouchesRequired) { - //TODO - ignoreTouch(touch); + failOrIgnoreTouch(touch); return; } if (touchesCount >= minNumTouchesRequired) { updateLocation(); - } + } } @@ -136,6 +145,10 @@ package org.gestouch.gestures if (state == GestureState.POSSIBLE) { + prevLocationX = _location.x; + prevLocationY = _location.y; + updateLocation(); + // Check if finger moved enough for gesture to be recognized var locationOffset:Point = touch.locationOffset; if (direction == PanGestureDirection.VERTICAL) @@ -149,9 +162,6 @@ package org.gestouch.gestures if (locationOffset.length > slop || slop != slop)//faster isNaN(slop) { - prevLocationX = _location.x; - prevLocationY = _location.y; - updateLocation(); offsetX = _location.x - prevLocationX; offsetY = _location.y - prevLocationY; // acummulate begin offsets for the case when this gesture recognition is delayed by requireGestureToFail diff --git a/src/org/gestouch/gestures/RotateGesture.as b/src/org/gestouch/gestures/RotateGesture.as index da1665b..1766c6a 100644 --- a/src/org/gestouch/gestures/RotateGesture.as +++ b/src/org/gestouch/gestures/RotateGesture.as @@ -3,11 +3,14 @@ package org.gestouch.gestures import org.gestouch.core.GestureState; import org.gestouch.core.Touch; import org.gestouch.events.RotateGestureEvent; - import org.gestouch.utils.GestureUtils; - import flash.display.InteractiveObject; import flash.geom.Point; + + /** + * + * @eventType org.gestouch.events.RotateGestureEvent + */ [Event(name="gestureRotate", type="org.gestouch.events.RotateGestureEvent")] /** * TODO: @@ -17,14 +20,15 @@ package org.gestouch.gestures */ public class RotateGesture extends Gesture { - public var slop:Number = Gesture.DEFAULT_SLOP >> 1; + public var slop:Number = Gesture.DEFAULT_SLOP; protected var _touch1:Touch; protected var _touch2:Touch; protected var _transformVector:Point; + protected var _thresholdAngle:Number; - public function RotateGesture(target:InteractiveObject = null) + public function RotateGesture(target:Object = null) { super(target); } @@ -52,12 +56,17 @@ package org.gestouch.gestures // // -------------------------------------------------------------------------- + override protected function eventTypeIsValid(type:String):Boolean + { + return type == RotateGestureEvent.GESTURE_ROTATE || super.eventTypeIsValid(type); + } + + override protected function onTouchBegin(touch:Touch):void { if (touchesCount > 2) { - //TODO - ignoreTouch(touch); + failOrIgnoreTouch(touch); return; } @@ -70,6 +79,9 @@ package org.gestouch.gestures _touch2 = touch; _transformVector = _touch2.location.subtract(_touch1.location); + + // @see chord length formula + _thresholdAngle = Math.asin(slop / (2 * _transformVector.length)) * 2; } } @@ -79,38 +91,41 @@ package org.gestouch.gestures if (touchesCount < 2) return; - var recognized:Boolean = true; + var currTransformVector:Point = _touch2.location.subtract(_touch1.location); + var rotation:Number = Math.atan2(currTransformVector.y, currTransformVector.x) - Math.atan2(_transformVector.y, _transformVector.x); - if (state == GestureState.POSSIBLE && slop > 0 && touch.locationOffset.length < slop) + if (state == GestureState.POSSIBLE) { - recognized = false; + const absRotation:Number = rotation >= 0 ? rotation : -rotation; + if (absRotation < _thresholdAngle) + { + // not recognized yet + return; + } + + // adjust angle to avoid initial "jump" + rotation = rotation > 0 ? rotation - _thresholdAngle : rotation + _thresholdAngle; } - if (recognized) + _transformVector.x = currTransformVector.x; + _transformVector.y = currTransformVector.y; + + updateLocation(); + + if (state == GestureState.POSSIBLE) { - var currTransformVector:Point = _touch2.location.subtract(_touch1.location); - var rotation:Number = Math.atan2(currTransformVector.y, currTransformVector.x) - Math.atan2(_transformVector.y, _transformVector.x); - rotation *= GestureUtils.RADIANS_TO_DEGREES; - _transformVector.x = currTransformVector.x; - _transformVector.y = currTransformVector.y; - - updateLocation(); - - if (state == GestureState.POSSIBLE) + if (setState(GestureState.BEGAN) && hasEventListener(RotateGestureEvent.GESTURE_ROTATE)) { - if (setState(GestureState.BEGAN) && hasEventListener(RotateGestureEvent.GESTURE_ROTATE)) - { - dispatchEvent(new RotateGestureEvent(RotateGestureEvent.GESTURE_ROTATE, false, false, GestureState.BEGAN, - _location.x, _location.y, _localLocation.x, _localLocation.y, rotation)); - } + dispatchEvent(new RotateGestureEvent(RotateGestureEvent.GESTURE_ROTATE, false, false, GestureState.BEGAN, + _location.x, _location.y, _localLocation.x, _localLocation.y, rotation)); } - else + } + else + { + if (setState(GestureState.CHANGED) && hasEventListener(RotateGestureEvent.GESTURE_ROTATE)) { - if (setState(GestureState.CHANGED) && hasEventListener(RotateGestureEvent.GESTURE_ROTATE)) - { - dispatchEvent(new RotateGestureEvent(RotateGestureEvent.GESTURE_ROTATE, false, false, GestureState.CHANGED, - _location.x, _location.y, _localLocation.x, _localLocation.y, rotation)); - } + dispatchEvent(new RotateGestureEvent(RotateGestureEvent.GESTURE_ROTATE, false, false, GestureState.CHANGED, + _location.x, _location.y, _localLocation.x, _localLocation.y, rotation)); } } } diff --git a/src/org/gestouch/gestures/SwipeGesture.as b/src/org/gestouch/gestures/SwipeGesture.as index 9665493..e464206 100644 --- a/src/org/gestouch/gestures/SwipeGesture.as +++ b/src/org/gestouch/gestures/SwipeGesture.as @@ -3,35 +3,85 @@ package org.gestouch.gestures import org.gestouch.core.GestureState; import org.gestouch.core.Touch; import org.gestouch.events.SwipeGestureEvent; + import org.gestouch.utils.GestureUtils; - import flash.display.InteractiveObject; + import flash.events.TimerEvent; import flash.geom.Point; + import flash.system.Capabilities; + import flash.utils.Timer; + + /** + * + * @eventType org.gestouch.events.SwipeGestureEvent + */ [Event(name="gestureSwipe", type="org.gestouch.events.SwipeGestureEvent")] /** - * TODO: - * -check native behavior on iDevice + * Recognition logic:
+ * 1. should be recognized during maxDuration period
+ * 2. velocity >= minVelocity OR offset >= minOffset + * * * @author Pavel fljot */ public class SwipeGesture extends Gesture { - public var slop:Number = Gesture.DEFAULT_SLOP; + private static const ANGLE:Number = 40 * GestureUtils.DEGREES_TO_RADIANS; + private static const MAX_DURATION:uint = 500; + private static const MIN_OFFSET:Number = Capabilities.screenDPI / 6; + private static const MIN_VELOCITY:Number = 2 * MIN_OFFSET / MAX_DURATION; + + /** + * "Dirty" region around touch begin location which does not taken into account + * for gesture failing conditions. It might be useful in case your device is too sensitive + * (so when you just touch the screen you get undesired accidental movement).
+ * NB! Unlike in other gestures, default value for slop is 0 here. + * + * @default 0 + */ + public var slop:Number = 0; public var numTouchesRequired:uint = 1; - public var minVelocity:Number = 0.8; - public var minOffset:Number = Gesture.DEFAULT_SLOP; public var direction:uint = SwipeGestureDirection.ORTHOGONAL; - public var maxDirectionalOffset:Number = Gesture.DEFAULT_SLOP << 1; + + /** + * The duration of period (in milliseconds) in which SwipeGesture must be recognized. + * If gesture is not recognized during this period it fails. Default value is 500 (half a + * second) and generally should not be changed. You can change it though for some special + * cases, most likely together with minVelocity and minOffset + * to achieve really custom behavior. + * + * @default 500 + * + * @see #minVelocity + * @see #minOffset + */ + public var maxDuration:uint = MAX_DURATION; + + /** + * Minimum offset (in pixels) for gesture to be recognized. + * Default value is Capabilities.screenDPI / 6 and generally should not + * be changed. + */ + public var minOffset:Number = MIN_OFFSET; + + /** + * Minimum velocity (in pixels per millisecond) for gesture to be recognized. + * Default value is 2 * minOffset / maxDuration and generally should not + * be changed. + * + * @see #minOffset + * @see #minDuration + */ + public var minVelocity:Number = MIN_VELOCITY; protected var _offset:Point = new Point(); protected var _startTime:int; protected var _noDirection:Boolean; protected var _avrgVel:Point = new Point(); - protected var _prevAvrgVel:Point = new Point(); - protected var _decelerationCounter:uint = 0; + protected var _timer:Timer; - public function SwipeGesture(target:InteractiveObject = null) + public function SwipeGesture(target:Object = null) { super(target); } @@ -50,14 +100,14 @@ package org.gestouch.gestures return SwipeGesture; } - + override public function reset():void { _startTime = 0; _offset.x = 0; _offset.y = 0; - _decelerationCounter = 0; - + _timer.reset(); + super.reset(); } @@ -70,12 +120,26 @@ package org.gestouch.gestures // // -------------------------------------------------------------------------- + override protected function eventTypeIsValid(type:String):Boolean + { + return type == SwipeGestureEvent.GESTURE_SWIPE || super.eventTypeIsValid(type); + } + + + override protected function preinit():void + { + super.preinit(); + + _timer = new Timer(maxDuration, 1); + _timer.addEventListener(TimerEvent.TIMER_COMPLETE, timer_timerCompleteHandler); + } + + override protected function onTouchBegin(touch:Touch):void { if (touchesCount > numTouchesRequired) { - //TODO: or ignore? - setState(GestureState.FAILED); + failOrIgnoreTouch(touch); return; } @@ -83,6 +147,10 @@ package org.gestouch.gestures { // Because we want to fail as quick as possible _startTime = touch.time; + + _timer.reset(); + _timer.delay = maxDuration; + _timer.start(); } if (touchesCount == numTouchesRequired) { @@ -100,6 +168,10 @@ package org.gestouch.gestures if (touchesCount < numTouchesRequired) return; + var totalTime:int = touch.time - _startTime; + if (totalTime == 0) + return;//It was somehow THAT MUCH performant on one Android tablet + var prevCentralPointX:Number = _centralPoint.x; var prevCentralPointY:Number = _centralPoint.y; updateCentralPoint(); @@ -107,43 +179,19 @@ package org.gestouch.gestures _offset.x = _centralPoint.x - _location.x; _offset.y = _centralPoint.y - _location.y; var offsetLength:Number = _offset.length; - - if (offsetLength < slop) - { - // no need in processing - we're in the very beginning of movement - return; - } // average velocity (total offset to total duration) - _prevAvrgVel.x = _avrgVel.x; - _prevAvrgVel.y = _avrgVel.y; - var absPrevAvrgVel:Number = _prevAvrgVel.length; - var totalTime:int = touch.time - _startTime; _avrgVel.x = _offset.x / totalTime; _avrgVel.y = _offset.y / totalTime; var avrgVel:Number = _avrgVel.length; - - if (avrgVel * 0.95 < absPrevAvrgVel) - { - _decelerationCounter++; - } - if (_decelerationCounter > 5 || avrgVel < 0.1) - { - setState(GestureState.FAILED); - return; - } - if (_noDirection) { - // We should quickly fail if we have noticable deceleration - // or first movement happend way later after touch - - if (avrgVel >= minVelocity && (minOffset != minOffset || offsetLength >= minOffset)) + if (avrgVel >= minVelocity || offsetLength >= minOffset) { if (setState(GestureState.RECOGNIZED) && hasEventListener(SwipeGestureEvent.GESTURE_SWIPE)) { - _localLocation = target.globalToLocal(_location);//refresh local location in case target moved + _localLocation = targetAdapter.globalToLocal(_location);//refresh local location in case target moved dispatchEvent(new SwipeGestureEvent(SwipeGestureEvent.GESTURE_SWIPE, false, false, GestureState.RECOGNIZED, _location.x, _location.y, _localLocation.x, _localLocation.y, _offset.x, _offset.y)); } @@ -161,25 +209,25 @@ package org.gestouch.gestures { var absOffsetX:Number = _offset.x > 0 ? _offset.x : -_offset.x; - if ((SwipeGestureDirection.HORIZONTAL & direction) == 0) - { - // horizontal velocity is greater then vertical, but we're not interested in any horizontal direction - setState(GestureState.FAILED); - } - else if (recentOffsetX < 0 && (direction & SwipeGestureDirection.LEFT) == 0 || - recentOffsetX > 0 && (direction & SwipeGestureDirection.RIGHT) == 0 || - Math.abs(_offset.y) > maxDirectionalOffset) + if ((recentOffsetX < 0 && (direction & SwipeGestureDirection.LEFT) == 0) || + (recentOffsetX > 0 && (direction & SwipeGestureDirection.RIGHT) == 0) || + Math.abs(Math.atan(_offset.y/_offset.x)) > ANGLE) { // movement in opposite direction // or too much diagonally - setState(GestureState.FAILED); + + // Give some tolerance for accidental offset on finger press (slop) + if (offsetLength > slop || slop != slop)//faster isNaN() + { + setState(GestureState.FAILED); + } } - else if (absVelX >= minVelocity && (minOffset != minOffset || absOffsetX >= minOffset)) + else if (absVelX >= minVelocity || absOffsetX >= minOffset) { _offset.y = 0; if (setState(GestureState.RECOGNIZED) && hasEventListener(SwipeGestureEvent.GESTURE_SWIPE)) { - _localLocation = target.globalToLocal(_location);//refresh local location in case target moved + _localLocation = targetAdapter.globalToLocal(_location);//refresh local location in case target moved dispatchEvent(new SwipeGestureEvent(SwipeGestureEvent.GESTURE_SWIPE, false, false, GestureState.RECOGNIZED, _location.x, _location.y, _localLocation.x, _localLocation.y, _offset.x, _offset.y)); } @@ -189,36 +237,37 @@ package org.gestouch.gestures { var absOffsetY:Number = _offset.y > 0 ? _offset.y : -_offset.y; - if ((SwipeGestureDirection.VERTICAL & direction) == 0) - { - // horizontal velocity is greater then vertical, but we're not interested in any horizontal direction - setState(GestureState.FAILED); - } - else if (recentOffsetY < 0 && (direction & SwipeGestureDirection.UP) == 0 || - recentOffsetY > 0 && (direction & SwipeGestureDirection.DOWN) == 0 || - Math.abs(_offset.x) > maxDirectionalOffset) + if ((recentOffsetY < 0 && (direction & SwipeGestureDirection.UP) == 0) || + (recentOffsetY > 0 && (direction & SwipeGestureDirection.DOWN) == 0) || + Math.abs(Math.atan(_offset.x/_offset.y)) > ANGLE) { // movement in opposite direction // or too much diagonally - setState(GestureState.FAILED); + + // Give some tolerance for accidental offset on finger press (slop) + if (offsetLength > slop || slop != slop)//faster isNaN() + { + setState(GestureState.FAILED); + } } - else if (absVelY >= minVelocity && (minOffset != minOffset || absOffsetY >= minOffset)) + else if (absVelY >= minVelocity || absOffsetY >= minOffset) { _offset.x = 0; if (setState(GestureState.RECOGNIZED) && hasEventListener(SwipeGestureEvent.GESTURE_SWIPE)) { - _localLocation = target.globalToLocal(_location);//refresh local location in case target moved + _localLocation = targetAdapter.globalToLocal(_location);//refresh local location in case target moved dispatchEvent(new SwipeGestureEvent(SwipeGestureEvent.GESTURE_SWIPE, false, false, GestureState.RECOGNIZED, _location.x, _location.y, _localLocation.x, _localLocation.y, _offset.x, _offset.y)); } } } - else + // Give some tolerance for accidental offset on finger press (slop) + else if (offsetLength > slop || slop != slop)//faster isNaN() { setState(GestureState.FAILED); } } - } + } override protected function onTouchEnd(touch:Touch):void @@ -234,10 +283,27 @@ package org.gestouch.gestures { if (hasEventListener(SwipeGestureEvent.GESTURE_SWIPE)) { - _localLocation = target.globalToLocal(_location);//refresh local location in case target moved + _localLocation = targetAdapter.globalToLocal(_location);//refresh local location in case target moved dispatchEvent(new SwipeGestureEvent(SwipeGestureEvent.GESTURE_SWIPE, false, false, GestureState.RECOGNIZED, _location.x, _location.y, _localLocation.x, _localLocation.y, _offset.x, _offset.y)); } } + + + + + //-------------------------------------------------------------------------- + // + // Event handlers + // + //-------------------------------------------------------------------------- + + protected function timer_timerCompleteHandler(event:TimerEvent):void + { + if (state == GestureState.POSSIBLE) + { + setState(GestureState.FAILED); + } + } } } \ No newline at end of file diff --git a/src/org/gestouch/gestures/TapGesture.as b/src/org/gestouch/gestures/TapGesture.as index fc5881b..5d5b6a3 100644 --- a/src/org/gestouch/gestures/TapGesture.as +++ b/src/org/gestouch/gestures/TapGesture.as @@ -4,14 +4,16 @@ package org.gestouch.gestures import org.gestouch.core.Touch; import org.gestouch.events.TapGestureEvent; - import flash.display.InteractiveObject; import flash.events.TimerEvent; import flash.utils.Timer; + /** + * + * @eventType org.gestouch.events.TapGestureEvent + */ [Event(name="gestureTap", type="org.gestouch.events.TapGestureEvent")] /** - * TODO: check failing conditions (iDevice) * * @author Pavel fljot */ @@ -19,7 +21,7 @@ package org.gestouch.gestures { public var numTouchesRequired:uint = 1; public var numTapsRequired:uint = 1; - public var slop:Number = Gesture.DEFAULT_SLOP; + public var slop:Number = Gesture.DEFAULT_SLOP << 2;//iOS has 45px for 132 dpi screen public var maxTapDelay:uint = 400; public var maxTapDuration:uint = 1500; @@ -28,7 +30,7 @@ package org.gestouch.gestures protected var _tapCounter:uint = 0; - public function TapGesture(target:InteractiveObject = null) + public function TapGesture(target:Object = null) { super(target); } @@ -47,17 +49,17 @@ package org.gestouch.gestures return TapGesture; } - + override public function reset():void { _numTouchesRequiredReached = false; _tapCounter = 0; _timer.reset(); - + super.reset(); } - + override public function canPreventGesture(preventedGesture:Gesture):Boolean { if (preventedGesture is TapGesture && @@ -77,6 +79,12 @@ package org.gestouch.gestures // // -------------------------------------------------------------------------- + override protected function eventTypeIsValid(type:String):Boolean + { + return type == TapGestureEvent.GESTURE_TAP || super.eventTypeIsValid(type); + } + + override protected function preinit():void { super.preinit(); @@ -90,9 +98,7 @@ package org.gestouch.gestures { if (touchesCount > numTouchesRequired) { - // We put more fingers then required at the same time, - // so treat that as failed - setState(GestureState.FAILED); + failOrIgnoreTouch(touch); return; } @@ -105,7 +111,8 @@ package org.gestouch.gestures if (touchesCount == numTouchesRequired) { - _numTouchesRequiredReached = true; + _numTouchesRequiredReached = true; + updateLocation(); } } @@ -123,7 +130,6 @@ package org.gestouch.gestures { if (!_numTouchesRequiredReached) { - //TODO: check this condition on iDevice setState(GestureState.FAILED); } else if (touchesCount == 0) @@ -136,7 +142,6 @@ package org.gestouch.gestures if (_tapCounter == numTapsRequired) { - updateLocation(); if (setState(GestureState.RECOGNIZED) && hasEventListener(TapGestureEvent.GESTURE_TAP)) { dispatchEvent(new TapGestureEvent(TapGestureEvent.GESTURE_TAP, false, false, GestureState.RECOGNIZED, diff --git a/src/org/gestouch/gestures/TransformGesture.as b/src/org/gestouch/gestures/TransformGesture.as index b3a2daa..909eb14 100644 --- a/src/org/gestouch/gestures/TransformGesture.as +++ b/src/org/gestouch/gestures/TransformGesture.as @@ -3,12 +3,14 @@ package org.gestouch.gestures import org.gestouch.core.GestureState; import org.gestouch.core.Touch; import org.gestouch.events.TransformGestureEvent; - import org.gestouch.utils.GestureUtils; - import flash.display.InteractiveObject; import flash.geom.Point; - - + + + /** + * + * @eventType org.gestouch.events.TransformGestureEvent + */ [Event(name="gestureTransform", type="org.gestouch.events.TransformGestureEvent")] /** * @author Pavel fljot @@ -22,7 +24,7 @@ package org.gestouch.gestures protected var _transformVector:Point; - public function TransformGesture(target:InteractiveObject = null) + public function TransformGesture(target:Object = null) { super(target); } @@ -59,12 +61,17 @@ package org.gestouch.gestures // // -------------------------------------------------------------------------- + override protected function eventTypeIsValid(type:String):Boolean + { + return type == TransformGestureEvent.GESTURE_TRANSFORM || super.eventTypeIsValid(type); + } + + override protected function onTouchBegin(touch:Touch):void { if (touchesCount > 2) { - //TODO: to ignore or to keep this touch somewhere? - ignoreTouch(touch); + failOrIgnoreTouch(touch); return; } @@ -97,51 +104,60 @@ package org.gestouch.gestures var prevLocation:Point = _location.clone(); updateLocation(); - var recognized:Boolean = true; + var currTransformVector:Point; - if (state == GestureState.POSSIBLE && slop > 0 && touch.locationOffset.length < slop) + if (state == GestureState.POSSIBLE) { - recognized = false; + if (slop > 0 && touch.locationOffset.length < slop) + { + // Not recognized yet + if (_touch2) + { + // Recalculate _transformVector to avoid initial "jump" on recognize + _transformVector = _touch2.location.subtract(_touch1.location); + } + return; + } } - if (recognized) + if (_touch2 && !currTransformVector) { - var prevLocalLocation:Point; - var offsetX:Number = _location.x - prevLocation.x; - var offsetY:Number = _location.y - prevLocation.y; - var scale:Number = 1; - var rotation:Number = 0; - if (_touch2) + currTransformVector = _touch2.location.subtract(_touch1.location); + } + + var prevLocalLocation:Point; + var offsetX:Number = _location.x - prevLocation.x; + var offsetY:Number = _location.y - prevLocation.y; + var scale:Number = 1; + var rotation:Number = 0; + if (_touch2) + { + rotation = Math.atan2(currTransformVector.y, currTransformVector.x) - Math.atan2(_transformVector.y, _transformVector.x); + scale = currTransformVector.length / _transformVector.length; + _transformVector = _touch2.location.subtract(_touch1.location); + } + + + if (state == GestureState.POSSIBLE) + { + if (setState(GestureState.BEGAN) && hasEventListener(TransformGestureEvent.GESTURE_TRANSFORM)) { - var currTransformVector:Point = _touch2.location.subtract(_touch1.location); - rotation = Math.atan2(currTransformVector.y, currTransformVector.x) - Math.atan2(_transformVector.y, _transformVector.x); - rotation *= GestureUtils.RADIANS_TO_DEGREES; - scale = currTransformVector.length / _transformVector.length; - _transformVector = _touch2.location.subtract(_touch1.location); + // Note that we dispatch previous location point which gives a way to perform + // accurate UI redraw. See examples project for more info. + prevLocalLocation = targetAdapter.globalToLocal(prevLocation); + dispatchEvent(new TransformGestureEvent(TransformGestureEvent.GESTURE_TRANSFORM, false, false, GestureState.BEGAN, + prevLocation.x, prevLocation.y, prevLocalLocation.x, prevLocalLocation.y, scale, scale, rotation, offsetX, offsetY)); } - - - if (state == GestureState.POSSIBLE) + } + else + { + if (setState(GestureState.CHANGED) && hasEventListener(TransformGestureEvent.GESTURE_TRANSFORM)) { - if (setState(GestureState.BEGAN) && hasEventListener(TransformGestureEvent.GESTURE_TRANSFORM)) - { - // Note that we dispatch previous location point which gives a way to perform - // accurate UI redraw. See examples project for more info. - prevLocalLocation = target.globalToLocal(prevLocation); - dispatchEvent(new TransformGestureEvent(TransformGestureEvent.GESTURE_TRANSFORM, false, false, GestureState.BEGAN, - prevLocation.x, prevLocation.y, prevLocalLocation.x, prevLocalLocation.y, scale, scale, rotation, offsetX, offsetY)); - } - } - else - { - if (setState(GestureState.CHANGED) && hasEventListener(TransformGestureEvent.GESTURE_TRANSFORM)) - { - // Note that we dispatch previous location point which gives a way to perform - // accurate UI redraw. See examples project for more info. - prevLocalLocation = target.globalToLocal(prevLocation); - dispatchEvent(new TransformGestureEvent(TransformGestureEvent.GESTURE_TRANSFORM, false, false, GestureState.CHANGED, - prevLocation.x, prevLocation.y, prevLocalLocation.x, prevLocalLocation.y, scale, scale, rotation, offsetX, offsetY)); - } + // Note that we dispatch previous location point which gives a way to perform + // accurate UI redraw. See examples project for more info. + prevLocalLocation = targetAdapter.globalToLocal(prevLocation); + dispatchEvent(new TransformGestureEvent(TransformGestureEvent.GESTURE_TRANSFORM, false, false, GestureState.CHANGED, + prevLocation.x, prevLocation.y, prevLocalLocation.x, prevLocalLocation.y, scale, scale, rotation, offsetX, offsetY)); } } } diff --git a/src/org/gestouch/gestures/ZoomGesture.as b/src/org/gestouch/gestures/ZoomGesture.as index 09608b3..49f7197 100644 --- a/src/org/gestouch/gestures/ZoomGesture.as +++ b/src/org/gestouch/gestures/ZoomGesture.as @@ -4,27 +4,30 @@ package org.gestouch.gestures import org.gestouch.core.Touch; import org.gestouch.events.ZoomGestureEvent; - import flash.display.InteractiveObject; import flash.geom.Point; + + /** + * + * @eventType org.gestouch.events.ZoomGestureEvent + */ [Event(name="gestureZoom", type="org.gestouch.events.ZoomGestureEvent")] /** - * TODO: - * -check native behavior on iDevice * * @author Pavel fljot */ public class ZoomGesture extends Gesture { - public var slop:Number = Gesture.DEFAULT_SLOP >> 1; + public var slop:Number = Gesture.DEFAULT_SLOP; public var lockAspectRatio:Boolean = true; protected var _touch1:Touch; protected var _touch2:Touch; protected var _transformVector:Point; + protected var _initialDistance:Number; - public function ZoomGesture(target:InteractiveObject = null) + public function ZoomGesture(target:Object = null) { super(target); } @@ -52,12 +55,17 @@ package org.gestouch.gestures // // -------------------------------------------------------------------------- + override protected function eventTypeIsValid(type:String):Boolean + { + return type == ZoomGestureEvent.GESTURE_ZOOM || super.eventTypeIsValid(type); + } + + override protected function onTouchBegin(touch:Touch):void { if (touchesCount > 2) { - //TODO - ignoreTouch(touch); + failOrIgnoreTouch(touch); return; } @@ -70,6 +78,7 @@ package org.gestouch.gestures _touch2 = touch; _transformVector = _touch2.location.subtract(_touch1.location); + _initialDistance = _transformVector.length; } } @@ -79,48 +88,59 @@ package org.gestouch.gestures if (touchesCount < 2) return; - var recognized:Boolean = true; + var currTransformVector:Point = _touch2.location.subtract(_touch1.location); + var scaleX:Number; + var scaleY:Number; - if (state == GestureState.POSSIBLE && slop > 0 && touch.locationOffset.length < slop) + if (state == GestureState.POSSIBLE) { - recognized = false; + const d:Number = currTransformVector.length - _initialDistance; + const absD:Number = d >= 0 ? d : -d; + if (absD < slop) + { + // Not recognized yet + return; + } + + if (slop > 0) + { + // adjust _transformVector to avoid initial "jump" + const slopVector:Point = currTransformVector.clone(); + slopVector.normalize(_initialDistance + (d >= 0 ? slop : -slop)); + _transformVector = slopVector; + } } - if (recognized) + + if (lockAspectRatio) { - var currTransformVector:Point = _touch2.location.subtract(_touch1.location); - var scaleX:Number; - var scaleY:Number; - if (lockAspectRatio) + scaleX = scaleY = currTransformVector.length / _transformVector.length; + } + else + { + scaleX = currTransformVector.x / _transformVector.x; + scaleY = currTransformVector.y / _transformVector.y; + } + + _transformVector.x = currTransformVector.x; + _transformVector.y = currTransformVector.y; + + updateLocation(); + + if (state == GestureState.POSSIBLE) + { + if (setState(GestureState.BEGAN) && hasEventListener(ZoomGestureEvent.GESTURE_ZOOM)) { - scaleX = scaleY = currTransformVector.length / _transformVector.length; + dispatchEvent(new ZoomGestureEvent(ZoomGestureEvent.GESTURE_ZOOM, false, false, GestureState.BEGAN, + _location.x, _location.y, _localLocation.x, _localLocation.y, scaleX, scaleY)); } - else + } + else + { + if (setState(GestureState.CHANGED) && hasEventListener(ZoomGestureEvent.GESTURE_ZOOM)) { - scaleX = currTransformVector.x / _transformVector.x; - scaleY = currTransformVector.y / _transformVector.y; - } - - _transformVector.x = currTransformVector.x; - _transformVector.y = currTransformVector.y; - - updateLocation(); - - if (state == GestureState.POSSIBLE) - { - if (setState(GestureState.BEGAN) && hasEventListener(ZoomGestureEvent.GESTURE_ZOOM)) - { - dispatchEvent(new ZoomGestureEvent(ZoomGestureEvent.GESTURE_ZOOM, false, false, GestureState.BEGAN, - _location.x, _location.y, _localLocation.x, _localLocation.y, scaleX, scaleY)); - } - } - else - { - if (setState(GestureState.CHANGED) && hasEventListener(ZoomGestureEvent.GESTURE_ZOOM)) - { - dispatchEvent(new ZoomGestureEvent(ZoomGestureEvent.GESTURE_ZOOM, false, false, GestureState.CHANGED, - _location.x, _location.y, _localLocation.x, _localLocation.y, scaleX, scaleY)); - } + dispatchEvent(new ZoomGestureEvent(ZoomGestureEvent.GESTURE_ZOOM, false, false, GestureState.CHANGED, + _location.x, _location.y, _localLocation.x, _localLocation.y, scaleX, scaleY)); } } } @@ -141,7 +161,7 @@ package org.gestouch.gestures else if (state == GestureState.POSSIBLE) { setState(GestureState.FAILED); - } + } } else//== 1 { diff --git a/src/org/gestouch/input/AbstractInputAdapter.as b/src/org/gestouch/input/AbstractInputAdapter.as deleted file mode 100644 index 8543af5..0000000 --- a/src/org/gestouch/input/AbstractInputAdapter.as +++ /dev/null @@ -1,51 +0,0 @@ -package org.gestouch.input -{ - import org.gestouch.core.IGesturesManager; - import org.gestouch.core.IInputAdapter; - import org.gestouch.core.ITouchesManager; - - - /** - * @author Pavel fljot - */ - public class AbstractInputAdapter implements IInputAdapter - { - protected var _touchesManager:ITouchesManager; - protected var _gesturesManager:IGesturesManager; - - - public function AbstractInputAdapter() - { - if (Object(this).constructor == AbstractInputAdapter) - { - throw new Error("This is abstract class and should not be directly instantiated."); - } - } - - - public function set touchesManager(value:ITouchesManager):void - { - _touchesManager = value; - } - - - public function set gesturesManager(value:IGesturesManager):void - { - _gesturesManager = value; - } - - - [Abstract] - public function init():void - { - throw new Error("This is abstract method."); - } - - - [Abstract] - public function dispose():void - { - throw new Error("This is abstract method."); - } - } -} \ No newline at end of file diff --git a/src/org/gestouch/input/MouseInputAdapter.as b/src/org/gestouch/input/MouseInputAdapter.as deleted file mode 100644 index 4a3e8ea..0000000 --- a/src/org/gestouch/input/MouseInputAdapter.as +++ /dev/null @@ -1,136 +0,0 @@ -package org.gestouch.input -{ - import org.gestouch.core.Touch; - import org.gestouch.core.gestouch_internal; - - import flash.display.InteractiveObject; - import flash.display.Stage; - import flash.events.EventPhase; - import flash.events.MouseEvent; - import flash.geom.Point; - import flash.utils.getTimer; - - - /** - * @author Pavel fljot - */ - public class MouseInputAdapter extends AbstractInputAdapter - { - private static const PRIMARY_TOUCH_POINT_ID:uint = 0; - - protected var _stage:Stage; - - - public function MouseInputAdapter(stage:Stage) - { - super(); - - if (!stage) - { - throw new Error("Stage must be not null."); - } - - _stage = stage; - } - - - override public function init():void - { - _stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, true); - _stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);// to catch with EventPhase.AT_TARGET - } - - - override public function dispose():void - { - _stage.removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, true); - _stage.removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler); - uninstallStageListeners(); - } - - - protected function installStageListeners():void - { - // Maximum priority to prevent event hijacking - _stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, true, int.MAX_VALUE); - _stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, false, int.MAX_VALUE); - _stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, true, int.MAX_VALUE); - _stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, false, int.MAX_VALUE); - } - - - protected function uninstallStageListeners():void - { - _stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, true); - _stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler); - _stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, true); - _stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler); - } - - - protected function mouseDownHandler(event:MouseEvent):void - { - if (event.eventPhase == EventPhase.BUBBLING_PHASE) - return;//we listen in capture or at_target (to catch on empty stage) - // Way to prevent MouseEvent/TouchEvent collisions. - // Also helps to ignore possible fake events. - if (_touchesManager.hasTouch(PRIMARY_TOUCH_POINT_ID)) - return; - - installStageListeners(); - - var touch:Touch = _touchesManager.createTouch(); - touch.target = event.target as InteractiveObject; - touch.id = PRIMARY_TOUCH_POINT_ID; - touch.gestouch_internal::setLocation(new Point(event.stageX, event.stageY)); - touch.gestouch_internal::setTime(getTimer()); - touch.gestouch_internal::setBeginTime(getTimer()); - - _touchesManager.addTouch(touch); - - _gesturesManager.gestouch_internal::onTouchBegin(touch); - } - - - protected function mouseMoveHandler(event:MouseEvent):void - { - if (event.eventPhase == EventPhase.BUBBLING_PHASE) - return;//we listen in capture or at_target (to catch on empty stage) - // Way to prevent MouseEvent/TouchEvent collisions. - // Also helps to ignore possible fake events. - if (!_touchesManager.hasTouch(PRIMARY_TOUCH_POINT_ID)) - return; - - var touch:Touch = _touchesManager.getTouch(PRIMARY_TOUCH_POINT_ID); - touch.gestouch_internal::updateLocation(event.stageX, event.stageY); - touch.gestouch_internal::setTime(getTimer()); - - _gesturesManager.gestouch_internal::onTouchMove(touch); - } - - - protected function mouseUpHandler(event:MouseEvent):void - { - if (event.eventPhase == EventPhase.BUBBLING_PHASE) - return;//we listen in capture or at_target (to catch on empty stage) - - // Way to prevent MouseEvent/TouchEvent collisions. - // Also helps to ignore possible fake events. - if (!_touchesManager.hasTouch(PRIMARY_TOUCH_POINT_ID)) - return; - - var touch:Touch = _touchesManager.getTouch(PRIMARY_TOUCH_POINT_ID); - touch.gestouch_internal::updateLocation(event.stageX, event.stageY); - touch.gestouch_internal::setTime(getTimer()); - - _gesturesManager.gestouch_internal::onTouchEnd(touch); - - _touchesManager.removeTouch(touch); - - if (_touchesManager.activeTouchesCount == 0) - { - uninstallStageListeners(); - } - } - } -} \ No newline at end of file diff --git a/src/org/gestouch/input/NativeInputAdapter.as b/src/org/gestouch/input/NativeInputAdapter.as new file mode 100644 index 0000000..7b6e2f5 --- /dev/null +++ b/src/org/gestouch/input/NativeInputAdapter.as @@ -0,0 +1,224 @@ +package org.gestouch.input +{ + import org.gestouch.core.IInputAdapter; + import org.gestouch.core.TouchesManager; + import org.gestouch.core.gestouch_internal; + + import flash.display.InteractiveObject; + import flash.display.Stage; + import flash.events.EventPhase; + import flash.events.MouseEvent; + import flash.events.TouchEvent; + import flash.ui.Multitouch; + import flash.ui.MultitouchInputMode; + + + /** + * @author Pavel fljot + */ + public class NativeInputAdapter implements IInputAdapter + { + protected static const MOUSE_TOUCH_POINT_ID:uint = 0; + + protected var _stage:Stage; + protected var _explicitlyHandleTouchEvents:Boolean; + protected var _explicitlyHandleMouseEvents:Boolean; + + use namespace gestouch_internal; + + + { + Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT; + } + + + public function NativeInputAdapter(stage:Stage, + explicitlyHandleTouchEvents:Boolean = false, + explicitlyHandleMouseEvents:Boolean = false) + { + super(); + + if (!stage) + { + throw new ArgumentError("Stage must be not null."); + } + + _stage = stage; + + _explicitlyHandleTouchEvents = explicitlyHandleTouchEvents; + _explicitlyHandleMouseEvents = explicitlyHandleMouseEvents; + } + + + protected var _touchesManager:TouchesManager; + public function set touchesManager(value:TouchesManager):void + { + _touchesManager = value; + } + + + + + //-------------------------------------------------------------------------- + // + // Public methods + // + //-------------------------------------------------------------------------- + + public function init():void + { + if (Multitouch.supportsTouchEvents || _explicitlyHandleTouchEvents) + { + _stage.addEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler, true); + _stage.addEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler, false); + _stage.addEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler, true); + _stage.addEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler, false); + // Maximum priority to prevent event hijacking and loosing the touch + _stage.addEventListener(TouchEvent.TOUCH_END, touchEndHandler, true, int.MAX_VALUE); + _stage.addEventListener(TouchEvent.TOUCH_END, touchEndHandler, false, int.MAX_VALUE); + } + + if (!Multitouch.supportsTouchEvents || _explicitlyHandleMouseEvents) + { + _stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, true); + _stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, false); + } + } + + + public function onDispose():void + { + _touchesManager = null; + + _stage.removeEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler, true); + _stage.removeEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler, false); + _stage.removeEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler, true); + _stage.removeEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler, false); + _stage.removeEventListener(TouchEvent.TOUCH_END, touchEndHandler, true); + _stage.removeEventListener(TouchEvent.TOUCH_END, touchEndHandler, false); + + _stage.removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, true); + _stage.removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, false); + unstallMouseListeners(); + } + + + + + //-------------------------------------------------------------------------- + // + // Private methods + // + //-------------------------------------------------------------------------- + + protected function installMouseListeners():void + { + _stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, true); + _stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, false); + // Maximum priority to prevent event hijacking + _stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, true, int.MAX_VALUE); + _stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, false, int.MAX_VALUE); + } + + + protected function unstallMouseListeners():void + { + _stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, true); + _stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, false); + // Maximum priority to prevent event hijacking + _stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, true); + _stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, false); + } + + + + + //-------------------------------------------------------------------------- + // + // Event handlers + // + //-------------------------------------------------------------------------- + + protected function touchBeginHandler(event:TouchEvent):void + { + // We listen in EventPhase.CAPTURE_PHASE or EventPhase.AT_TARGET + // (to catch on empty stage) phases only + if (event.eventPhase == EventPhase.BUBBLING_PHASE) + return; + + _touchesManager.onTouchBegin(event.touchPointID, event.stageX, event.stageY, event.target as InteractiveObject); + } + + + protected function touchMoveHandler(event:TouchEvent):void + { + // We listen in EventPhase.CAPTURE_PHASE or EventPhase.AT_TARGET + // (to catch on empty stage) phases only + if (event.eventPhase == EventPhase.BUBBLING_PHASE) + return; + + _touchesManager.onTouchMove(event.touchPointID, event.stageX, event.stageY); + } + + + protected function touchEndHandler(event:TouchEvent):void + { + // We listen in EventPhase.CAPTURE_PHASE or EventPhase.AT_TARGET + // (to catch on empty stage) phases only + if (event.eventPhase == EventPhase.BUBBLING_PHASE) + return; + + if (event.hasOwnProperty("isTouchPointCanceled") && event["isTouchPointCanceled"]) + { + _touchesManager.onTouchCancel(event.touchPointID, event.stageX, event.stageY); + } + else + { + _touchesManager.onTouchEnd(event.touchPointID, event.stageX, event.stageY); + } + } + + + protected function mouseDownHandler(event:MouseEvent):void + { + // We listen in EventPhase.CAPTURE_PHASE or EventPhase.AT_TARGET + // (to catch on empty stage) phases only + if (event.eventPhase == EventPhase.BUBBLING_PHASE) + return; + + const touchAccepted:Boolean = _touchesManager.onTouchBegin(MOUSE_TOUCH_POINT_ID, event.stageX, event.stageY, event.target as InteractiveObject); + + if (touchAccepted) + { + installMouseListeners(); + } + } + + + protected function mouseMoveHandler(event:MouseEvent):void + { + // We listen in EventPhase.CAPTURE_PHASE or EventPhase.AT_TARGET + // (to catch on empty stage) phases only + if (event.eventPhase == EventPhase.BUBBLING_PHASE) + return; + + _touchesManager.onTouchMove(MOUSE_TOUCH_POINT_ID, event.stageX, event.stageY); + } + + + protected function mouseUpHandler(event:MouseEvent):void + { + // We listen in EventPhase.CAPTURE_PHASE or EventPhase.AT_TARGET + // (to catch on empty stage) phases only + if (event.eventPhase == EventPhase.BUBBLING_PHASE) + return; + + _touchesManager.onTouchEnd(MOUSE_TOUCH_POINT_ID, event.stageX, event.stageY); + + if (_touchesManager.activeTouchesCount == 0) + { + unstallMouseListeners(); + } + } + } +} diff --git a/src/org/gestouch/input/TUIOInputAdapter.as b/src/org/gestouch/input/TUIOInputAdapter.as index 5dae3e9..fa90562 100644 --- a/src/org/gestouch/input/TUIOInputAdapter.as +++ b/src/org/gestouch/input/TUIOInputAdapter.as @@ -1,17 +1,33 @@ package org.gestouch.input { - import org.gestouch.input.AbstractInputAdapter; + import org.gestouch.core.IInputAdapter; + import org.gestouch.core.TouchesManager; /** + * TODO: You can implement your own TUIO Input Adapter (and supply touchesManager with + * touch info), but IMHO it is way easier to use NativeInputAdapter and any TUIO library + * and manually dispatch native TouchEvents using DisplayObjectContainer#getObjectsUnderPoint() + * + * @see NativeInputAdapter + * @see http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/DisplayObjectContainer.html#getObjectsUnderPoint() DisplayObjectContainer#getObjectsUnderPoint() + * * @author Pavel fljot */ - public class TUIOInputAdapter extends AbstractInputAdapter + public class TUIOInputAdapter implements IInputAdapter { - public function TUIOInputAdapter() + public function init():void + { + } + + + public function onDispose():void + { + } + + + public function set touchesManager(value:TouchesManager):void { - super(); - //TODO } } -} \ No newline at end of file +} diff --git a/src/org/gestouch/input/TouchInputAdapter.as b/src/org/gestouch/input/TouchInputAdapter.as deleted file mode 100644 index 99f82f4..0000000 --- a/src/org/gestouch/input/TouchInputAdapter.as +++ /dev/null @@ -1,186 +0,0 @@ -package org.gestouch.input -{ - import org.gestouch.core.Touch; - import org.gestouch.core.gestouch_internal; - - import flash.display.InteractiveObject; - import flash.display.Stage; - import flash.events.EventPhase; - import flash.events.TouchEvent; - import flash.geom.Point; - import flash.ui.Multitouch; - import flash.ui.MultitouchInputMode; - import flash.utils.getTimer; - - - /** - * @author Pavel fljot - */ - public class TouchInputAdapter extends AbstractInputAdapter - { - protected var _stage:Stage; - /** - * The hash map of touches instantiated via TouchEvent. - * Used to avoid collisions (double processing) with MouseInputAdapter. - * - * TODO: any better way? - */ - protected var _touchesMap:Object = {}; - - { - Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT; - } - - - public function TouchInputAdapter(stage:Stage) - { - super(); - - if (!stage) - { - throw new Error("Stage must be not null."); - } - - _stage = stage; - } - - - override public function init():void - { - _stage.addEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler, true); - _stage.addEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler);// to catch with EventPhase.AT_TARGET - } - - - override public function dispose():void - { - _stage.removeEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler, true); - _stage.removeEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler); - uninstallStageListeners(); - } - - - protected function installStageListeners():void - { - // Maximum priority to prevent event hijacking - _stage.addEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler, true, int.MAX_VALUE); - _stage.addEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler, false, int.MAX_VALUE); - _stage.addEventListener(TouchEvent.TOUCH_END, touchEndHandler, true, int.MAX_VALUE); - _stage.addEventListener(TouchEvent.TOUCH_END, touchEndHandler, false, int.MAX_VALUE); - } - - - protected function uninstallStageListeners():void - { - _stage.removeEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler, true); - _stage.removeEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler); - _stage.removeEventListener(TouchEvent.TOUCH_END, touchEndHandler, true); - _stage.removeEventListener(TouchEvent.TOUCH_END, touchEndHandler); - } - - - protected function touchBeginHandler(event:TouchEvent):void - { - if (event.eventPhase == EventPhase.BUBBLING_PHASE) - return;//we listen in capture or at_target (to catch on empty stage) - // Way to prevent MouseEvent/TouchEvent collisions. - // Also helps to ignore possible fake events. - if (_touchesManager.hasTouch(event.touchPointID)) - return; - - installStageListeners(); - - var touch:Touch = _touchesManager.createTouch(); - touch.id = event.touchPointID; - touch.target = event.target as InteractiveObject; - touch.gestouch_internal::setLocation(new Point(event.stageX, event.stageY)); - touch.sizeX = event.sizeX; - touch.sizeY = event.sizeY; - touch.pressure = event.pressure; - //TODO: conditional compilation? - if (event.hasOwnProperty("timestamp")) - { - touch.gestouch_internal::setTime(event["timestamp"]); - touch.gestouch_internal::setBeginTime(event["timestamp"]); - } - else - { - touch.gestouch_internal::setTime(getTimer()); - touch.gestouch_internal::setBeginTime(getTimer()); - } - - _touchesManager.addTouch(touch); - _touchesMap[touch.id] = true; - - _gesturesManager.gestouch_internal::onTouchBegin(touch); - } - - - protected function touchMoveHandler(event:TouchEvent):void - { - if (event.eventPhase == EventPhase.BUBBLING_PHASE) - return;//we listen in capture or at_target (to catch on empty stage) - // Way to prevent MouseEvent/TouchEvent collisions. - // Also helps to ignore possible fake events. - if (!_touchesManager.hasTouch(event.touchPointID) || !_touchesMap.hasOwnProperty(event.touchPointID)) - return; - - var touch:Touch = _touchesManager.getTouch(event.touchPointID); - touch.gestouch_internal::updateLocation(event.stageX, event.stageY); - touch.sizeX = event.sizeX; - touch.sizeY = event.sizeY; - touch.pressure = event.pressure; - //TODO: conditional compilation? - if (event.hasOwnProperty("timestamp")) - { - touch.gestouch_internal::setTime(event["timestamp"]); - } - else - { - touch.gestouch_internal::setTime(getTimer()); - } - - _gesturesManager.gestouch_internal::onTouchMove(touch); - } - - - protected function touchEndHandler(event:TouchEvent):void - { - if (event.eventPhase == EventPhase.BUBBLING_PHASE) - return;//we listen in capture or at_target (to catch on empty stage) - - // Way to prevent MouseEvent/TouchEvent collisions. - // Also helps to ignore possible fake events. - if (!_touchesManager.hasTouch(event.touchPointID)) - return; - - var touch:Touch = _touchesManager.getTouch(event.touchPointID); - touch.gestouch_internal::updateLocation(event.stageX, event.stageY); - touch.sizeX = event.sizeX; - touch.sizeY = event.sizeY; - touch.pressure = event.pressure; - //TODO: conditional compilation? - if (event.hasOwnProperty("timestamp")) - { - touch.gestouch_internal::setTime(event["timestamp"]); - } - else - { - touch.gestouch_internal::setTime(getTimer()); - } - - _gesturesManager.gestouch_internal::onTouchEnd(touch); - - _touchesManager.removeTouch(touch); - delete _touchesMap[touch.id]; - - if (_touchesManager.activeTouchesCount == 0) - { - uninstallStageListeners(); - } - - // TODO: handle cancelled touch: - // if (event.hasOwnProperty("isTouchPointCanceled") && event["isTouchPointCanceled"] && ... - } - } -} \ No newline at end of file diff --git a/src/org/gestouch/utils/GestureUtils.as b/src/org/gestouch/utils/GestureUtils.as index 4d57d69..13eef7b 100644 --- a/src/org/gestouch/utils/GestureUtils.as +++ b/src/org/gestouch/utils/GestureUtils.as @@ -1,5 +1,6 @@ package org.gestouch.utils { + import flash.geom.Point; import flash.system.Capabilities; /** * Set of constants. @@ -24,5 +25,6 @@ package org.gestouch.utils * Precalculated coefficient Math.PI * 2 */ public static const PI_DOUBLE:Number = Math.PI * 2; + public static const GLOBAL_ZERO:Point = new Point(); } } \ No newline at end of file diff --git a/version.properties b/version.properties index 2354cd1..a01ef79 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -project.version = 0.3.1 \ No newline at end of file +project.version = 0.4 \ No newline at end of file